blob: 3bcffa3ae4af56e1eb8afdcd24353dd0ef968de2 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.web.bindery.requestfactory.shared.impl;
import static com.google.web.bindery.requestfactory.shared.impl.BaseProxyCategory.stableId;
import static com.google.web.bindery.requestfactory.shared.impl.Constants.REQUEST_CONTEXT_STATE;
import static com.google.web.bindery.requestfactory.shared.impl.Constants.STABLE_ID;
import com.google.web.bindery.autobean.shared.AutoBean;
import com.google.web.bindery.autobean.shared.AutoBeanCodex;
import com.google.web.bindery.autobean.shared.AutoBeanFactory;
import com.google.web.bindery.autobean.shared.AutoBeanUtils;
import com.google.web.bindery.autobean.shared.AutoBeanVisitor;
import com.google.web.bindery.autobean.shared.Splittable;
import com.google.web.bindery.autobean.shared.ValueCodex;
import com.google.web.bindery.autobean.shared.impl.AbstractAutoBean;
import com.google.web.bindery.autobean.shared.impl.EnumMap;
import com.google.web.bindery.autobean.shared.impl.StringQuoter;
import com.google.web.bindery.event.shared.UmbrellaException;
import com.google.web.bindery.requestfactory.shared.BaseProxy;
import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.EntityProxyChange;
import com.google.web.bindery.requestfactory.shared.EntityProxyId;
import com.google.web.bindery.requestfactory.shared.FanoutReceiver;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestTransport.TransportReceiver;
import com.google.web.bindery.requestfactory.shared.ServerFailure;
import com.google.web.bindery.requestfactory.shared.WriteOperation;
import com.google.web.bindery.requestfactory.shared.impl.posers.DatePoser;
import com.google.web.bindery.requestfactory.shared.messages.IdMessage;
import com.google.web.bindery.requestfactory.shared.messages.IdMessage.Strength;
import com.google.web.bindery.requestfactory.shared.messages.InvocationMessage;
import com.google.web.bindery.requestfactory.shared.messages.JsonRpcRequest;
import com.google.web.bindery.requestfactory.shared.messages.MessageFactory;
import com.google.web.bindery.requestfactory.shared.messages.OperationMessage;
import com.google.web.bindery.requestfactory.shared.messages.RequestMessage;
import com.google.web.bindery.requestfactory.shared.messages.ResponseMessage;
import com.google.web.bindery.requestfactory.shared.messages.ServerFailureMessage;
import com.google.web.bindery.requestfactory.shared.messages.ViolationMessage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.metadata.ConstraintDescriptor;
/**
* Base implementations for RequestContext services.
*/
public abstract class AbstractRequestContext implements RequestContext, EntityCodex.EntitySource {
/**
* Allows the payload dialect to be injected into the AbstractRequestContext without the caller
* needing to be concerned with how the implementation object is instantiated.
*/
public enum Dialect {
STANDARD {
@Override
DialectImpl create(AbstractRequestContext context) {
return context.new StandardPayloadDialect();
}
},
JSON_RPC {
@Override
DialectImpl create(AbstractRequestContext context) {
return context.new JsonRpcPayloadDialect();
}
};
abstract DialectImpl create(AbstractRequestContext context);
}
/**
* Encapsulates all state contained by the AbstractRequestContext.
*/
protected static class State {
/**
* Supports the case where chained contexts are used and a response comes back from the server
* with a proxy type not reachable from the canonical context.
*/
public Set<AbstractRequestContext> appendedContexts;
public final AbstractRequestContext canonical;
public final DialectImpl dialect;
public FanoutReceiver<Void> fanout;
/**
* When {@code true} the {@link AbstractRequestContext#fire()} method will be a no-op.
*/
public boolean fireDisabled;
public final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
public boolean locked;
/**
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=5952.
*/
public boolean diffing;
/**
* A map of all EntityProxies that the RequestContext has interacted with. Objects are placed
* into this map by being returned from {@link #create}, passed into {@link #edit}, or used as
* an invocation argument.
*/
public final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies =
new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>();
/**
* A map that contains the canonical instance of an entity to return in the return graph, since
* this is built from scratch.
*/
public final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies =
new HashMap<SimpleProxyId<?>, AutoBean<?>>();
public final AbstractRequestFactory requestFactory;
/**
* A map that allows us to handle the case where the server has sent back an unpersisted entity.
* Because we assume that the server is stateless, the client will need to swap out the
* request-local ids with a regular client-allocated id.
*/
public final Map<Integer, SimpleProxyId<?>> syntheticIds =
new HashMap<Integer, SimpleProxyId<?>>();
public State(AbstractRequestFactory requestFactory, DialectImpl dialect,
AbstractRequestContext canonical) {
this.requestFactory = requestFactory;
this.canonical = canonical;
this.dialect = dialect;
}
public void addContext(AbstractRequestContext ctx) {
if (appendedContexts == null) {
appendedContexts = Collections.singleton(ctx);
} else {
if (appendedContexts.size() == 1) {
appendedContexts = new LinkedHashSet<AbstractRequestContext>(appendedContexts);
}
appendedContexts.add(ctx);
}
}
public AbstractRequestContext getCanonicalContext() {
return canonical;
}
public boolean isClean() {
return editedProxies.isEmpty() && invocations.isEmpty() && !locked
&& returnedProxies.isEmpty() && syntheticIds.isEmpty();
}
public boolean isCompatible(State state) {
// Object comparison intentional
return requestFactory == state.requestFactory
&& dialect.getClass().equals(state.dialect.getClass());
}
}
interface DialectImpl {
void addInvocation(AbstractRequest<?> request);
String makePayload();
void processPayload(Receiver<Void> receiver, String payload);
}
class JsonRpcPayloadDialect implements DialectImpl {
/**
* Called by generated subclasses to enqueue a method invocation.
*/
public void addInvocation(AbstractRequest<?> request) {
/*
* TODO(bobv): Support for multiple invocations per request needs to be ironed out. Once this
* is done, addInvocation() can be removed from the DialectImpl interface and restored to to
* AbstractRequestContext.
*/
if (!state.invocations.isEmpty()) {
throw new RuntimeException("Only one invocation per request, pending backend support");
}
state.invocations.add(request);
for (Object arg : request.getRequestData().getOrderedParameters()) {
retainArg(arg);
}
}
public String makePayload() {
RequestData data = state.invocations.get(0).getRequestData();
AutoBean<JsonRpcRequest> bean = MessageFactoryHolder.FACTORY.jsonRpcRequest();
JsonRpcRequest request = bean.as();
request.setVersion("2.0");
request.setApiVersion(data.getApiVersion());
request.setId(payloadId++);
Map<String, Splittable> params = new HashMap<String, Splittable>();
for (Map.Entry<String, Object> entry : data.getNamedParameters().entrySet()) {
Object obj = entry.getValue();
Splittable value = encode(obj);
params.put(entry.getKey(), value);
}
if (data.getRequestResource() != null) {
params.put("resource", encode(data.getRequestResource()));
}
request.setParams(params);
request.setMethod(data.getOperation());
return AutoBeanCodex.encode(bean).getPayload();
}
public void processPayload(Receiver<Void> receiver, String payload) {
Splittable raw = StringQuoter.split(payload);
@SuppressWarnings("unchecked")
Receiver<Object> callback = (Receiver<Object>) state.invocations.get(0).getReceiver();
if (!raw.isNull("error")) {
Splittable error = raw.get("error");
ServerFailure failure =
new ServerFailure(error.get("message").asString(), error.get("code").asString(),
payload, true);
fail(receiver, failure);
return;
}
Splittable result = raw.get("result");
@SuppressWarnings("unchecked")
Class<BaseProxy> target =
(Class<BaseProxy>) state.invocations.get(0).getRequestData().getReturnType();
SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target);
AutoBean<BaseProxy> bean = createProxy(target, id, true);
// XXX expose this as a proper API
((AbstractAutoBean<?>) bean).setData(result);
// AutoBeanCodex.decodeInto(result, bean);
if (callback != null) {
callback.onSuccess(bean.as());
}
if (receiver != null) {
receiver.onSuccess(null);
}
}
Splittable encode(Object obj) {
if (obj == null) {
return Splittable.NULL;
} else if (obj instanceof Collection) {
return collectionEncode((Collection<?>) obj);
}
return nonCollectionEncode(obj);
}
private Splittable collectionEncode(Collection<?> collection) {
StringBuffer sb = new StringBuffer();
Iterator<?> it = collection.iterator();
sb.append("[");
if (it.hasNext()) {
// TODO: Allow for the encoding of nested collections. See issue 5974.
sb.append(nonCollectionEncode(it.next()).getPayload());
while (it.hasNext()) {
sb.append(",");
// TODO: Allow for the encoding of nested collections. See issue 5974.
sb.append(nonCollectionEncode(it.next()).getPayload());
}
}
sb.append("]");
return StringQuoter.split(sb.toString());
}
private Splittable nonCollectionEncode(Object obj) {
if (obj instanceof Collection) {
// TODO: Allow for the encoding of nested collections. See issue 5974.
// Once we do this, this can turn into an assert.
throw new RuntimeException(
"Unable to encode request as JSON payload; Request methods must have parameters of the form List<T> or Set<T>, where T is a scalar (non-collection) type.");
}
Splittable value;
if (obj instanceof Enum && getAutoBeanFactory() instanceof EnumMap) {
value = ValueCodex.encode(((EnumMap) getAutoBeanFactory()).getToken((Enum<?>) obj));
} else if (ValueCodex.canDecode(obj.getClass())) {
value = ValueCodex.encode(obj);
} else {
// XXX user-provided implementation of interface?
value = AutoBeanCodex.encode(AutoBeanUtils.getAutoBean(obj));
}
return value;
}
}
class StandardPayloadDialect implements DialectImpl {
/**
* Called by generated subclasses to enqueue a method invocation.
*/
public void addInvocation(AbstractRequest<?> request) {
state.invocations.add(request);
for (Object arg : request.getRequestData().getOrderedParameters()) {
retainArg(arg);
}
}
/**
* Assemble all of the state that has been accumulated in this context. This includes:
* <ul>
* <li>Diffs accumulated on objects passed to {@link #edit}.
* <li>Invocations accumulated as Request subtypes passed to {@link #addInvocation}.
* </ul>
*/
public String makePayload() {
// Get the factory from the runtime-specific holder.
MessageFactory f = MessageFactoryHolder.FACTORY;
List<OperationMessage> operations = makePayloadOperations();
List<InvocationMessage> invocationMessages = makePayloadInvocations();
// Create the outer envelope message
AutoBean<RequestMessage> bean = f.request();
RequestMessage requestMessage = bean.as();
requestMessage.setRequestFactory(getRequestFactory().getFactoryTypeToken());
if (!invocationMessages.isEmpty()) {
requestMessage.setInvocations(invocationMessages);
}
if (!operations.isEmpty()) {
requestMessage.setOperations(operations);
}
return AutoBeanCodex.encode(bean).getPayload();
}
public void processPayload(final Receiver<Void> receiver, String payload) {
ResponseMessage response =
AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ResponseMessage.class, payload).as();
if (response.getGeneralFailure() != null) {
ServerFailureMessage failure = response.getGeneralFailure();
ServerFailure fail =
new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure
.getStackTrace(), failure.isFatal());
fail(receiver, fail);
return;
}
// Process violations and then stop
if (response.getViolations() != null) {
Set<ConstraintViolation<?>> errors = new HashSet<ConstraintViolation<?>>();
for (ViolationMessage message : response.getViolations()) {
errors.add(new MyConstraintViolation(message));
}
violation(receiver, errors);
return;
}
// Process operations
processReturnOperations(response);
// Send return values
Set<Throwable> causes = null;
for (int i = 0, j = state.invocations.size(); i < j; i++) {
try {
if (response.getStatusCodes().get(i)) {
state.invocations.get(i).onSuccess(response.getInvocationResults().get(i));
} else {
ServerFailureMessage failure =
AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ServerFailureMessage.class,
response.getInvocationResults().get(i)).as();
state.invocations.get(i).onFail(
new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure
.getStackTrace(), failure.isFatal()));
}
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
if (receiver != null) {
try {
receiver.onSuccess(null);
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
// After success, shut down the context
state.editedProxies.clear();
state.invocations.clear();
state.returnedProxies.clear();
if (causes != null) {
throw new UmbrellaException(causes);
}
}
}
private class MyConstraintViolation implements ConstraintViolation<BaseProxy> {
private final BaseProxy leafBean;
private final String messageTemplate;
private final String message;
private final String path;
private final BaseProxy rootBean;
private final Class<? extends BaseProxy> rootBeanClass;
public MyConstraintViolation(ViolationMessage msg) {
AutoBean<? extends BaseProxy> leafProxy = findEditedProxy(msg.getLeafBeanId());
leafBean = leafProxy == null ? null : leafProxy.as();
message = msg.getMessage();
messageTemplate = msg.getMessageTemplate();
path = msg.getPath();
AutoBean<? extends BaseProxy> rootProxy = findEditedProxy(msg.getRootBeanId());
rootBeanClass = rootProxy.getType();
rootBean = rootProxy.as();
}
public ConstraintDescriptor<?> getConstraintDescriptor() {
return null;
}
public Object getInvalidValue() {
return null;
}
public Object getLeafBean() {
return leafBean;
}
public String getMessage() {
return message;
}
public String getMessageTemplate() {
return messageTemplate;
}
public Path getPropertyPath() {
return new Path() {
public Iterator<Node> iterator() {
return Collections.<Node> emptyList().iterator();
}
@Override
public String toString() {
return path;
}
};
}
public BaseProxy getRootBean() {
return rootBean;
}
@SuppressWarnings("unchecked")
public Class<BaseProxy> getRootBeanClass() {
return (Class<BaseProxy>) rootBeanClass;
}
private AutoBean<? extends BaseProxy> findEditedProxy(IdMessage idMessage) {
// Support violations for value objects.
SimpleProxyId<BaseProxy> rootId = getId(idMessage);
// The stub is empty, since we don't process any OperationMessages
AutoBean<BaseProxy> stub = getProxyForReturnPayloadGraph(rootId);
// So pick up the instance that we just sent to the server
return state.editedProxies.get(BaseProxyCategory.stableId(stub));
}
}
private static final WriteOperation[] DELETE_ONLY = {WriteOperation.DELETE};
private static final WriteOperation[] PERSIST_AND_UPDATE = {
WriteOperation.PERSIST, WriteOperation.UPDATE};
private static final WriteOperation[] UPDATE_ONLY = {WriteOperation.UPDATE};
private static int payloadId = 100;
private State state;
protected AbstractRequestContext(AbstractRequestFactory factory, Dialect dialect) {
setState(new State(factory, dialect.create(this), this));
}
public <T extends RequestContext> T append(T other) {
AbstractRequestContext child = (AbstractRequestContext) other;
if (!state.isCompatible(child.state)) {
throw new IllegalStateException(getClass().getName() + " and " + child.getClass().getName()
+ " are not compatible");
}
if (!child.state.isClean()) {
throw new IllegalStateException("The provided RequestContext has been changed");
}
child.setState(state);
return other;
}
/**
* Create a new object, with an ephemeral id.
*/
public <T extends BaseProxy> T create(Class<T> clazz) {
checkLocked();
SimpleProxyId<T> id = state.requestFactory.allocateId(clazz);
AutoBean<T> created = createProxy(clazz, id, false);
return takeOwnership(created);
}
public <T extends BaseProxy> T edit(T object) {
return editProxy(object);
}
/**
* Take ownership of a proxy instance and make it editable.
*/
public <T extends BaseProxy> T editProxy(T object) {
AutoBean<T> bean = checkStreamsNotCrossed(object);
checkLocked();
@SuppressWarnings("unchecked")
AutoBean<T> previouslySeen =
(AutoBean<T>) state.editedProxies.get(BaseProxyCategory.stableId(bean));
if (previouslySeen != null && !previouslySeen.isFrozen()) {
/*
* If we've seen the object before, it might be because it was passed in as a method argument.
* This does not guarantee its mutability, so check that here before returning the cached
* object.
*/
return previouslySeen.as();
}
// Create editable copies
AutoBean<T> parent = bean;
bean = cloneBeanAndCollections(bean);
bean.setTag(Constants.PARENT_OBJECT, parent);
return bean.as();
}
@Override
public <P extends EntityProxy> Request<P> find(final EntityProxyId<P> proxyId) {
return new AbstractRequest<P>(this) {
{
requestContext.addInvocation(this);
}
@Override
protected RequestData makeRequestData() {
// This method is normally generated, hence the ugly constructor
return new RequestData(Constants.FIND_METHOD_OPERATION, new Object[] {proxyId},
propertyRefs, proxyId.getProxyClass(), null);
}
};
}
/**
* Make sure there's a default receiver so errors don't get dropped. This behavior should be
* revisited when chaining is supported, depending on whether or not chained invocations can fail
* independently.
*/
public void fire() {
boolean needsReceiver = true;
for (AbstractRequest<?> request : state.invocations) {
if (request.hasReceiver()) {
needsReceiver = false;
break;
}
}
if (needsReceiver) {
doFire(new Receiver<Void>() {
@Override
public void onSuccess(Void response) {
// Don't care
}
});
} else {
doFire(null);
}
}
public void fire(final Receiver<Void> receiver) {
if (receiver == null) {
throw new IllegalArgumentException();
}
doFire(receiver);
}
/**
* EntityCodex support.
*/
public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(Splittable serializedProxyId) {
IdMessage ref =
AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, IdMessage.class, serializedProxyId).as();
@SuppressWarnings("unchecked")
SimpleProxyId<Q> id = (SimpleProxyId<Q>) getId(ref);
return getProxyForReturnPayloadGraph(id);
}
public AbstractRequestFactory getRequestFactory() {
return state.requestFactory;
}
/**
* EntityCodex support.
*/
public Splittable getSerializedProxyId(SimpleProxyId<?> stableId) {
AutoBean<IdMessage> bean = MessageFactoryHolder.FACTORY.id();
IdMessage ref = bean.as();
ref.setServerId(stableId.getServerId());
ref.setTypeToken(getRequestFactory().getTypeToken(stableId.getProxyClass()));
if (stableId.isSynthetic()) {
ref.setStrength(Strength.SYNTHETIC);
ref.setSyntheticId(stableId.getSyntheticId());
} else if (stableId.isEphemeral()) {
ref.setStrength(Strength.EPHEMERAL);
ref.setClientId(stableId.getClientId());
}
return AutoBeanCodex.encode(bean);
}
public boolean isChanged() {
/*
* NB: Don't use the presence of ephemeral objects for this test.
*
* Diff the objects until one is found to be different. It's not just a
* simple flag-check because of the possibility of "unmaking" a change, per
* the JavaDoc.
*
* TODO: try to get rid of the 'diffing' flag and optimize the diffing of
* objects: http://code.google.com/p/google-web-toolkit/issues/detail?id=7379
*/
assert !state.diffing;
state.diffing = true;
try {
for (AutoBean<? extends BaseProxy> bean : state.editedProxies.values()) {
AutoBean<?> previous = bean.getTag(Constants.PARENT_OBJECT);
if (previous == null) {
// Compare to empty object
Class<?> proxyClass = stableId(bean).getProxyClass();
previous = getAutoBeanFactory().create(proxyClass);
}
if (!AutoBeanUtils.diff(previous, bean).isEmpty()) {
return true;
}
}
return false;
} finally {
state.diffing = false;
}
}
/**
* EntityCodex support.
*/
public boolean isEntityType(Class<?> clazz) {
return state.requestFactory.isEntityType(clazz);
}
public boolean isLocked() {
return state.locked;
}
/**
* EntityCodex support.
*/
public boolean isValueType(Class<?> clazz) {
return state.requestFactory.isValueType(clazz);
}
public void setFireDisabled(boolean disabled) {
state.fireDisabled = disabled;
}
/**
* Called by generated subclasses to enqueue a method invocation.
*/
protected void addInvocation(AbstractRequest<?> request) {
state.dialect.addInvocation(request);
};
/**
* Creates a new proxy with an assigned ID.
*
* @param clazz The proxy type
* @param id The id to be assigned to the new proxy
* @param useAppendedContexts if {@code true} use the AutoBeanFactory types associated with any
* contexts that have been passed into {@link #append(RequestContext)}. If {@code false},
* this method will only create proxy types reachable from the implemented RequestContext
* interface.
* @throws IllegalArgumentException if the requested proxy type cannot be created
*/
protected <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id,
boolean useAppendedContexts) {
AutoBean<T> created = null;
if (useAppendedContexts) {
for (AbstractRequestContext ctx : state.appendedContexts) {
created = ctx.getAutoBeanFactory().create(clazz);
if (created != null) {
break;
}
}
} else {
created = getAutoBeanFactory().create(clazz);
}
if (created != null) {
created.setTag(STABLE_ID, id);
return created;
}
throw new IllegalArgumentException("Unknown proxy type " + clazz.getName());
}
/**
* Invoke the appropriate {@code onFailure} callbacks, possibly throwing an
* {@link UmbrellaException} if one or more callbacks fails.
*/
protected void fail(Receiver<Void> receiver, ServerFailure failure) {
reuse();
failure.setRequestContext(this);
Set<Throwable> causes = null;
for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(state.invocations)) {
try {
request.onFail(failure);
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
if (receiver != null) {
try {
receiver.onFailure(failure);
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
if (causes != null) {
throw new UmbrellaException(causes);
}
}
/**
* Returns an AutoBeanFactory that can produce the types reachable only from this RequestContext.
*/
protected abstract AutoBeanFactory getAutoBeanFactory();
/**
* Invoke the appropriate {@code onViolation} callbacks, possibly throwing an
* {@link UmbrellaException} if one or more callbacks fails.
*/
protected void violation(final Receiver<Void> receiver, Set<ConstraintViolation<?>> errors) {
reuse();
Set<Throwable> causes = null;
for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(state.invocations)) {
try {
request.onViolation(errors);
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
if (receiver != null) {
try {
receiver.onConstraintViolation(errors);
} catch (Throwable t) {
if (causes == null) {
causes = new HashSet<Throwable>();
}
causes.add(t);
}
}
if (causes != null) {
throw new UmbrellaException(causes);
}
}
/**
* Resolves an IdMessage into an SimpleProxyId.
*/
SimpleProxyId<BaseProxy> getId(IdMessage op) {
if (Strength.SYNTHETIC.equals(op.getStrength())) {
return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
}
return state.requestFactory.getId(op.getTypeToken(), op.getServerId(), op.getClientId());
}
/**
* Creates or retrieves a new canonical AutoBean to represent the given id in the returned
* payload.
*/
<Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(SimpleProxyId<Q> id) {
@SuppressWarnings("unchecked")
AutoBean<Q> bean = (AutoBean<Q>) state.returnedProxies.get(id);
if (bean == null) {
Class<Q> proxyClass = id.getProxyClass();
bean = createProxy(proxyClass, id, true);
state.returnedProxies.put(id, bean);
}
return bean;
}
/**
* Whether the RequestContext is currently diffing proxies.
* <p>
* This flag is used in {@link BaseProxyCategory} and
* {@link EntityProxyCategory} to influence the way proxies are being
* compared for equality, and to prevent auto-editing proxies when
* walking reference properties.
* <p>
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=5952
* <p>
* TODO: try to get rid of this flag.
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=7379
*/
boolean isDiffing() {
return state.diffing;
}
/**
* Create a single OperationMessage that encapsulates the state of a proxy AutoBean.
*/
AutoBean<OperationMessage> makeOperationMessage(SimpleProxyId<BaseProxy> stableId,
AutoBean<?> proxyBean, boolean useDelta) {
// The OperationMessages describes operations on exactly one entity
AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation();
OperationMessage operation = toReturn.as();
operation.setTypeToken(state.requestFactory.getTypeToken(stableId.getProxyClass()));
// Find the object to compare against
AutoBean<?> parent;
if (stableId.isEphemeral()) {
// Newly-created object, use a blank object to compare against
parent = createProxy(stableId.getProxyClass(), stableId, true);
// Newly-created objects go into the persist operation bucket
operation.setOperation(WriteOperation.PERSIST);
// The ephemeral id is passed to the server
operation.setClientId(stableId.getClientId());
operation.setStrength(Strength.EPHEMERAL);
} else if (stableId.isSynthetic()) {
// Newly-created object, use a blank object to compare against
parent = createProxy(stableId.getProxyClass(), stableId, true);
// Newly-created objects go into the persist operation bucket
operation.setOperation(WriteOperation.PERSIST);
// The ephemeral id is passed to the server
operation.setSyntheticId(stableId.getSyntheticId());
operation.setStrength(Strength.SYNTHETIC);
} else {
parent = proxyBean.getTag(Constants.PARENT_OBJECT);
// Requests involving existing objects use the persisted id
operation.setServerId(stableId.getServerId());
operation.setOperation(WriteOperation.UPDATE);
}
assert !useDelta || parent != null;
// Send our version number to the server to cut down on future payloads
String version = proxyBean.getTag(Constants.VERSION_PROPERTY_B64);
if (version != null) {
operation.setVersion(version);
}
Map<String, Object> diff = Collections.emptyMap();
if (isEntityType(stableId.getProxyClass())) {
// Compute what's changed on the client
diff =
useDelta ? AutoBeanUtils.diff(parent, proxyBean) : AutoBeanUtils
.getAllProperties(proxyBean);
} else if (isValueType(stableId.getProxyClass())) {
// Send everything
diff = AutoBeanUtils.getAllProperties(proxyBean);
}
if (!diff.isEmpty()) {
Map<String, Splittable> propertyMap = new HashMap<String, Splittable>();
for (Map.Entry<String, Object> entry : diff.entrySet()) {
propertyMap.put(entry.getKey(), EntityCodex.encode(this, entry.getValue()));
}
operation.setPropertyMap(propertyMap);
}
return toReturn;
}
/**
* Create a new EntityProxy from a snapshot in the return payload.
*
* @param id the EntityProxyId of the object
* @param returnRecord the JSON map containing property/value pairs
* @param operations the WriteOperation eventns to broadcast over the EventBus
*/
<Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, OperationMessage op,
WriteOperation... operations) {
AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id);
toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion());
final Map<String, Splittable> properties = op.getPropertyMap();
if (properties != null) {
// Apply updates
toMutate.accept(new AutoBeanVisitor() {
@Override
public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
PropertyContext ctx) {
if (ctx.canSet()) {
if (properties.containsKey(propertyName)) {
Splittable raw = properties.get(propertyName);
Class<?> elementType =
ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx)
.getElementType() : null;
Object decoded =
EntityCodex.decode(AbstractRequestContext.this, ctx.getType(), elementType, raw);
ctx.set(decoded);
}
}
return false;
}
@Override
public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
if (ctx.canSet()) {
if (properties.containsKey(propertyName)) {
Splittable raw = properties.get(propertyName);
Object decoded = ValueCodex.decode(ctx.getType(), raw);
/*
* Hack for Date subtypes, consider generalizing for "custom serializers"
*/
if (decoded != null && Date.class.equals(ctx.getType())) {
decoded = new DatePoser((Date) decoded);
}
ctx.set(decoded);
}
}
return false;
}
});
}
// Finished applying updates, freeze the bean
makeImmutable(toMutate);
Q proxy = toMutate.as();
/*
* Notify subscribers if the object differs from when it first came into the RequestContext.
*/
if (operations != null && state.requestFactory.isEntityType(id.getProxyClass())) {
for (WriteOperation writeOperation : operations) {
if (writeOperation.equals(WriteOperation.UPDATE)
&& !state.requestFactory.hasVersionChanged(id, op.getVersion())) {
// No updates if the server reports no change
continue;
}
state.requestFactory.getEventBus().fireEventFromSource(
new EntityProxyChange<EntityProxy>((EntityProxy) proxy, writeOperation),
id.getProxyClass());
}
}
return proxy;
}
/**
* Get-or-create method for synthetic ids.
*
* @see #syntheticIds
*/
private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId(String typeToken,
int syntheticId) {
@SuppressWarnings("unchecked")
SimpleProxyId<Q> toReturn = (SimpleProxyId<Q>) state.syntheticIds.get(syntheticId);
if (toReturn == null) {
toReturn =
state.requestFactory.allocateId(state.requestFactory.<Q> getTypeFromToken(typeToken));
state.syntheticIds.put(syntheticId, toReturn);
}
return toReturn;
}
private void checkLocked() {
if (state.locked) {
throw new IllegalStateException("A request is already in progress");
}
}
/**
* This method checks that a proxy object is either immutable, or already edited by this context.
*/
private <T> AutoBean<T> checkStreamsNotCrossed(T object) {
AutoBean<T> bean = AutoBeanUtils.getAutoBean(object);
if (bean == null) {
// Unexpected; some kind of foreign implementation?
throw new IllegalArgumentException(object.getClass().getName());
}
State otherState = bean.getTag(REQUEST_CONTEXT_STATE);
if (!bean.isFrozen() && otherState != this.state) {
/*
* This means something is way off in the weeds. If a bean is editable, it's supposed to be
* associated with a RequestContext.
*/
assert otherState != null : "Unfrozen bean with null RequestContext";
/*
* Already editing the object in another context or it would have been in the editing map.
*/
throw new IllegalArgumentException("Attempting to edit an EntityProxy"
+ " previously edited by another RequestContext");
}
return bean;
}
/**
* Shallow-clones an autobean and makes duplicates of the collection types. A regular
* {@link AutoBean#clone} won't duplicate reference properties.
*/
private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections(final AutoBean<T> toClone) {
AutoBean<T> clone = toClone.getFactory().create(toClone.getType());
clone.setTag(STABLE_ID, toClone.getTag(STABLE_ID));
clone.setTag(Constants.VERSION_PROPERTY_B64, toClone.getTag(Constants.VERSION_PROPERTY_B64));
/*
* Take ownership here to prevent cycles in value objects from overflowing the stack.
*/
takeOwnership(clone);
clone.accept(new AutoBeanVisitor() {
final Map<String, Object> values = AutoBeanUtils.getAllProperties(toClone);
@Override
public boolean visitCollectionProperty(String propertyName, AutoBean<Collection<?>> value,
CollectionPropertyContext ctx) {
// javac generics bug
value =
AutoBeanUtils.<Collection<?>, Collection<?>> getAutoBean((Collection<?>) values
.get(propertyName));
if (value != null) {
Collection<Object> collection;
if (List.class == ctx.getType()) {
collection = new ArrayList<Object>();
} else if (Set.class == ctx.getType()) {
collection = new HashSet<Object>();
} else {
// Should not get here if the validator works correctly
throw new IllegalArgumentException(ctx.getType().getName());
}
if (isValueType(ctx.getElementType()) || isEntityType(ctx.getElementType())) {
/*
* Proxies must be edited up-front so that the elements in the collection have stable
* identity.
*/
for (Object o : value.as()) {
if (o == null) {
collection.add(null);
} else {
collection.add(editProxy((BaseProxy) o));
}
}
} else {
// For simple values, just copy the values
collection.addAll(value.as());
}
ctx.set(collection);
}
return false;
}
@Override
public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
PropertyContext ctx) {
value = AutoBeanUtils.getAutoBean(values.get(propertyName));
if (value != null) {
if (isValueType(ctx.getType()) || isEntityType(ctx.getType())) {
/*
* Value proxies must be cloned upfront, since the value is replaced outright.
*/
@SuppressWarnings("unchecked")
AutoBean<BaseProxy> valueBean = (AutoBean<BaseProxy>) value;
ctx.set(editProxy(valueBean.as()));
} else {
ctx.set(value.as());
}
}
return false;
}
@Override
public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
ctx.set(values.get(propertyName));
return false;
}
});
return clone;
}
private void doFire(Receiver<Void> receiver) {
final Receiver<Void> finalReceiver;
if (state.fireDisabled) {
if (receiver != null) {
if (state.fanout == null) {
state.fanout = new FanoutReceiver<Void>();
}
state.fanout.add(receiver);
}
return;
} else if (state.fanout != null) {
if (receiver != null) {
state.fanout.add(receiver);
}
finalReceiver = state.fanout;
} else {
finalReceiver = receiver;
}
checkLocked();
state.locked = true;
freezeEntities(true);
String payload = state.dialect.makePayload();
state.requestFactory.getRequestTransport().send(payload, new TransportReceiver() {
public void onTransportFailure(ServerFailure failure) {
fail(finalReceiver, failure);
}
public void onTransportSuccess(String payload) {
state.dialect.processPayload(finalReceiver, payload);
}
});
}
/**
* Set the frozen status of all EntityProxies owned by this context.
*/
private void freezeEntities(boolean frozen) {
for (AutoBean<?> bean : state.editedProxies.values()) {
bean.setFrozen(frozen);
}
}
/**
* Make an EntityProxy immutable.
*/
private void makeImmutable(final AutoBean<? extends BaseProxy> toMutate) {
// Always diff'ed against itself, producing a no-op
toMutate.setTag(Constants.PARENT_OBJECT, toMutate);
// Act with entity-identity semantics
toMutate.setTag(REQUEST_CONTEXT_STATE, null);
toMutate.setFrozen(true);
}
/**
* Create an InvocationMessage for each remote method call being made by the context.
*/
private List<InvocationMessage> makePayloadInvocations() {
MessageFactory f = MessageFactoryHolder.FACTORY;
List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>();
for (AbstractRequest<?> invocation : state.invocations) {
// RequestData is produced by the generated subclass
RequestData data = invocation.getRequestData();
InvocationMessage message = f.invocation().as();
// Operation; essentially a method descriptor
message.setOperation(data.getOperation());
// The arguments to the with() calls
Set<String> refsToSend = data.getPropertyRefs();
if (!refsToSend.isEmpty()) {
message.setPropertyRefs(refsToSend);
}
// Parameter values or references
List<Splittable> parameters = new ArrayList<Splittable>(data.getOrderedParameters().length);
for (Object param : data.getOrderedParameters()) {
parameters.add(EntityCodex.encode(this, param));
}
if (!parameters.isEmpty()) {
message.setParameters(parameters);
}
invocationMessages.add(message);
}
return invocationMessages;
}
/**
* Compute deltas for each entity seen by the context.
* <p>
* TODO(t.broyer): reduce payload size by only sending proxies that are
* directly referenced by invocation arguments or by other proxies. For
* backwards-compatibility with no-op requests and operation-only requests,
* only do so when there's at least one invocation (or we can choose to
* break backwards compatibility for those edge-cases).
*/
private List<OperationMessage> makePayloadOperations() {
assert isLocked();
assert !state.diffing;
state.diffing = true;
try {
List<OperationMessage> operations = new ArrayList<OperationMessage>();
for (AutoBean<? extends BaseProxy> currentView : state.editedProxies.values()) {
OperationMessage operation =
makeOperationMessage(BaseProxyCategory.stableId(currentView), currentView, true).as();
operations.add(operation);
}
return operations;
} finally {
state.diffing = false;
}
}
/**
* Process an array of OperationMessages.
*/
private void processReturnOperations(ResponseMessage response) {
List<OperationMessage> ops = response.getOperations();
// If there are no observable effects, this will be null
if (ops == null) {
return;
}
for (OperationMessage op : ops) {
SimpleProxyId<?> id = getId(op);
WriteOperation[] toPropagate = null;
// May be null if the server is returning an unpersisted object
WriteOperation effect = op.getOperation();
if (effect != null) {
switch (effect) {
case DELETE:
toPropagate = DELETE_ONLY;
break;
case PERSIST:
toPropagate = PERSIST_AND_UPDATE;
break;
case UPDATE:
toPropagate = UPDATE_ONLY;
break;
default:
// Should never reach here
throw new RuntimeException(effect.toString());
}
}
processReturnOperation(id, op, toPropagate);
}
assert state.returnedProxies.size() == ops.size();
}
/**
* Ensures that any method arguments are retained in the context's sphere of influence.
*/
private void retainArg(Object arg) {
if (arg instanceof Iterable<?>) {
for (Object o : (Iterable<?>) arg) {
retainArg(o);
}
} else if (arg instanceof BaseProxy) {
// Calling edit will validate and set up the tracking we need
edit((BaseProxy) arg);
}
}
/**
* Returns the requests that were dequeued as part of reusing the context.
*/
private void reuse() {
freezeEntities(false);
state.locked = false;
}
private void setState(State state) {
this.state = state;
state.addContext(this);
}
/**
* Make the EnityProxy bean edited and owned by this RequestContext.
*/
private <T extends BaseProxy> T takeOwnership(AutoBean<T> bean) {
state.editedProxies.put(stableId(bean), bean);
bean.setTag(REQUEST_CONTEXT_STATE, this.state);
return bean.as();
}
}