First pass at implementing ValueProxy support. Resolve issue 5522, issue 5373, issue 5559. http://code.google.com/p/google-web-toolkit/wiki/RequestFactory_2_1_1 Patch by: bobv Review by: rchandia,rjrjr Review at http://gwt-code-reviews.appspot.com/1108802 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9252 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/console/HttpClientTransport.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/console/HttpClientTransport.java index 4325979..8b5b0c5 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/console/HttpClientTransport.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/console/HttpClientTransport.java
@@ -41,7 +41,6 @@ this.uri = uri; } - @Override public void send(String payload, TransportReceiver receiver) { HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost();
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Address.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Address.java index c7059bd..815f349 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Address.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Address.java
@@ -15,8 +15,6 @@ */ package com.google.gwt.sample.dynatablerf.domain; -import com.google.gwt.sample.dynatablerf.server.SchoolCalendarService; - import javax.validation.constraints.DecimalMin; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -26,14 +24,6 @@ * Represents an address. */ public class Address { - /** - * The RequestFactory requires a static finder method for each proxied type. - * Soon it should allow you to customize how instances are found. - */ - public static Address findAddress(String id) { - return SchoolCalendarService.findPerson(id).getAddress(); - } - @NotNull @Size(min = 1) private String city; @@ -77,10 +67,6 @@ return city; } - public String getId() { - return id; - } - public String getState() { return state; } @@ -89,10 +75,6 @@ return street; } - public Integer getVersion() { - return version; - } - public String getZip() { return zip; } @@ -101,23 +83,10 @@ return new Address(this); } - /** - * When this was written the RequestFactory required a persist method per - * type. That requirement should be relaxed very soon (and may well have been - * already if we forget to update this comment). - */ - public void persist() { - SchoolCalendarService.persist(this); - } - public void setCity(String city) { this.city = city; } - public void setId(String id) { - this.id = id; - } - public void setState(String state) { this.state = state; } @@ -126,10 +95,6 @@ this.street = street; } - public void setVersion(Integer version) { - this.version = version; - } - public void setZip(String zip) { this.zip = zip; }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java index 3c10fd8..6f65c67 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java
@@ -50,7 +50,7 @@ } @NotNull - private final Address address = new Address(); + private Address address = new Address(); @NotNull private Schedule classSchedule = new Schedule(); @@ -158,7 +158,7 @@ } public void setAddress(Address address) { - this.address.copyFrom(address); + this.address = address; } public void setDaysFilter(List<Boolean> daysFilter) { @@ -172,7 +172,6 @@ public void setId(String id) { this.id = id; - address.setId(id); } public void setMentor(Person mentor) {
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/PersonSource.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/PersonSource.java index 19b06e5..1279342 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/PersonSource.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/PersonSource.java
@@ -18,7 +18,6 @@ import static com.google.gwt.sample.dynatablerf.shared.DynaTableRequestFactory.SchoolCalendarRequest.ALL_DAYS; import static com.google.gwt.sample.dynatablerf.shared.DynaTableRequestFactory.SchoolCalendarRequest.NO_DAYS; -import com.google.gwt.sample.dynatablerf.domain.Address; import com.google.gwt.sample.dynatablerf.domain.Person; import java.util.ArrayList; @@ -90,12 +89,6 @@ } @Override - public void persist(Address address) { - address.setVersion(address.getVersion() + 1); - findPerson(address.getId()).getAddress().copyFrom(address); - } - - @Override public void persist(Person person) { if (person.getId() == null) { person.setId(Long.toString(++serial)); @@ -149,11 +142,6 @@ } @Override - public void persist(Address address) { - backingStore.persist(address); - } - - @Override public void persist(Person person) { backingStore.persist(person); } @@ -187,7 +175,5 @@ public abstract List<Person> getPeople(int startIndex, int maxCount, List<Boolean> daysFilter); - public abstract void persist(Address address); - public abstract void persist(Person person); }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/SchoolCalendarService.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/SchoolCalendarService.java index 5493cd7..b4097cf 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/SchoolCalendarService.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/server/SchoolCalendarService.java
@@ -17,7 +17,6 @@ import static com.google.gwt.sample.dynatablerf.shared.DynaTableRequestFactory.SchoolCalendarRequest.ALL_DAYS; -import com.google.gwt.sample.dynatablerf.domain.Address; import com.google.gwt.sample.dynatablerf.domain.Person; import java.io.IOException; @@ -62,11 +61,6 @@ ALL_DAYS).get(0); } - public static void persist(Address address) { - checkPersonSource(); - PERSON_SOURCE.get().persist(address); - } - public static void persist(Person person) { checkPersonSource(); PERSON_SOURCE.get().persist(person);
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/AddressProxy.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/AddressProxy.java index f96e5c8..8e87ef6 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/AddressProxy.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/AddressProxy.java
@@ -15,16 +15,15 @@ */ package com.google.gwt.sample.dynatablerf.shared; -import com.google.gwt.requestfactory.shared.EntityProxy; -import com.google.gwt.requestfactory.shared.EntityProxyId; import com.google.gwt.requestfactory.shared.ProxyFor; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.sample.dynatablerf.domain.Address; /** * Represents an Address in the client code. */ -@ProxyFor(Address.class) -public interface AddressProxy extends EntityProxy { +@ProxyFor(value = Address.class) +public interface AddressProxy extends ValueProxy { String getCity(); String getState(); @@ -40,6 +39,4 @@ void setStreet(String street); void setZip(String zip); - - EntityProxyId<AddressProxy> stableId(); }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/DynaTableRequestFactory.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/DynaTableRequestFactory.java index 0a0e38a..ce18e88 100644 --- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/DynaTableRequestFactory.java +++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/shared/DynaTableRequestFactory.java
@@ -21,7 +21,6 @@ import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Service; -import com.google.gwt.sample.dynatablerf.domain.Address; import com.google.gwt.sample.dynatablerf.domain.Person; import com.google.gwt.sample.dynatablerf.server.SchoolCalendarService; @@ -34,15 +33,6 @@ * {@link com.google.gwt.core.client.GWT#create}. */ public interface DynaTableRequestFactory extends RequestFactory { - - /** - * Source of request objects for the Address class. - */ - @Service(Address.class) - interface AddressRequest extends RequestContext { - InstanceRequest<AddressProxy, Void> persist(); - } - /** * Source of request objects for the Person class. */ @@ -67,8 +57,6 @@ Request<PersonProxy> getRandomPerson(); } - AddressRequest addressRequest(); - LoggingRequest loggingRequest(); PersonRequest personRequest();
diff --git a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java index f3a0bb2..e8aa4bb 100644 --- a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java +++ b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
@@ -24,11 +24,11 @@ import java.util.List; /** - * Implements the EntityCodex.Splittable interface + * Implements the EntityCodex.Splittable interface using a raw JavaScriptObject. */ public final class JsoSplittable extends JavaScriptObject implements Splittable { /** - * This type is only used in DevMode because we can't treat Strings as JSOs. + * This type is used because we can't treat Strings as JSOs. */ public static class StringSplittable implements Splittable { private final String value;
diff --git a/user/src/com/google/gwt/autobean/server/BeanMethod.java b/user/src/com/google/gwt/autobean/server/BeanMethod.java index 117bf54..24c2009 100644 --- a/user/src/com/google/gwt/autobean/server/BeanMethod.java +++ b/user/src/com/google/gwt/autobean/server/BeanMethod.java
@@ -138,13 +138,21 @@ Class<?>[] searchParams = new Class<?>[declaredParams.length + 1]; searchParams[0] = AutoBean.class; System.arraycopy(declaredParams, 0, searchParams, 1, declaredParams.length); + Class<?> autoBeanType = handler.getBean().getType(); for (Class<?> clazz : handler.getBean().getConfiguration().getCategories()) { try { Method found = clazz.getMethod(method.getName(), searchParams); - if (Modifier.isStatic(found.getModifiers())) { - return found; + if (!Modifier.isStatic(found.getModifiers())) { + continue; } + // Check the AutoBean parameterization of the 0th argument + Class<?> foundAutoBean = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization( + AutoBean.class, found.getGenericParameterTypes()[0])); + if (!foundAutoBean.isAssignableFrom(autoBeanType)) { + continue; + } + return found; } catch (NoSuchMethodException expected) { } catch (IllegalArgumentException e) { throw new RuntimeException(e);
diff --git a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java index a15d27b..b331ad5 100644 --- a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java +++ b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java
@@ -15,6 +15,8 @@ */ package com.google.gwt.autobean.server; +import com.google.gwt.autobean.shared.AutoBeanCodex; + import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -47,4 +49,12 @@ } throw new RuntimeException("Unhandled invocation " + method.getName()); } + + /** + * For debugging use only. + */ + @Override + public String toString() { + return AutoBeanCodex.encode(bean).getPayload(); + } } \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java index 6b9f22e..6550f82 100644 --- a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java +++ b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
@@ -138,6 +138,21 @@ return array.length(); } + /** + * For debugging use only. + */ + @Override + public String toString() { + if (obj != null) { + return obj.toString(); + } else if (array != null) { + return array.toString(); + } else if (string != null) { + return string; + } + return "<Uninitialized>"; + } + private JsonSplittable makeSplittable(Object object) { if (object instanceof JSONObject) { return new JsonSplittable((JSONObject) object);
diff --git a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java index d4d954b..c7c64ab 100644 --- a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java +++ b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java
@@ -60,6 +60,7 @@ static { Map<Class<?>, Class<?>> autoBoxMap = new HashMap<Class<?>, Class<?>>(); + autoBoxMap.put(boolean.class, Boolean.class); autoBoxMap.put(byte.class, Byte.class); autoBoxMap.put(char.class, Character.class); autoBoxMap.put(double.class, Double.class);
diff --git a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java index b82af0e..d55d45f 100644 --- a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java +++ b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java
@@ -20,7 +20,7 @@ * convert enum values to strings. */ public interface EnumMap { - public <E extends Enum<E>> E getEnum(Class<E> clazz, String token); + <E extends Enum<E>> E getEnum(Class<E> clazz, String token); - public String getToken(Enum<?> e); + String getToken(Enum<?> e); }
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java index 50fd171..2e22c51 100644 --- a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java +++ b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
@@ -24,6 +24,7 @@ import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestFactory; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.Violation; import java.util.ArrayList; @@ -38,10 +39,34 @@ public abstract class AbstractRequestFactoryEditorDriver<R, E extends Editor<R>> implements RequestFactoryEditorDriver<R, E> { + /** + * Since the ValueProxy is being mutated in-place, we need a way to stabilize + * its hashcode for future equality checks. + */ + private static class ValueProxyHolder { + private final ValueProxy proxy; + + public ValueProxyHolder(ValueProxy proxy) { + this.proxy = proxy; + } + + @Override + public boolean equals(Object o) { + return proxy.equals(((ValueProxyHolder) o).proxy); + } + + @Override + public int hashCode() { + return proxy.getClass().hashCode(); + } + } + private static final DelegateMap.KeyMethod PROXY_ID_KEY = new DelegateMap.KeyMethod() { - public Object key(Object object) { + public Object key(final Object object) { if (object instanceof EntityProxy) { return ((EntityProxy) object).stableId(); + } else if (object instanceof ValueProxy) { + return new ValueProxyHolder((ValueProxy) object); } return null; } @@ -115,7 +140,11 @@ * Find the delegates that are attached to the object. Use getRaw() here * since the violation doesn't include an EntityProxy reference */ - List<AbstractEditorDelegate<?, ?>> delegateList = delegateMap.getRaw(error.getProxyId()); + Object key = error.getInvalidProxy(); + // Object key = error.getOriginalProxy(); + // if (key == null) { + // } + List<AbstractEditorDelegate<?, ?>> delegateList = delegateMap.get(key); if (delegateList != null) { // For each delegate editing some record...
diff --git a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java index b7eb331..916eee7 100644 --- a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java +++ b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
@@ -33,19 +33,23 @@ import com.google.gwt.requestfactory.client.impl.AbstractClientRequestFactory; import com.google.gwt.requestfactory.rebind.model.ContextMethod; import com.google.gwt.requestfactory.rebind.model.EntityProxyModel; +import com.google.gwt.requestfactory.rebind.model.EntityProxyModel.Type; import com.google.gwt.requestfactory.rebind.model.RequestFactoryModel; import com.google.gwt.requestfactory.rebind.model.RequestMethod; import com.google.gwt.requestfactory.shared.EntityProxyId; import com.google.gwt.requestfactory.shared.impl.AbstractRequest; import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext; import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory; +import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory; import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory; import com.google.gwt.requestfactory.shared.impl.RequestData; +import com.google.gwt.requestfactory.shared.impl.ValueProxyCategory; import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; import com.google.gwt.user.rebind.SourceWriter; import java.io.PrintWriter; import java.util.HashMap; +import java.util.HashSet; /** * Generates implementations of @@ -96,8 +100,11 @@ private void writeAutoBeanFactory(SourceWriter sw) { // Map in static implementations of EntityProxy methods - sw.println("@%s(%s.class)", Category.class.getCanonicalName(), - EntityProxyCategory.class.getCanonicalName()); + sw.println("@%s({%s.class, %s.class, %s.class})", + Category.class.getCanonicalName(), + EntityProxyCategory.class.getCanonicalName(), + ValueProxyCategory.class.getCanonicalName(), + BaseProxyCategory.class.getCanonicalName()); // Don't wrap our id type, because it makes code grungy sw.println("@%s(%s.class)", NoWrap.class.getCanonicalName(), EntityProxyId.class.getCanonicalName()); @@ -251,6 +258,12 @@ + " = new %1$s<String, Class<?>>();", HashMap.class.getCanonicalName()); sw.println("private static final %1$s<Class<?>, String> typesToTokens" + " = new %1$s<Class<?>, String>();", HashMap.class.getCanonicalName()); + sw.println( + "private static final %1$s<Class<?>> entityProxyTypes = new %1$s<Class<?>>();", + HashSet.class.getCanonicalName()); + sw.println( + "private static final %1$s<Class<?>> valueProxyTypes = new %1$s<Class<?>>();", + HashSet.class.getCanonicalName()); sw.println("static {"); sw.indent(); for (EntityProxyModel type : model.getAllProxyModels()) { @@ -260,6 +273,10 @@ // typesToTokens.put(Foo.class, Foo); sw.println("typesToTokens.put(%1$s.class, \"%1$s\");", type.getQualifiedSourceName()); + // fooProxyTypes.add(MyFooProxy.class); + sw.println("%s.add(%s.class);", type.getType().equals(Type.ENTITY) + ? "entityProxyTypes" : "valueProxyTypes", + type.getQualifiedSourceName()); } sw.outdent(); sw.println("}"); @@ -271,5 +288,11 @@ sw.println("@Override protected String getTypeToken(Class type) {"); sw.indentln("return typesToTokens.get(type);"); sw.println("}"); + sw.println("@Override public boolean isEntityType(Class<?> type) {"); + sw.indentln("return entityProxyTypes.contains(type);"); + sw.println("}"); + sw.println("@Override public boolean isValueType(Class<?> type) {"); + sw.indentln("return valueProxyTypes.contains(type);"); + sw.println("}"); } }
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java index 52989d1..7676035 100644 --- a/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java +++ b/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java
@@ -23,6 +23,14 @@ */ public class EntityProxyModel { /** + * The kind of proxy. This is an enum in case more proxy types are defined in + * the future. + */ + public enum Type { + ENTITY, VALUE + } + + /** * Builds {@link EntityProxyModel}. */ public static class Builder { @@ -56,11 +64,16 @@ public void setRequestMethods(List<RequestMethod> requestMethods) { toReturn.requestMethods = requestMethods; } + + public void setType(Type type) { + toReturn.type = type; + } } private Class<?> proxyFor; private String qualifiedSourceName; private List<RequestMethod> requestMethods; + private Type type; private EntityProxyModel() { } @@ -77,6 +90,10 @@ return Collections.unmodifiableList(requestMethods); } + public Type getType() { + return type; + } + /** * For debugging use only. */
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java index be2eb96..56d859a 100644 --- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java +++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
@@ -24,6 +24,7 @@ import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.editor.rebind.model.ModelUtils; +import com.google.gwt.requestfactory.rebind.model.EntityProxyModel.Type; import com.google.gwt.requestfactory.rebind.model.RequestMethod.CollectionType; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.InstanceRequest; @@ -34,6 +35,7 @@ import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Service; import com.google.gwt.requestfactory.shared.ServiceName; +import com.google.gwt.requestfactory.shared.ValueProxy; import java.util.ArrayList; import java.util.Collection; @@ -61,13 +63,13 @@ return "Unable to create RequestFactoryModel model due to previous errors"; } - private final TreeLogger logger; private final JClassType collectionInterface; private final List<ContextMethod> contextMethods = new ArrayList<ContextMethod>(); private final JClassType entityProxyInterface; private final JClassType factoryType; private final JClassType instanceRequestInterface; private final JClassType listInterface; + private final TreeLogger logger; private final TypeOracle oracle; /** * This map prevents cyclic type dependencies from overflowing the stack. @@ -82,6 +84,7 @@ private final JClassType requestContextInterface; private final JClassType requestFactoryInterface; private final JClassType requestInterface; + private final JClassType valueProxyInterface; public RequestFactoryModel(TreeLogger logger, JClassType factoryType) throws UnableToCompleteException { @@ -96,6 +99,7 @@ requestContextInterface = oracle.findType(RequestContext.class.getCanonicalName()); requestFactoryInterface = oracle.findType(RequestFactory.class.getCanonicalName()); requestInterface = oracle.findType(Request.class.getCanonicalName()); + valueProxyInterface = oracle.findType(ValueProxy.class.getCanonicalName()); for (JMethod method : factoryType.getOverridableMethods()) { if (method.getEnclosingType().equals(requestFactoryInterface)) { @@ -206,6 +210,17 @@ peerBuilders.put(entityProxyType, builder); builder.setQualifiedSourceName(ModelUtils.getQualifiedBaseName(entityProxyType)); + if (entityProxyInterface.isAssignableFrom(entityProxyType)) { + builder.setType(Type.ENTITY); + } else if (valueProxyInterface.isAssignableFrom(entityProxyType)) { + builder.setType(Type.VALUE); + } else { + poison("The type %s is not assignable to either %s or %s", + entityProxyInterface.getQualifiedSourceName(), + valueProxyInterface.getQualifiedSourceName()); + // Cannot continue, since knowing the behavior is crucial + die(poisonedMessage()); + } // Get the server domain object type ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class); @@ -332,8 +347,9 @@ if (ModelUtils.isValueType(oracle, transportedClass)) { // Simple values, like Integer and String methodBuilder.setValueType(true); - } else if (entityProxyInterface.isAssignableFrom(transportedClass)) { - // EntityProxy return types + } else if (entityProxyInterface.isAssignableFrom(transportedClass) + || valueProxyInterface.isAssignableFrom(transportedClass)) { + // EntityProxy and ValueProxy return types methodBuilder.setEntityType(getEntityProxyType(transportedClass)); } else if (collectionInterface.isAssignableFrom(transportedClass)) { // Only allow certain collections for now
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java index c2bfc34..6ffb4c2 100644 --- a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java +++ b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
@@ -18,19 +18,24 @@ import com.google.gwt.autobean.server.impl.TypeUtils; import com.google.gwt.autobean.shared.ValueCodex; import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; +import com.google.gwt.requestfactory.shared.InstanceRequest; import com.google.gwt.requestfactory.shared.ProxyFor; import com.google.gwt.requestfactory.shared.ProxyForName; +import com.google.gwt.requestfactory.shared.Request; import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.Service; import com.google.gwt.requestfactory.shared.ServiceName; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.messages.EntityCodex; import com.google.gwt.requestfactory.shared.messages.EntityCodex.EntitySource; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Set; @@ -91,13 +96,14 @@ clazz.getCanonicalName()); } - public Class<?> getClientType(Class<?> domainClass) { + public Class<?> getClientType(Class<?> domainClass, Class<?> clientClass) { String name; synchronized (validator) { - name = validator.getEntityProxyTypeName(domainClass.getName()); + name = validator.getEntityProxyTypeName(domainClass.getName(), + clientClass.getName()); } if (name != null) { - return forName(name).asSubclass(EntityProxy.class); + return forName(name).asSubclass(BaseProxy.class); } if (List.class.isAssignableFrom(domainClass)) { return List.class; @@ -117,7 +123,7 @@ return List.class; } else if (Set.class.equals(clazz)) { return Set.class; - } else if (EntityProxy.class.isAssignableFrom(clazz)) { + } else if (BaseProxy.class.isAssignableFrom(clazz)) { ProxyFor pf = clazz.getAnnotation(ProxyFor.class); if (pf != null) { return pf.value(); @@ -135,7 +141,7 @@ public String getFlatId(EntitySource source, Object domainObject) { Object id = getProperty(domainObject, "id"); if (id == null) { - report("Could not retrieve id property for domain object"); + return null; } if (!isKeyType(id.getClass())) { die(null, "The type %s is not a valid key type", @@ -165,6 +171,23 @@ return die(toReport, "Could not retrieve property %s", property); } + public Type getRequestReturnType(Method contextMethod) { + Class<?> returnClass = contextMethod.getReturnType(); + if (InstanceRequest.class.isAssignableFrom(returnClass)) { + Type[] params = TypeUtils.getParameterization(InstanceRequest.class, + contextMethod.getGenericReturnType()); + assert params.length == 2; + return params[1]; + } else if (Request.class.isAssignableFrom(returnClass)) { + Type param = TypeUtils.getSingleParameterization(Request.class, + contextMethod.getGenericReturnType()); + return param; + } else { + throw new IllegalArgumentException("Unknown RequestContext return type " + + returnClass.getCanonicalName()); + } + } + public String getTypeToken(Class<?> clazz) { return clazz.getName(); } @@ -173,7 +196,7 @@ // TODO: Make version any value type Object version = getProperty(domainObject, "version"); if (version == null) { - report("Could not retrieve version property"); + return 0; } if (!(version instanceof Integer)) { die(null, "The getVersion() method on type %s did not return" @@ -244,21 +267,23 @@ return die(ex, "Cauld not load domain object using id", id.toString()); } - public Class<? extends EntityProxy> resolveClass(String typeToken) { + public Class<? extends BaseProxy> resolveClass(String typeToken) { Class<?> found = forName(typeToken); - if (!EntityProxy.class.isAssignableFrom(found)) { - die(null, "The requested type %s is not assignable to %s", typeToken, - EntityProxy.class.getName()); + if (!EntityProxy.class.isAssignableFrom(found) + && !ValueProxy.class.isAssignableFrom(found)) { + die(null, "The requested type %s is not assignable to %s or %s", + typeToken, EntityProxy.class.getCanonicalName(), + ValueProxy.class.getCanonicalName()); } synchronized (validator) { validator.antidote(); - validator.validateEntityProxy(found.getName()); + validator.validateProxy(found.getName()); if (validator.isPoisoned()) { die(null, "The type %s did not pass RequestFactory validation", found.getCanonicalName()); } } - return found.asSubclass(EntityProxy.class); + return found.asSubclass(BaseProxy.class); } public Method resolveDomainMethod(Method requestContextMethod) { @@ -289,8 +314,8 @@ Class<?>[] parameterTypes = requestContextMethod.getParameterTypes(); Class<?>[] domainArgs = new Class<?>[parameterTypes.length]; for (int i = 0, j = domainArgs.length; i < j; i++) { - if (EntityProxy.class.isAssignableFrom(parameterTypes[i])) { - domainArgs[i] = getDomainClass(parameterTypes[i].asSubclass(EntityProxy.class)); + if (BaseProxy.class.isAssignableFrom(parameterTypes[i])) { + domainArgs[i] = getDomainClass(parameterTypes[i].asSubclass(BaseProxy.class)); } else if (EntityProxyId.class.isAssignableFrom(parameterTypes[i])) { domainArgs[i] = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization( EntityProxyId.class, @@ -346,7 +371,7 @@ report(e); return; } - die(ex, "Could not locate getter for property %s in type %s", property, + die(ex, "Could not locate setter for property %s in type %s", property, domainObject.getClass().getCanonicalName()); } @@ -381,7 +406,7 @@ private boolean isKeyType(Class<?> clazz) { return ValueCodex.canDecode(clazz) - || EntityProxy.class.isAssignableFrom(clazz); + || BaseProxy.class.isAssignableFrom(clazz); } /**
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java index bbe81e9..2108542 100644 --- a/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java +++ b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
@@ -28,6 +28,7 @@ import com.google.gwt.dev.util.Name; import com.google.gwt.dev.util.Name.BinaryName; import com.google.gwt.dev.util.Name.SourceOrBinaryName; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.InstanceRequest; import com.google.gwt.requestfactory.shared.ProxyFor; @@ -37,6 +38,7 @@ import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Service; import com.google.gwt.requestfactory.shared.ServiceName; +import com.google.gwt.requestfactory.shared.ValueProxy; import java.io.IOException; import java.io.InputStream; @@ -138,6 +140,7 @@ private class DomainMapper extends EmptyVisitor { private final ErrorContext logger; private String domainInternalName; + private boolean isValueType; public DomainMapper(ErrorContext logger) { this.logger = logger; @@ -148,6 +151,10 @@ return domainInternalName; } + public boolean isValueType() { + return isValueType; + } + @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { @@ -168,9 +175,12 @@ @Override public void visit(String name, Object value) { - domainInternalName = ((Type) value).getInternalName(); + if ("value".equals(name)) { + domainInternalName = ((Type) value).getInternalName(); + } else if ("isValueType".equals(name)) { + isValueType = (Boolean) value; + } } - }; } @@ -178,25 +188,30 @@ return new EmptyVisitor() { @Override public void visit(String name, Object value) { - String sourceName = (String) value; + if ("value".equals(name)) { + String sourceName = (String) value; - /* - * The input is a source name, so we need to convert it to an - * internal name. We'll do this by substituting dollar signs for the - * last slash in the name until there are no more slashes. - */ - StringBuffer desc = new StringBuffer(sourceName.replace('.', '/')); - while (!loader.exists(desc.toString() + ".class")) { - logger.spam("Did not find " + desc.toString()); - int idx = desc.lastIndexOf("/"); - if (idx == -1) { - return; + /* + * The input is a source name, so we need to convert it to an + * internal name. We'll do this by substituting dollar signs for + * the last slash in the name until there are no more slashes. + */ + StringBuffer desc = new StringBuffer(sourceName.replace('.', '/')); + while (!loader.exists(desc.toString() + ".class")) { + logger.spam("Did not find " + desc.toString()); + int idx = desc.lastIndexOf("/"); + if (idx == -1) { + return; + } + desc.setCharAt(idx, '$'); } - desc.setCharAt(idx, '$'); - } - domainInternalName = desc.toString(); - logger.spam(domainInternalName); + domainInternalName = desc.toString(); + logger.spam(domainInternalName); + } else if ("isValueType".equals(name)) { + isValueType = (Boolean) value; + logger.spam("isValueType: %s", isValueType); + } } }; } @@ -399,6 +414,58 @@ } } + /** + * Return all types referenced by a method signature. + */ + private static class TypesInSignatureCollector extends SignatureAdapter { + private final Set<Type> found = new HashSet<Type>(); + + public Type[] getFound() { + return found.toArray(new Type[found.size()]); + } + + public SignatureVisitor visitArrayType() { + return this; + } + + public SignatureVisitor visitClassBound() { + return this; + } + + @Override + public void visitClassType(String name) { + found.add(Type.getObjectType(name)); + } + + public SignatureVisitor visitExceptionType() { + return this; + } + + public SignatureVisitor visitInterface() { + return this; + } + + public SignatureVisitor visitInterfaceBound() { + return this; + } + + public SignatureVisitor visitParameterType() { + return this; + } + + public SignatureVisitor visitReturnType() { + return this; + } + + public SignatureVisitor visitSuperclass() { + return this; + } + + public SignatureVisitor visitTypeArgument(char wildcard) { + return this; + } + } + @SuppressWarnings("unchecked") static final Set<Class<?>> VALUE_TYPES = Collections.unmodifiableSet(new HashSet<Class<?>>( Arrays.asList(Boolean.class, Character.class, Class.class, Date.class, @@ -439,10 +506,19 @@ private final Set<String> badTypes = new HashSet<String>(); /** + * The type {@link BaseProxy}. + */ + private final Type baseProxyIntf = Type.getType(BaseProxy.class); + /** * Maps client types (e.g. FooProxy) to server domain types (e.g. Foo). */ private final Map<Type, Type> clientToDomainType = new HashMap<Type, Type>(); - private final Map<Type, Type> domainToClientType = new HashMap<Type, Type>(); + /** + * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy, + * FooBProxy). + */ + private final Map<Type, List<Type>> domainToClientType = new HashMap<Type, List<Type>>(); + /** * The type {@link EntityProxy}. */ @@ -480,6 +556,10 @@ */ private final Map<Type, List<Type>> supertypes = new HashMap<Type, List<Type>>(); /** + * The type {@link ValueProxy}. + */ + private final Type valueProxyIntf = Type.getType(ValueProxy.class); + /** * A set to prevent re-validation of a type. */ private final Set<String> validatedTypes = new HashSet<String>(); @@ -530,53 +610,37 @@ * <li>The domain object has getId() and getVersion() methods</li> * <li>All property methods in the EntityProxy can be mapped onto an * equivalent domain method</li> - * <li>All referenced EntityProxy types are valid</li> + * <li>All referenced proxy types are valid</li> * </ul> * * @param binaryName the binary name (e.g. {@link Class#getName()}) of the * EntityProxy subtype */ public void validateEntityProxy(String binaryName) { + validateProxy(binaryName, entityProxyIntf, true); + } + + /** + * Determine if the specified type implements a proxy interface and apply the + * appropriate validations. This can be used as a general-purpose entry method + * when writing unit tests. + * + * @param binaryName the binary name (e.g. {@link Class#getName()}) of the + * EntityProxy or ValueProxy subtype + */ + public void validateProxy(String binaryName) { if (fastFail(binaryName)) { return; } Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName)); - ErrorContext logger = parentLogger.setType(proxyType); - - // Quick sanity check for calling code - if (!isAssignable(logger, entityProxyIntf, proxyType)) { - parentLogger.poison("%s is not a %s", print(proxyType), - EntityProxy.class.getSimpleName()); - return; - } - - // Find the domain type - Type domainType = getDomainType(logger, proxyType); - if (domainType == errorType) { - logger.poison( - "The type %s must be annotated with a @%s or @%s annotation", - BinaryName.toSourceName(binaryName), ProxyFor.class.getSimpleName(), - ProxyForName.class.getSimpleName()); - return; - } - - // Check for getId() and getVersion() in domain - checkIdAndVersion(logger, domainType); - - // Collect all methods in the client proxy type - Set<RFMethod> clientPropertyMethods = getMethodsInHierarchy(logger, - proxyType); - - // Find the equivalent domain getter/setter method - for (RFMethod clientPropertyMethod : clientPropertyMethods) { - // Ignore stableId(). Can't use descriptor due to overrides - if ("stableId".equals(clientPropertyMethod.getName()) - && clientPropertyMethod.getArgumentTypes().length == 0) { - continue; - } - checkPropertyMethod(logger, clientPropertyMethod, domainType); - maybeCheckReferredProxies(logger, clientPropertyMethod); + if (isAssignable(parentLogger, entityProxyIntf, proxyType)) { + validateEntityProxy(binaryName); + } else if (isAssignable(parentLogger, valueProxyIntf, proxyType)) { + validateValueProxy(binaryName); + } else { + parentLogger.poison("%s is neither an %s nor a %s", print(proxyType), + print(entityProxyIntf), print(valueProxyIntf)); } } @@ -685,13 +749,78 @@ } /** - * Given the binary name of a domain type, return the EntityProxy type that - * has been seen to map to the domain type. + * This method checks a ValueProxy interface against its peer domain object to + * determine if the server code would be able to process a request using the + * methods defined in the ValueProxy interface. It does not perform any checks + * as to whether or not the ValueProxy could actually be generated by the + * Generator. + * <p> + * This method may be called repeatedly on a single instance of the validator. + * Doing so will amortize type calculation costs. + * <p> + * Checks implemented: + * <ul> + * <li> <code>binaryName</code> implements ValueProxy</li> + * <li><code>binaryName</code> has a {@link ProxyFor} or {@link ProxyForName} + * annotation</li> + * <li>All property methods in the EntityProxy can be mapped onto an + * equivalent domain method</li> + * <li>All referenced proxy types are valid</li> + * </ul> + * + * @param binaryName the binary name (e.g. {@link Class#getName()}) of the + * EntityProxy subtype */ - String getEntityProxyTypeName(String domainTypeNameBinaryName) { - Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeNameBinaryName)); - Type found = domainToClientType.get(key); - return found == null ? null : found.getClassName(); + public void validateValueProxy(String binaryName) { + validateProxy(binaryName, valueProxyIntf, false); + } + + /** + * Given the binary name of a domain type, return the BaseProxy type that is + * assignable to {@code clientType}. This method allows multiple proxy types + * to be assigned to a domain type for use in different contexts (e.g. API + * slices). If there are multiple client types mapped to + * {@code domainTypeBinaryName} and assignable to {@code clientTypeBinaryName} + * , the first matching type will be returned. + */ + String getEntityProxyTypeName(String domainTypeBinaryName, + String clientTypeBinaryName) { + Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeBinaryName)); + List<Type> found = domainToClientType.get(key); + if (found == null || found.isEmpty()) { + return null; + } + // Common case + if (found.size() == 1) { + return found.get(0).getClassName(); + } + + // Search for the first assignable type + Type assignableTo = Type.getObjectType(BinaryName.toInternalName(clientTypeBinaryName)); + for (Type t : found) { + if (isAssignable(parentLogger, assignableTo, t)) { + return t.getClassName(); + } + } + return null; + } + + /** + * Record the mapping of a domain type to a client type. Proxy types will be + * added to {@link #domainToClientType}. + */ + private void addToDomainMap(ErrorContext logger, Type domainType, + Type clientType) { + clientToDomainType.put(clientType, domainType); + + if (isAssignable(logger, baseProxyIntf, clientType)) { + List<Type> list = domainToClientType.get(domainType); + if (list == null) { + list = new ArrayList<Type>(); + domainToClientType.put(domainType, list); + } + list.add(clientType); + } } /** @@ -918,14 +1047,15 @@ * object. */ private Type getDomainType(ErrorContext logger, Type clientType) { - Type toReturn = clientToDomainType.get(clientType); - if (toReturn != null) { - return toReturn; + Type domainType = clientToDomainType.get(clientType); + if (domainType != null) { + return domainType; } if (isValueType(logger, clientType) || isCollectionType(logger, clientType)) { - toReturn = clientType; - } else if (entityProxyIntf.equals(clientType)) { - toReturn = objectType; + domainType = clientType; + } else if (entityProxyIntf.equals(clientType) + || valueProxyIntf.equals(clientType)) { + domainType = objectType; } else { logger = logger.setType(clientType); DomainMapper pv = new DomainMapper(logger); @@ -934,22 +1064,16 @@ logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType), ProxyFor.class.getSimpleName(), Service.class.getSimpleName()); - toReturn = errorType; + domainType = errorType; } else { - toReturn = Type.getObjectType(pv.getDomainInternalName()); + domainType = Type.getObjectType(pv.getDomainInternalName()); + if (pv.isValueType()) { + valueTypes.add(clientType); + } } } - clientToDomainType.put(clientType, toReturn); - if (isAssignable(logger, entityProxyIntf, clientType)) { - Type previousProxyType = domainToClientType.put(toReturn, clientType); - if (previousProxyType != null) { - logger.poison( - "The domain type %s has more than one proxy type: %s and %s", - toReturn.getClassName(), previousProxyType.getClassName(), - clientType.getClassName()); - } - } - return toReturn; + addToDomainMap(logger, domainType, clientType); + return domainType; } /** @@ -1094,28 +1218,88 @@ } /** - * Examine an array of Types and call {@link #validateEntityProxy(String)} if - * the type is an EntityProxy. + * Examine an array of Types and call {@link #validateEntityProxy(String)} or + * {@link #validateValueProxy(String)} if the type is a proxy. */ - private void maybeCheckEntityProxyType(ErrorContext logger, Type... types) { + private void maybeCheckProxyType(ErrorContext logger, Type... types) { for (Type type : types) { if (isAssignable(logger, entityProxyIntf, type)) { validateEntityProxy(type.getClassName()); + } else if (isAssignable(logger, valueProxyIntf, type)) { + validateValueProxy(type.getClassName()); + } else if (isAssignable(logger, baseProxyIntf, type)) { + logger.poison( + "Invalid type hierarchy for %s. Only types derived from %s or %s may be used.", + print(type), print(entityProxyIntf), print(valueProxyIntf)); } } } /** - * Examine the arguments ond return value of a method and check any + * Examine the arguments and return value of a method and check any * EntityProxies referred. */ private void maybeCheckReferredProxies(ErrorContext logger, RFMethod method) { - Type[] argTypes = method.getArgumentTypes(); - Type returnType = getReturnType(logger, method); + if (method.getSignature() != null) { + TypesInSignatureCollector collector = new TypesInSignatureCollector(); + SignatureReader reader = new SignatureReader(method.getSignature()); + reader.accept(collector); + maybeCheckProxyType(logger, collector.getFound()); + } else { + Type[] argTypes = method.getArgumentTypes(); + Type returnType = getReturnType(logger, method); - // Check EntityProxy args ond return types against the domain - maybeCheckEntityProxyType(logger, argTypes); - maybeCheckEntityProxyType(logger, returnType); + // Check EntityProxy args ond return types against the domain + maybeCheckProxyType(logger, argTypes); + maybeCheckProxyType(logger, returnType); + } + } + + private void validateProxy(String binaryName, Type expectedType, + boolean requireId) { + if (fastFail(binaryName)) { + return; + } + + Type proxyType = Type.getObjectType(BinaryName.toInternalName(binaryName)); + ErrorContext logger = parentLogger.setType(proxyType); + + // Quick sanity check for calling code + if (!isAssignable(logger, expectedType, proxyType)) { + parentLogger.poison("%s is not a %s", print(proxyType), + print(expectedType)); + return; + } + + // Find the domain type + Type domainType = getDomainType(logger, proxyType); + if (domainType == errorType) { + logger.poison( + "The type %s must be annotated with a @%s or @%s annotation", + BinaryName.toSourceName(binaryName), ProxyFor.class.getSimpleName(), + ProxyForName.class.getSimpleName()); + return; + } + + // Check for getId() and getVersion() in domain + if (requireId) { + checkIdAndVersion(logger, domainType); + } + + // Collect all methods in the client proxy type + Set<RFMethod> clientPropertyMethods = getMethodsInHierarchy(logger, + proxyType); + + // Find the equivalent domain getter/setter method + for (RFMethod clientPropertyMethod : clientPropertyMethods) { + // Ignore stableId(). Can't use descriptor due to overrides + if ("stableId".equals(clientPropertyMethod.getName()) + && clientPropertyMethod.getArgumentTypes().length == 0) { + continue; + } + checkPropertyMethod(logger, clientPropertyMethod, domainType); + maybeCheckReferredProxies(logger, clientPropertyMethod); + } } /**
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestState.java b/user/src/com/google/gwt/requestfactory/server/RequestState.java new file mode 100644 index 0000000..9dcccdc --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/server/RequestState.java
@@ -0,0 +1,199 @@ +/* + * 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.gwt.requestfactory.server; + +import com.google.gwt.autobean.server.AutoBeanFactoryMagic; +import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.requestfactory.server.SimpleRequestProcessor.IdToEntityMap; +import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer; +import com.google.gwt.requestfactory.shared.BaseProxy; +import com.google.gwt.requestfactory.shared.EntityProxy; +import com.google.gwt.requestfactory.shared.ValueProxy; +import com.google.gwt.requestfactory.shared.impl.Constants; +import com.google.gwt.requestfactory.shared.impl.IdFactory; +import com.google.gwt.requestfactory.shared.impl.SimpleProxyId; +import com.google.gwt.requestfactory.shared.messages.EntityCodex; +import com.google.gwt.requestfactory.shared.messages.IdUtil; + +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * Encapsulates all state relating to the processing of a single request so that + * the SimpleRequestProcessor can be stateless. + */ +class RequestState implements EntityCodex.EntitySource { + final IdToEntityMap beans = new IdToEntityMap(); + private final Map<Object, SimpleProxyId<?>> domainObjectsToId; + private final IdFactory idFactory; + private final ServiceLayer service; + private final Resolver resolver; + + public RequestState(RequestState parent) { + idFactory = parent.idFactory; + domainObjectsToId = parent.domainObjectsToId; + service = parent.service; + resolver = new Resolver(this); + } + + public RequestState(final ServiceLayer service) { + this.service = service; + idFactory = new IdFactory() { + @Override + public boolean isEntityType(Class<?> clazz) { + return EntityProxy.class.isAssignableFrom(clazz); + } + + @Override + public boolean isValueType(Class<?> clazz) { + return ValueProxy.class.isAssignableFrom(clazz); + } + + @Override + @SuppressWarnings("unchecked") + protected <P extends BaseProxy> Class<P> getTypeFromToken(String typeToken) { + return (Class<P>) service.resolveClass(typeToken); + } + + @Override + protected String getTypeToken(Class<?> clazz) { + return service.getTypeToken(clazz); + } + }; + domainObjectsToId = new IdentityHashMap<Object, SimpleProxyId<?>>(); + resolver = new Resolver(this); + } + + public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( + SimpleProxyId<Q> id, Object domainObject) { + @SuppressWarnings("unchecked") + AutoBean<Q> toReturn = (AutoBean<Q>) beans.get(id); + if (toReturn == null) { + toReturn = createEntityProxyBean(id, domainObject); + } + return toReturn; + } + + /** + * EntityCodex support. + */ + public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( + String serializedProxyId) { + String typeToken = IdUtil.getTypeToken(serializedProxyId); + SimpleProxyId<Q> id; + if (IdUtil.isEphemeral(serializedProxyId)) { + id = idFactory.getId(typeToken, null, + IdUtil.getClientId(serializedProxyId)); + } else { + id = idFactory.getId( + typeToken, + SimpleRequestProcessor.fromBase64(IdUtil.getServerId(serializedProxyId))); + } + return getBeanForPayload(id); + } + + public IdFactory getIdFactory() { + return idFactory; + } + + public Resolver getResolver() { + return resolver; + } + + /** + * EntityCodex support. This method is identical to + * {@link IdFactory#getHistoryToken(SimpleProxyId)} except that it + * base64-encodes the server ids. + * <p> + * XXX: Fix the above + */ + public String getSerializedProxyId(SimpleProxyId<?> stableId) { + if (stableId.isEphemeral()) { + return IdUtil.ephemeralId(stableId.getClientId(), + service.getTypeToken(stableId.getProxyClass())); + } else if (stableId.isSynthetic()) { + return IdUtil.syntheticId(stableId.getSyntheticId(), + service.getTypeToken(stableId.getProxyClass())); + } else { + return IdUtil.persistedId( + SimpleRequestProcessor.toBase64(stableId.getServerId()), + service.getTypeToken(stableId.getProxyClass())); + } + } + + public ServiceLayer getServiceLayer() { + return service; + } + + /** + * If the given domain object has been previously associated with an id, + * return it + */ + public SimpleProxyId<?> getStableId(Object domain) { + return domainObjectsToId.get(domain); + } + + /** + * EntityCodex support. + */ + public boolean isEntityType(Class<?> clazz) { + return idFactory.isEntityType(clazz); + } + + /** + * EntityCodex support. + */ + public boolean isValueType(Class<?> clazz) { + return idFactory.isValueType(clazz); + } + + /** + * Creates an AutoBean for the given id, tracking a domain object. + */ + private <Q extends BaseProxy> AutoBean<Q> createEntityProxyBean( + SimpleProxyId<Q> id, Object domainObject) { + AutoBean<Q> toReturn = AutoBeanFactoryMagic.createBean(id.getProxyClass(), + SimpleRequestProcessor.CONFIGURATION); + toReturn.setTag(Constants.STABLE_ID, id); + toReturn.setTag(Constants.DOMAIN_OBJECT, domainObject); + beans.put(id, toReturn); + return toReturn; + } + + /** + * Returns the AutoBean corresponding to the given id, or creates if it does + * not yet exist. + */ + private <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( + SimpleProxyId<Q> id) { + @SuppressWarnings("unchecked") + AutoBean<Q> toReturn = (AutoBean<Q>) beans.get(id); + if (toReturn == null) { + // Resolve the domain object + Class<?> domainClass = service.getDomainClass(id.getProxyClass()); + Object domain; + if (id.isEphemeral()) { + domain = service.createDomainObject(domainClass); + } else { + String address = id.getServerId(); + domain = service.loadDomainObject(this, domainClass, address); + } + domainObjectsToId.put(domain, id); + toReturn = createEntityProxyBean(id, domain); + } + return toReturn; + } +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/server/Resolver.java b/user/src/com/google/gwt/requestfactory/server/Resolver.java new file mode 100644 index 0000000..20abe2c --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/server/Resolver.java
@@ -0,0 +1,431 @@ +/* + * 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.gwt.requestfactory.server; + +import com.google.gwt.autobean.server.impl.TypeUtils; +import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanUtils; +import com.google.gwt.autobean.shared.AutoBeanVisitor; +import com.google.gwt.autobean.shared.ValueCodex; +import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer; +import com.google.gwt.requestfactory.shared.BaseProxy; +import com.google.gwt.requestfactory.shared.EntityProxy; +import com.google.gwt.requestfactory.shared.EntityProxyId; +import com.google.gwt.requestfactory.shared.impl.Constants; +import com.google.gwt.requestfactory.shared.impl.SimpleProxyId; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Responsible for converting between domain and client entities. This class has + * a small amount of temporary state used to handle graph cycles and assignment + * of synthetic ids. + * + * @see RequestState#getResolver() + */ +class Resolver { + /** + * A parameterized type with a single parameter. + */ + private static class CollectionType implements ParameterizedType { + private final Class<?> rawType; + private final Class<?> elementType; + + private CollectionType(Class<?> rawType, Class<?> elementType) { + this.rawType = rawType; + this.elementType = elementType; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CollectionType)) { + return false; + } + CollectionType other = (CollectionType) o; + return rawType.equals(other.rawType) + && elementType.equals(other.elementType); + } + + public Type[] getActualTypeArguments() { + return new Type[] {elementType}; + } + + public Type getOwnerType() { + return null; + } + + public Type getRawType() { + return rawType; + } + + @Override + public int hashCode() { + return rawType.hashCode() * 13 + elementType.hashCode() * 7; + } + } + + /** + * Used to map the objects being resolved and its API slice to the client-side + * value. This handles the case where a domain object is returned to the + * client mapped to two proxies of differing types. + */ + private static class ResolutionKey { + private final Object domainObject; + private final int hashCode; + private final Type requestedType; + + public ResolutionKey(Object domainObject, Type requestedType) { + this.domainObject = domainObject; + this.requestedType = requestedType; + this.hashCode = System.identityHashCode(domainObject) * 13 + + requestedType.hashCode() * 7; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ResolutionKey)) { + return false; + } + ResolutionKey other = (ResolutionKey) o; + // Object identity comparison intentional + if (domainObject != other.domainObject) { + return false; + } + if (!requestedType.equals(other.requestedType)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return hashCode; + } + + /** + * For debugging use only. + */ + @Override + public String toString() { + return domainObject.toString() + " => " + requestedType.toString(); + } + } + + /** + * Returns the trailing {@code [n]} index value from a path. + */ + static int index(String path) { + int idx = path.lastIndexOf('['); + if (idx == -1) { + return -1; + } + return Integer.parseInt(path.substring(idx + 1, path.lastIndexOf(']'))); + } + + /** + * Returns {@code true} if the given prefix is one of the requested property + * references. + */ + static boolean matchesPropertyRef(Set<String> propertyRefs, String newPrefix) { + return propertyRefs.contains(newPrefix.replaceAll("\\[\\d+\\]", "")); + } + + /** + * Removes the trailing {@code [n]} from a path. + */ + static String snipIndex(String path) { + int idx = path.lastIndexOf('['); + if (idx == -1) { + return path; + } + return path.substring(0, idx); + } + + /** + * Maps domain values to client values. This map prevents cycles in the object + * graph from causing infinite recursion. + */ + private final Map<ResolutionKey, Object> resolved = new HashMap<ResolutionKey, Object>(); + private final ServiceLayer service; + private final RequestState state; + private int syntheticId; + + /** + * Should only be called from {@link RequestState}. + */ + Resolver(RequestState state) { + this.state = state; + this.service = state.getServiceLayer(); + } + + /** + * Given a domain object, return a value that can be encoded by the client. + * + * @param domainValue the domain object to be converted into a client-side + * value + * @param assignableTo the type in the client to which the resolved value + * should be assignable + * @param propertyRefs the property references requested by the client + */ + public Object resolveClientValue(Object domainValue, Type assignableTo, + Set<String> propertyRefs) { + return resolveClientValue(domainValue, assignableTo, + getPropertyRefs(propertyRefs), ""); + } + + /** + * Convert a client-side value into a domain value. + * + * @param maybeEntityProxy the client object to resolve + * @param detectDeadEntities if <code>true</code> this method will throw a + * ReportableException containing a {@link DeadEntityException} if an + * EntityProxy cannot be resolved + */ + public Object resolveDomainValue(Object maybeEntityProxy, + boolean detectDeadEntities) { + if (maybeEntityProxy instanceof BaseProxy) { + AutoBean<BaseProxy> bean = AutoBeanUtils.getAutoBean((BaseProxy) maybeEntityProxy); + Object domain = bean.getTag(Constants.DOMAIN_OBJECT); + if (domain == null && detectDeadEntities) { + throw new ReportableException(new DeadEntityException( + "The requested entity is not available on the server")); + } + return domain; + } else if (maybeEntityProxy instanceof Collection<?>) { + Collection<Object> accumulator; + if (maybeEntityProxy instanceof List) { + accumulator = new ArrayList<Object>(); + } else if (maybeEntityProxy instanceof Set) { + accumulator = new HashSet<Object>(); + } else { + throw new ReportableException("Unsupported collection type " + + maybeEntityProxy.getClass().getName()); + } + for (Object o : (Collection<?>) maybeEntityProxy) { + accumulator.add(resolveDomainValue(o, detectDeadEntities)); + } + return accumulator; + } + return maybeEntityProxy; + } + + /** + * Expand the property references in an InvocationMessage into a + * fully-expanded list of properties. For example, <code>[foo.bar.baz]</code> + * will be converted into <code>[foo, foo.bar, foo.bar.baz]</code>. + */ + private Set<String> getPropertyRefs(Set<String> refs) { + if (refs == null) { + return Collections.emptySet(); + } + + Set<String> toReturn = new TreeSet<String>(); + for (String raw : refs) { + for (int idx = raw.length(); idx >= 0; idx = raw.lastIndexOf('.', idx - 1)) { + toReturn.add(raw.substring(0, idx)); + } + } + return toReturn; + } + + /** + * Converts a domain entity into an EntityProxy that will be sent to the + * client. + */ + private <T extends BaseProxy> T resolveClientProxy(final Object domainEntity, + Class<T> proxyType, final Set<String> propertyRefs, ResolutionKey key, + final String prefix) { + if (domainEntity == null) { + return null; + } + + SimpleProxyId<? extends BaseProxy> id = state.getStableId(domainEntity); + + boolean isEntityProxy = state.isEntityType(proxyType); + final boolean isOwnerValueProxy = state.isValueType(proxyType); + int version; + + // Create the id or update an ephemeral id by calculating its address + if (id == null || id.isEphemeral()) { + // The address is an id or an id plus a path + String address; + if (isEntityProxy) { + // Compute data needed to return id to the client + address = service.getFlatId(state, domainEntity); + version = service.getVersion(domainEntity); + } else { + address = null; + version = 0; + } + if (id == null) { + if (address == null) { + /* + * This will happen when server code attempts to return an unpersisted + * object to the client. In this case, we'll assign a synthetic id + * that is valid for the duration of the response. The client is + * expected to assign a client-local id to this object and then it + * will behave as though it were an object newly-created by the + * client. + */ + id = state.getIdFactory().allocateSyntheticId(proxyType, + ++syntheticId); + } else { + id = state.getIdFactory().getId(proxyType, address, null); + } + } else { + id.setServerId(address); + } + } else if (isEntityProxy) { + // Already have the id, just pull the current version + version = service.getVersion(domainEntity); + } else { + // The version of a value object is always 0 + version = 0; + } + + @SuppressWarnings("unchecked") + AutoBean<T> bean = (AutoBean<T>) state.getBeanForPayload(id, domainEntity); + resolved.put(key, bean.as()); + bean.setTag(Constants.IN_RESPONSE, true); + bean.setTag(Constants.ENCODED_VERSION_PROPERTY, version); + bean.accept(new AutoBeanVisitor() { + + @Override + public boolean visitReferenceProperty(String propertyName, + AutoBean<?> value, PropertyContext ctx) { + // Does the user care about the property? + String newPrefix = (prefix.length() > 0 ? (prefix + ".") : "") + + propertyName; + + /* + * Send the property if the enclosing type is a ValueProxy, if the owner + * requested the property, or if the property is a list of values. + */ + Class<?> elementType = ctx instanceof CollectionPropertyContext + ? ((CollectionPropertyContext) ctx).getElementType() : null; + boolean shouldSend = isOwnerValueProxy + || matchesPropertyRef(propertyRefs, newPrefix) + || elementType != null && ValueCodex.canDecode(elementType); + + if (!shouldSend) { + return false; + } + + // Call the getter + Object domainValue = service.getProperty(domainEntity, propertyName); + if (domainValue == null) { + return false; + } + + // Turn the domain object into something usable on the client side + Type type; + if (elementType == null) { + type = ctx.getType(); + } else { + type = new CollectionType(ctx.getType(), elementType); + } + Object clientValue = resolveClientValue(domainValue, type, + propertyRefs, newPrefix); + + ctx.set(clientValue); + return false; + } + + @Override + public boolean visitValueProperty(String propertyName, Object value, + PropertyContext ctx) { + // Limit unrequested value properties? + value = service.getProperty(domainEntity, propertyName); + ctx.set(value); + return false; + } + }); + + return bean.as(); + } + + /** + * Recursive-descent implementation. + */ + private Object resolveClientValue(Object domainValue, Type returnType, + Set<String> propertyRefs, String prefix) { + if (domainValue == null) { + return null; + } + + Class<?> assignableTo = TypeUtils.ensureBaseType(returnType); + ResolutionKey key = new ResolutionKey(domainValue, returnType); + + Object previous = resolved.get(key); + if (previous != null && assignableTo.isInstance(previous)) { + return assignableTo.cast(previous); + } + + Class<?> returnClass = service.getClientType(domainValue.getClass(), + assignableTo); + + // Pass simple values through + if (ValueCodex.canDecode(returnClass)) { + return assignableTo.cast(domainValue); + } + + // Convert entities to EntityProxies or EntityProxyIds + boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass); + boolean isId = EntityProxyId.class.isAssignableFrom(returnClass); + if (isProxy || isId) { + BaseProxy entity = resolveClientProxy(domainValue, + assignableTo.asSubclass(BaseProxy.class), propertyRefs, key, prefix); + if (isId) { + return assignableTo.cast(((EntityProxy) entity).stableId()); + } + return assignableTo.cast(entity); + } + + // Convert collections + if (Collection.class.isAssignableFrom(returnClass)) { + Collection<Object> accumulator; + if (List.class.isAssignableFrom(returnClass)) { + accumulator = new ArrayList<Object>(); + } else if (Set.class.isAssignableFrom(returnClass)) { + accumulator = new HashSet<Object>(); + } else { + throw new ReportableException("Unsupported collection type" + + returnClass.getName()); + } + resolved.put(key, accumulator); + + Type elementType = TypeUtils.getSingleParameterization(Collection.class, + returnType); + for (Object o : (Collection<?>) domainValue) { + accumulator.add(resolveClientValue(o, elementType, propertyRefs, prefix)); + } + return assignableTo.cast(accumulator); + } + + throw new ReportableException("Unsupported domain type " + + returnClass.getCanonicalName()); + } +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java index e8dede4..8637cf6 100644 --- a/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java +++ b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
@@ -24,15 +24,17 @@ import com.google.gwt.autobean.shared.AutoBeanVisitor; import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.ValueCodex; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; import com.google.gwt.requestfactory.shared.InstanceRequest; import com.google.gwt.requestfactory.shared.ServerFailure; import com.google.gwt.requestfactory.shared.WriteOperation; +import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory; import com.google.gwt.requestfactory.shared.impl.Constants; import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory; -import com.google.gwt.requestfactory.shared.impl.IdFactory; -import com.google.gwt.requestfactory.shared.impl.SimpleEntityProxyId; +import com.google.gwt.requestfactory.shared.impl.SimpleProxyId; +import com.google.gwt.requestfactory.shared.impl.ValueProxyCategory; import com.google.gwt.requestfactory.shared.messages.EntityCodex; import com.google.gwt.requestfactory.shared.messages.EntityCodex.EntitySource; import com.google.gwt.requestfactory.shared.messages.IdUtil; @@ -53,13 +55,10 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeSet; import javax.validation.ConstraintViolation; @@ -77,14 +76,24 @@ /** * The way to introduce polymorphism. */ - Class<?> getClientType(Class<?> domainClass); + Class<?> getClientType(Class<?> domainClass, Class<?> clientType); Class<?> getDomainClass(Class<?> clazz); + /** + * May return {@code null} to indicate that the domain object has not been + * persisted. + */ String getFlatId(EntitySource source, Object domainObject); Object getProperty(Object domainObject, String property); + /** + * Compute the return type for a method declared in a RequestContext by + * analyzing the generic method declaration. + */ + Type getRequestReturnType(Method contextMethod); + String getTypeToken(Class<?> domainClass); int getVersion(Object domainObject); @@ -93,7 +102,7 @@ Object loadDomainObject(EntitySource source, Class<?> clazz, String flatId); - Class<? extends EntityProxy> resolveClass(String typeToken); + Class<? extends BaseProxy> resolveClass(String typeToken); Method resolveDomainMethod(Method requestContextMethod); @@ -111,120 +120,24 @@ * specific type. */ @SuppressWarnings("serial") - private static class IdToEntityMap extends - HashMap<SimpleEntityProxyId<?>, AutoBean<? extends EntityProxy>> { + static class IdToEntityMap extends + HashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> { } - private class RequestState implements EntityCodex.EntitySource { - private final IdToEntityMap beans = new IdToEntityMap(); - private final Map<Object, Integer> domainObjectsToClientId; - private final IdFactory idFactory; + /** + * Allows the creation of properly-configured AutoBeans without having to + * create an AutoBeanFactory with the desired annotations. + */ + static final Configuration CONFIGURATION = new Configuration.Builder().setCategories( + EntityProxyCategory.class, ValueProxyCategory.class, + BaseProxyCategory.class).setNoWrap(EntityProxyId.class).build(); - public RequestState() { - idFactory = new IdFactory() { - @Override - @SuppressWarnings("unchecked") - protected <P extends EntityProxy> Class<P> getTypeFromToken( - String typeToken) { - return (Class<P>) service.resolveClass(typeToken); - } + /** + * Vends message objects. + */ + static final MessageFactory FACTORY = AutoBeanFactoryMagic.create(MessageFactory.class); - @Override - protected String getTypeToken(Class<?> clazz) { - return service.getTypeToken(clazz); - } - }; - domainObjectsToClientId = new IdentityHashMap<Object, Integer>(); - } - - public RequestState(RequestState parent) { - idFactory = parent.idFactory; - domainObjectsToClientId = parent.domainObjectsToClientId; - } - - public <Q extends EntityProxy> AutoBean<Q> getBeanForPayload( - SimpleEntityProxyId<Q> id) { - @SuppressWarnings("unchecked") - AutoBean<Q> toReturn = (AutoBean<Q>) beans.get(id); - if (toReturn == null) { - toReturn = AutoBeanFactoryMagic.createBean(id.getProxyClass(), - CONFIGURATION); - toReturn.setTag(STABLE_ID, id); - - // Resolve the domain object - Class<?> domainClass = service.getDomainClass(id.getProxyClass()); - - Object domain; - if (id.isEphemeral()) { - domain = service.createDomainObject(domainClass); - // Don't call getFlatId here, resolve the ids after invocations - domainObjectsToClientId.put(domain, id.getClientId()); - } else { - domain = service.loadDomainObject(this, domainClass, - fromBase64(id.getServerId())); - } - toReturn.setTag(DOMAIN_OBJECT, domain); - - beans.put(id, toReturn); - } - return toReturn; - } - - /** - * EntityCodex support. - */ - public <Q extends EntityProxy> AutoBean<Q> getBeanForPayload( - String serializedProxyId) { - String typeToken = IdUtil.getTypeToken(serializedProxyId); - SimpleEntityProxyId<Q> id; - if (IdUtil.isEphemeral(serializedProxyId)) { - id = idFactory.getId(typeToken, null, - IdUtil.getClientId(serializedProxyId)); - } else { - id = idFactory.getId(typeToken, IdUtil.getServerId(serializedProxyId)); - } - return getBeanForPayload(id); - } - - /** - * If the domain object was sent with an ephemeral id, return the client id. - */ - public Integer getClientId(Object domainEntity) { - return domainObjectsToClientId.get(domainEntity); - } - - /** - * EntityCodex support. - */ - public String getSerializedProxyId(EntityProxyId<?> stableId) { - SimpleEntityProxyId<?> impl = (SimpleEntityProxyId<?>) stableId; - if (impl.isEphemeral()) { - return IdUtil.ephemeralId(impl.getClientId(), - service.getTypeToken(stableId.getProxyClass())); - } else { - return IdUtil.persistedId(impl.getServerId(), - service.getTypeToken(stableId.getProxyClass())); - } - } - - /** - * EntityCodex support. - */ - public boolean isEntityType(Class<?> clazz) { - return EntityProxy.class.isAssignableFrom(clazz); - } - } - - private static final Configuration CONFIGURATION = new Configuration.Builder().setCategories( - EntityProxyCategory.class).setNoWrap(EntityProxyId.class).build(); - private static final MessageFactory FACTORY = AutoBeanFactoryMagic.create(MessageFactory.class); - - // Tag constants - private static final String DOMAIN_OBJECT = "domainObject"; - private static final String IN_RESPONSE = "inResponse"; - private static final String STABLE_ID = "stableId"; - - private static String fromBase64(String encoded) { + static String fromBase64(String encoded) { try { return new String(Base64Utils.fromBase64(encoded), "UTF-8"); } catch (UnsupportedEncodingException e) { @@ -232,7 +145,7 @@ } } - private static String toBase64(String data) { + static String toBase64(String data) { try { return Base64Utils.toBase64(data.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { @@ -241,6 +154,7 @@ } private ExceptionHandler exceptionHandler = new DefaultExceptionHandler(); + private final ServiceLayer service; public SimpleRequestProcessor(ServiceLayer serviceLayer) { @@ -271,7 +185,7 @@ * Main processing method. */ void process(RequestMessage req, ResponseMessage resp) { - final RequestState source = new RequestState(); + final RequestState source = new RequestState(service); // Apply operations processOperationMessages(source, req); @@ -319,41 +233,44 @@ private void createReturnOperations(List<OperationMessage> operations, RequestState returnState, IdToEntityMap toProcess) { - for (Map.Entry<SimpleEntityProxyId<?>, AutoBean<? extends EntityProxy>> entry : toProcess.entrySet()) { - SimpleEntityProxyId<?> id = entry.getKey(); + for (Map.Entry<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> entry : toProcess.entrySet()) { + SimpleProxyId<?> id = entry.getKey(); - AutoBean<? extends EntityProxy> bean = entry.getValue(); - Object domainObject = bean.getTag(DOMAIN_OBJECT); + AutoBean<? extends BaseProxy> bean = entry.getValue(); + Object domainObject = bean.getTag(Constants.DOMAIN_OBJECT); WriteOperation writeOperation; if (id.isEphemeral()) { - resolveClientEntityProxy(returnState, domainObject, - Collections.<String> emptySet(), ""); - if (id.isEphemeral()) { - throw new ReportableException("Could not persist entity " - + service.getFlatId(returnState, domainObject.toString())); - } + // See if the entity has been persisted in the meantime + returnState.getResolver().resolveClientValue(domainObject, + id.getProxyClass(), Collections.<String> emptySet()); } - if (service.loadDomainObject(returnState, - service.getDomainClass(id.getProxyClass()), - fromBase64(id.getServerId())) == null) { + int version; + if (id.isEphemeral() || id.isSynthetic()) { + // If the object isn't persistent, there's no reason to send an update + writeOperation = null; + version = 0; + } else if (service.loadDomainObject(returnState, + service.getDomainClass(id.getProxyClass()), id.getServerId()) == null) { writeOperation = WriteOperation.DELETE; + version = 0; } else if (id.wasEphemeral()) { writeOperation = WriteOperation.PERSIST; + version = service.getVersion(domainObject); } else { writeOperation = WriteOperation.UPDATE; + version = service.getVersion(domainObject); } - boolean inResponse = bean.getTag(IN_RESPONSE) != null; - int version = domainObject == null ? 0 : service.getVersion(domainObject); + boolean inResponse = bean.getTag(Constants.IN_RESPONSE) != null; /* * Don't send any data back to the client for an update on an object that * isn't part of the response payload when the client's version matches * the domain version. */ - if (writeOperation.equals(WriteOperation.UPDATE) && !inResponse) { + if (WriteOperation.UPDATE.equals(writeOperation) && !inResponse) { if (Integer.valueOf(version).equals( bean.getTag(Constants.ENCODED_VERSION_PROPERTY))) { continue; @@ -361,9 +278,15 @@ } OperationMessage op = FACTORY.operation().as(); - if (writeOperation == WriteOperation.PERSIST) { + + /* + * Send a client id if the id is ephemeral or was previously associated + * with a client id. + */ + if (id.wasEphemeral()) { op.setClientId(id.getClientId()); } + op.setOperation(writeOperation); // Only send properties for entities that are part of the return graph @@ -380,7 +303,12 @@ op.setPropertyMap(propertyMap); } - op.setServerId(id.getServerId()); + if (!id.isEphemeral() && !id.isSynthetic()) { + // Send the server address only for persistent objects + op.setServerId(toBase64(id.getServerId())); + } + + op.setSyntheticId(id.getSyntheticId()); op.setTypeToken(service.getTypeToken(id.getProxyClass())); op.setVersion(version); @@ -411,7 +339,8 @@ split = invocation.getParameters().get(i); } Object arg = EntityCodex.decode(source, type, elementType, split); - arg = resolveDomainValue(arg, !EntityProxyId.class.equals(contextArgs[i])); + arg = source.getResolver().resolveDomainValue(arg, + !EntityProxyId.class.equals(contextArgs[i])); args.add(arg); } @@ -446,26 +375,6 @@ return args; } - /** - * Expand the property references in an InvocationMessage into a - * fully-expanded list of properties. For example, <code>[foo.bar.baz]</code> - * will be converted into <code>[foo, foo.bar, foo.bar.baz]</code>. - */ - private Set<String> getPropertyRefs(InvocationMessage invocation) { - Set<String> refs = invocation.getPropertyRefs(); - if (refs == null) { - return Collections.emptySet(); - } - - Set<String> toReturn = new TreeSet<String>(); - for (String raw : refs) { - for (int idx = raw.length(); idx >= 0; idx = raw.lastIndexOf('.', idx - 1)) { - toReturn.add(raw.substring(0, idx)); - } - } - return toReturn; - } - private void processInvocationMessages(RequestState state, List<InvocationMessage> invlist, List<Splittable> results, List<Boolean> success, RequestState returnState) { @@ -483,8 +392,9 @@ Object returnValue = service.invoke(domainMethod, args.toArray()); // Convert domain object to client object - returnValue = resolveClientValue(returnState, returnValue, - getPropertyRefs(invocation), ""); + Type requestReturnType = service.getRequestReturnType(contextMethod); + returnValue = state.getResolver().resolveClientValue(returnValue, + requestReturnType, invocation.getPropertyRefs()); // Convert the client object to a string results.add(EntityCodex.encode(returnState, returnValue)); @@ -511,10 +421,10 @@ operation.getServerId(), operation.getTypeToken()); AutoBean<? extends EntityProxy> bean = state.getBeanForPayload(payloadId); // Use the version later to know which objects need to be sent back - bean.setTag(Constants.ENCODED_ID_PROPERTY, operation.getVersion()); + bean.setTag(Constants.ENCODED_VERSION_PROPERTY, operation.getVersion()); // Load the domain object with properties, if it exists - final Object domain = bean.getTag(DOMAIN_OBJECT); + final Object domain = bean.getTag(Constants.DOMAIN_OBJECT); if (domain != null) { // Apply any property updates final Map<String, Splittable> flatValueMap = operation.getPropertyMap(); @@ -529,7 +439,8 @@ ? ((CollectionPropertyContext) ctx).getElementType() : null; Object newValue = EntityCodex.decode(state, ctx.getType(), elementType, flatValueMap.get(propertyName)); - Object resolved = resolveDomainValue(newValue, false); + Object resolved = state.getResolver().resolveDomainValue( + newValue, false); service.setProperty(domain, propertyName, service.getDomainClass(ctx.getType()), resolved); } @@ -542,7 +453,8 @@ if (flatValueMap.containsKey(propertyName)) { Splittable split = flatValueMap.get(propertyName); Object newValue = ValueCodex.decode(ctx.getType(), split); - Object resolved = resolveDomainValue(newValue, false); + Object resolved = state.getResolver().resolveDomainValue( + newValue, false); service.setProperty(domain, propertyName, ctx.getType(), resolved); } @@ -555,175 +467,30 @@ } /** - * Converts a domain entity into an EntityProxy that will be sent to the - * client. - */ - private EntityProxy resolveClientEntityProxy(final RequestState state, - final Object domainEntity, final Set<String> propertyRefs, - final String prefix) { - if (domainEntity == null) { - return null; - } - - // Compute data needed to return id to the client - String flatId = toBase64(service.getFlatId(state, domainEntity)); - Class<? extends EntityProxy> proxyType = service.getClientType( - domainEntity.getClass()).asSubclass(EntityProxy.class); - - // Retrieve the id, possibly setting its persisted value - SimpleEntityProxyId<? extends EntityProxy> id = state.idFactory.getId( - proxyType, flatId, state.getClientId(domainEntity)); - - AutoBean<? extends EntityProxy> bean = state.getBeanForPayload(id); - bean.setTag(IN_RESPONSE, true); - bean.accept(new AutoBeanVisitor() { - @Override - public boolean visitReferenceProperty(String propertyName, - AutoBean<?> value, PropertyContext ctx) { - // Does the user care about the property? - String newPrefix = (prefix.length() > 0 ? (prefix + ".") : "") - + propertyName; - - /* - * Send if the user cares about the property or if it's a collection of - * values. - */ - Class<?> elementType = ctx instanceof CollectionPropertyContext - ? ((CollectionPropertyContext) ctx).getElementType() : null; - boolean shouldSend = propertyRefs.contains(newPrefix) - || elementType != null && !state.isEntityType(elementType); - - if (!shouldSend) { - return false; - } - - // Call the getter - Object domainValue = service.getProperty(domainEntity, propertyName); - if (domainValue == null) { - return false; - } - - // Turn the domain object into something usable on the client side - Object resolved = resolveClientValue(state, domainValue, propertyRefs, - newPrefix); - - ctx.set(ctx.getType().cast(resolved)); - return false; - } - - @Override - public boolean visitValueProperty(String propertyName, Object value, - PropertyContext ctx) { - // Limit unrequested value properties? - value = service.getProperty(domainEntity, propertyName); - ctx.set(ctx.getType().cast(value)); - return false; - } - }); - bean.setTag(Constants.ENCODED_VERSION_PROPERTY, - service.getVersion(domainEntity)); - return proxyType.cast(bean.as()); - } - - /** - * Given a method a domain object, return a value that can be encoded by the - * client. - */ - private Object resolveClientValue(RequestState source, Object domainValue, - Set<String> propertyRefs, String prefix) { - if (domainValue == null) { - return null; - } - - Class<?> returnClass = service.getClientType(domainValue.getClass()); - - // Pass simple values through - if (ValueCodex.canDecode(returnClass)) { - return domainValue; - } - - // Convert entities to EntityProxies - if (EntityProxy.class.isAssignableFrom(returnClass)) { - return resolveClientEntityProxy(source, domainValue, propertyRefs, prefix); - } - - // Convert collections - if (Collection.class.isAssignableFrom(returnClass)) { - Collection<Object> accumulator; - if (List.class.isAssignableFrom(returnClass)) { - accumulator = new ArrayList<Object>(); - } else if (Set.class.isAssignableFrom(returnClass)) { - accumulator = new HashSet<Object>(); - } else { - throw new ReportableException("Unsupported collection type" - + returnClass.getName()); - } - - for (Object o : (Collection<?>) domainValue) { - accumulator.add(resolveClientValue(source, o, propertyRefs, prefix)); - } - return accumulator; - } - - throw new ReportableException("Unsupported domain type " - + returnClass.getCanonicalName()); - } - - /** - * Convert a client-side value into a domain value. - * - * @param the client object to resolve - * @param detectDeadEntities if <code>true</code> this method will throw a - * ReportableException containing a {@link DeadEntityException} if an - * EntityProxy cannot be resolved - */ - private Object resolveDomainValue(Object maybeEntityProxy, - boolean detectDeadEntities) { - if (maybeEntityProxy instanceof EntityProxy) { - AutoBean<EntityProxy> bean = AutoBeanUtils.getAutoBean((EntityProxy) maybeEntityProxy); - Object domain = bean.getTag(DOMAIN_OBJECT); - if (domain == null && detectDeadEntities) { - throw new ReportableException(new DeadEntityException( - "The requested entity is not available on the server")); - } - return domain; - } else if (maybeEntityProxy instanceof Collection<?>) { - Collection<Object> accumulator; - if (maybeEntityProxy instanceof List) { - accumulator = new ArrayList<Object>(); - } else if (maybeEntityProxy instanceof Set) { - accumulator = new HashSet<Object>(); - } else { - throw new ReportableException("Unsupported collection type " - + maybeEntityProxy.getClass().getName()); - } - for (Object o : (Collection<?>) maybeEntityProxy) { - accumulator.add(resolveDomainValue(o, detectDeadEntities)); - } - return accumulator; - } - return maybeEntityProxy; - } - - /** * Validate all of the entities referenced in a RequestState. */ private List<ViolationMessage> validateEntities(RequestState source) { List<ViolationMessage> errorMessages = new ArrayList<ViolationMessage>(); - for (Map.Entry<SimpleEntityProxyId<?>, AutoBean<? extends EntityProxy>> entry : source.beans.entrySet()) { - Object domainObject = entry.getValue().getTag(DOMAIN_OBJECT); + for (Map.Entry<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> entry : source.beans.entrySet()) { + AutoBean<? extends BaseProxy> bean = entry.getValue(); + Object domainObject = bean.getTag(Constants.DOMAIN_OBJECT); // The object could have been deleted if (domainObject != null) { Set<ConstraintViolation<Object>> errors = service.validate(domainObject); if (errors != null && !errors.isEmpty()) { - SimpleEntityProxyId<?> id = entry.getKey(); + SimpleProxyId<?> id = entry.getKey(); for (ConstraintViolation<Object> error : errors) { ViolationMessage message = FACTORY.violation().as(); message.setClientId(id.getClientId()); message.setMessage(error.getMessage()); message.setPath(error.getPropertyPath().toString()); - message.setServerId(id.getServerId()); + if (id.isEphemeral()) { + message.setClientId(id.getClientId()); + } + if (id.getServerId() != null) { + message.setServerId(toBase64(id.getServerId())); + } message.setTypeToken(service.getTypeToken(id.getProxyClass())); errorMessages.add(message); }
diff --git a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java index e6c412b..74247af 100644 --- a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java +++ b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
@@ -21,12 +21,16 @@ import com.google.gwt.autobean.shared.AutoBeanFactory.NoWrap; import com.google.gwt.event.shared.EventBus; import com.google.gwt.requestfactory.server.testing.InProcessRequestContext.RequestContextHandler; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestFactory; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory; +import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory; import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory; +import com.google.gwt.requestfactory.shared.impl.ValueProxyCategory; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; @@ -37,7 +41,9 @@ * An JRE-compatible implementation of RequestFactory. */ class InProcessRequestFactory extends AbstractRequestFactory { - @Category(EntityProxyCategory.class) + @Category(value = { + EntityProxyCategory.class, ValueProxyCategory.class, + BaseProxyCategory.class}) @NoWrap(EntityProxyId.class) interface Factory extends AutoBeanFactory { } @@ -60,7 +66,7 @@ InProcessRequestFactory.this).new RequestContextHandler(); return context.cast(Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), - new Class<?>[]{context}, handler)); + new Class<?>[] {context}, handler)); } } @@ -71,17 +77,27 @@ } @Override + public boolean isEntityType(Class<?> clazz) { + return EntityProxy.class.isAssignableFrom(clazz); + } + + @Override + public boolean isValueType(Class<?> clazz) { + return ValueProxy.class.isAssignableFrom(clazz); + } + + @Override protected AutoBeanFactory getAutoBeanFactory() { return AutoBeanFactoryMagic.create(Factory.class); } @Override @SuppressWarnings("unchecked") - protected <P extends EntityProxy> Class<P> getTypeFromToken(String typeToken) { + protected <P extends BaseProxy> Class<P> getTypeFromToken(String typeToken) { try { - Class<? extends EntityProxy> found = Class.forName(typeToken, false, + Class<? extends BaseProxy> found = Class.forName(typeToken, false, Thread.currentThread().getContextClassLoader()).asSubclass( - EntityProxy.class); + BaseProxy.class); return (Class<P>) found; } catch (ClassNotFoundException e) { return null; @@ -90,6 +106,6 @@ @Override protected String getTypeToken(Class<?> clazz) { - return EntityProxy.class.isAssignableFrom(clazz) ? clazz.getName() : null; + return isEntityType(clazz) || isValueType(clazz) ? clazz.getName() : null; } }
diff --git a/user/src/com/google/gwt/requestfactory/shared/BaseProxy.java b/user/src/com/google/gwt/requestfactory/shared/BaseProxy.java new file mode 100644 index 0000000..3764fc0 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/BaseProxy.java
@@ -0,0 +1,27 @@ +/* + * 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.gwt.requestfactory.shared; + +/** + * The root type from which all client-side proxy objects are derived. Users + * should not extend the BaseProxy type directly, but use either + * {@link EntityProxy} or {@link ValueProxy}. + * + * @see EntityProxy + * @see ValueProxy + */ +public interface BaseProxy { +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/EntityProxy.java b/user/src/com/google/gwt/requestfactory/shared/EntityProxy.java index 1e5cecb..4bb312d 100644 --- a/user/src/com/google/gwt/requestfactory/shared/EntityProxy.java +++ b/user/src/com/google/gwt/requestfactory/shared/EntityProxy.java
@@ -18,7 +18,7 @@ /** * A proxy for a server-side domain object. */ -public interface EntityProxy { +public interface EntityProxy extends BaseProxy { /** * Returns the {@link EntityProxyId} that identifies a particular instance of * the type proxied by the receiver.
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestContext.java b/user/src/com/google/gwt/requestfactory/shared/RequestContext.java index 495d252..c5ad7af 100644 --- a/user/src/com/google/gwt/requestfactory/shared/RequestContext.java +++ b/user/src/com/google/gwt/requestfactory/shared/RequestContext.java
@@ -21,13 +21,13 @@ public interface RequestContext { /** * Returns a new mutable proxy that this request can carry to the server, - * perhaps to be persisted. If a persist does happen, a CREATE event will be - * posted including the EntityProxyId of this proxy. - * + * perhaps to be persisted. If the object is succesfully persisted, a PERSIST + * event will be posted including the EntityProxyId of this proxy. + * * @param clazz a Class object of type T - * @return an {@link EntityProxy} instance of type T + * @return an {@link BaseProxy} instance of type T */ - <T extends EntityProxy> T create(Class<T> clazz); + <T extends BaseProxy> T create(Class<T> clazz); /** * Returns a mutable version of the proxy, whose mutations will accumulate in @@ -35,9 +35,9 @@ * be mutable. * * @param object an instance of type T - * @return an {@link EntityProxy} instance of type T + * @return an {@link EntityProxy} or {@link ValueProxy} instance of type T */ - <T extends EntityProxy> T edit(T object); + <T extends BaseProxy> T edit(T object); /** * Send the accumulated changes and method invocations associated with the @@ -51,7 +51,7 @@ /** * For receiving errors or validation failures only. - * + * * @param receiver a {@link Receiver} instance * @throws IllegalArgumentException if <code>receiver</code> is * <code>null</code>
diff --git a/user/src/com/google/gwt/requestfactory/shared/ValueProxy.java b/user/src/com/google/gwt/requestfactory/shared/ValueProxy.java new file mode 100644 index 0000000..2882b3e --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/ValueProxy.java
@@ -0,0 +1,23 @@ +/* + * 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.gwt.requestfactory.shared; + +/** + * An analog to EntityProxy for domain types that do not have an identity + * concept. + */ +public interface ValueProxy extends BaseProxy { +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/Violation.java b/user/src/com/google/gwt/requestfactory/shared/Violation.java index 3698740..23466b0 100644 --- a/user/src/com/google/gwt/requestfactory/shared/Violation.java +++ b/user/src/com/google/gwt/requestfactory/shared/Violation.java
@@ -21,6 +21,14 @@ */ public interface Violation { /** + * If the ConstraintViolation occurred while validating a object, this method + * will return a BaseProxy that contains the invalid values. + * + * @return the BaseProxy that caused the ConstraintViolation + */ + BaseProxy getInvalidProxy(); + + /** * Returns the message associated with this {@link Violation}. * * @return a String message @@ -28,6 +36,16 @@ String getMessage(); /** + * If the ConstraintViolation occurred while validating a value object that + * originated from the server, this method will return a BaseProxy that + * contains the original values. + * + * @return the BaseProxy originally sent by the server or {@code null} if the + * BaseProxy was created on the client. + */ + BaseProxy getOriginalProxy(); + + /** * Returns the path associated with this {@link Violation}. * * @return a String path @@ -35,9 +53,11 @@ String getPath(); /** - * Returns the proxy id associated with this {@link Violation}. + * Returns the proxy id associated with this {@link Violation} if the object + * associated with the violation is an {@link EntityProxy}. * - * @return an {@link EntityProxyId} instance + * @return an {@link EntityProxyId} instance or {@code null} if the object is + * a ValueProxy. */ EntityProxyId<?> getProxyId(); }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java index 680286b..1b3befb 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -15,12 +15,16 @@ */ package com.google.gwt.requestfactory.shared.impl; +import static com.google.gwt.requestfactory.shared.impl.BaseProxyCategory.stableId; +import static com.google.gwt.requestfactory.shared.impl.Constants.REQUEST_CONTEXT; + import com.google.gwt.autobean.shared.AutoBean; import com.google.gwt.autobean.shared.AutoBeanCodex; import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.autobean.shared.AutoBeanVisitor; import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.ValueCodex; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyChange; import com.google.gwt.requestfactory.shared.EntityProxyId; @@ -28,10 +32,13 @@ import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestTransport.TransportReceiver; import com.google.gwt.requestfactory.shared.ServerFailure; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.Violation; import com.google.gwt.requestfactory.shared.WriteOperation; +import com.google.gwt.requestfactory.shared.impl.posers.DatePoser; import com.google.gwt.requestfactory.shared.messages.EntityCodex; import com.google.gwt.requestfactory.shared.messages.IdMessage; +import com.google.gwt.requestfactory.shared.messages.IdUtil; import com.google.gwt.requestfactory.shared.messages.InvocationMessage; import com.google.gwt.requestfactory.shared.messages.MessageFactory; import com.google.gwt.requestfactory.shared.messages.OperationMessage; @@ -41,6 +48,9 @@ import com.google.gwt.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.LinkedHashMap; @@ -55,20 +65,46 @@ EntityCodex.EntitySource { private class MyViolation implements Violation { + private final BaseProxy currentProxy; private final EntityProxyId<?> id; - private final String path; private final String message; + private final String path; + private final BaseProxy parentProxy; public MyViolation(ViolationMessage message) { - id = getId(message); + // Support violations for value objects. + SimpleProxyId<BaseProxy> baseId = getId(message); + if (baseId instanceof EntityProxyId<?>) { + id = (EntityProxyId<?>) baseId; + } else { + id = null; + } + // The stub is empty, since we don't process any OperationMessages + AutoBean<BaseProxy> stub = getProxyForReturnPayloadGraph(baseId); + + // So pick up the instance that we just sent to the server + AutoBean<?> edited = editedProxies.get(BaseProxyCategory.stableId(stub)); + currentProxy = (BaseProxy) edited.as(); + + // Try to find the original, immutable version. + AutoBean<BaseProxy> parentBean = edited.getTag(PARENT_OBJECT); + parentProxy = parentBean == null ? null : parentBean.as(); path = message.getPath(); this.message = message.getMessage(); } + public BaseProxy getInvalidProxy() { + return currentProxy; + } + public String getMessage() { return message; } + public BaseProxy getOriginalProxy() { + return parentProxy; + } + public String getPath() { return path; } @@ -88,12 +124,20 @@ * Objects are placed into this map by being passed into {@link #edit} or as * an invocation argument. */ - private final Map<SimpleEntityProxyId<?>, AutoBean<?>> editedProxies = new LinkedHashMap<SimpleEntityProxyId<?>, AutoBean<?>>(); + private final Map<SimpleProxyId<?>, AutoBean<?>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<?>>(); /** * A map that contains the canonical instance of an entity to return in the * return graph, since this is built from scratch. */ - private final Map<SimpleEntityProxyId<?>, AutoBean<?>> returnedProxies = new HashMap<SimpleEntityProxyId<?>, AutoBean<?>>(); + private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies = new HashMap<SimpleProxyId<?>, AutoBean<?>>(); + + /** + * 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. + */ + private final Map<Integer, SimpleProxyId<?>> syntheticIds = new HashMap<Integer, SimpleProxyId<?>>(); protected AbstractRequestContext(AbstractRequestFactory factory) { this.requestFactory = factory; @@ -102,21 +146,27 @@ /** * Create a new object, with an ephemeral id. */ - public <T extends EntityProxy> T create(Class<T> clazz) { + public <T extends BaseProxy> T create(Class<T> clazz) { checkLocked(); - AutoBean<T> created = requestFactory.createEntityProxy(clazz, + AutoBean<T> created = requestFactory.createProxy(clazz, requestFactory.allocateId(clazz)); return takeOwnership(created); } - public <T extends EntityProxy> T edit(T object) { - AutoBean<T> bean = checkStreamsNotCrossed(object); + 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>) editedProxies.get(object.stableId()); + AutoBean<T> previouslySeen = (AutoBean<T>) 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 @@ -130,7 +180,7 @@ AutoBean<T> parent = bean; bean = cloneBeanAndCollections(bean); bean.setTag(PARENT_OBJECT, parent); - return takeOwnership(bean); + return bean.as(); } /** @@ -169,9 +219,15 @@ /** * EntityCodex support. */ - public <Q extends EntityProxy> AutoBean<Q> getBeanForPayload( + public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( String serializedProxyId) { - SimpleEntityProxyId<Q> id = requestFactory.getProxyId(serializedProxyId); + SimpleProxyId<Q> id; + if (IdUtil.isSynthetic(serializedProxyId)) { + id = allocateSyntheticId(IdUtil.getTypeToken(serializedProxyId), + IdUtil.getSyntheticId(serializedProxyId)); + } else { + id = requestFactory.getBaseProxyId(serializedProxyId); + } return getProxyForReturnPayloadGraph(id); } @@ -182,7 +238,7 @@ /** * EntityCodex support. */ - public String getSerializedProxyId(EntityProxyId<?> stableId) { + public String getSerializedProxyId(SimpleProxyId<?> stableId) { return requestFactory.getHistoryToken(stableId); } @@ -212,7 +268,7 @@ * EntityCodex support. */ public boolean isEntityType(Class<?> clazz) { - return requestFactory.getTypeToken(clazz) != null; + return requestFactory.isEntityType(clazz); } public boolean isLocked() { @@ -220,6 +276,13 @@ } /** + * EntityCodex support. + */ + public boolean isValueType(Class<?> clazz) { + return requestFactory.isValueType(clazz); + } + + /** * Called by generated subclasses to enqueue a method invocation. */ protected void addInvocation(AbstractRequest<?> request) { @@ -229,6 +292,22 @@ } } + /** + * 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>) syntheticIds.get(syntheticId); + if (toReturn == null) { + toReturn = requestFactory.allocateId(requestFactory.<Q> getTypeFromToken(typeToken)); + syntheticIds.put(syntheticId, toReturn); + } + return toReturn; + } + private void checkLocked() { if (locked) { throw new IllegalStateException("A request is already in progress"); @@ -246,7 +325,7 @@ throw new IllegalArgumentException(object.getClass().getName()); } - RequestContext context = bean.getTag(EntityProxyCategory.REQUEST_CONTEXT); + RequestContext context = bean.getTag(REQUEST_CONTEXT); if (!bean.isFrozen() && context != this) { /* * This means something is way off in the weeds. If a bean is editable, @@ -268,17 +347,64 @@ * Shallow-clones an autobean and makes duplicates of the collection types. A * regular {@link AutoBean#clone} won't duplicate reference properties. */ - private <T> AutoBean<T> cloneBeanAndCollections(AutoBean<T> toClone) { + private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections( + AutoBean<T> toClone) { AutoBean<T> clone = toClone.clone(false); + /* + * Take ownership here to prevent cycles in value objects from overflowing + * the stack. + */ + takeOwnership(clone); clone.accept(new AutoBeanVisitor() { + + @Override + public boolean visitCollectionProperty(String propertyName, + AutoBean<Collection<?>> value, CollectionPropertyContext ctx) { + 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())) { + /* + * Value proxies must be cloned upfront, since the value is replaced + * outright. + */ + for (Object o : value.as()) { + if (o == null) { + collection.add(null); + } else { + collection.add(editProxy((ValueProxy) o)); + } + } + } else { + // For entities and simple values, just alias the values + collection.addAll(value.as()); + } + + ctx.set(collection); + } + return false; + } + @Override public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, PropertyContext ctx) { if (value != null) { - if (List.class == ctx.getType()) { - ctx.set(new ArrayList<Object>((List<?>) value.as())); - } else if (Set.class == ctx.getType()) { - ctx.set(new HashSet<Object>((Set<?>) value.as())); + if (isValueType(ctx.getType())) { + /* + * Value proxies must be cloned upfront, since the value is replaced + * outright. + */ + @SuppressWarnings("unchecked") + AutoBean<ValueProxy> valueBean = (AutoBean<ValueProxy>) value; + ctx.set(editProxy(valueBean.as())); } } return false; @@ -382,32 +508,30 @@ } /** - * Resolves an IdMessage into an EntityProxyId. + * Resolves an IdMessage into an SimpleProxyId. */ - private SimpleEntityProxyId<EntityProxy> getId(IdMessage op) { - SimpleEntityProxyId<EntityProxy> id; - if (op.getClientId() > 0) { - id = requestFactory.getId(op.getTypeToken(), op.getServerId(), - op.getClientId()); - } else { - id = requestFactory.getId(op.getTypeToken(), op.getServerId()); + private SimpleProxyId<BaseProxy> getId(IdMessage op) { + if (op.getSyntheticId() > 0) { + return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId()); } - return id; + if (op.getClientId() > 0) { + return requestFactory.getId(op.getTypeToken(), op.getServerId(), + op.getClientId()); + } + return requestFactory.getId(op.getTypeToken(), op.getServerId()); } /** * Creates or retrieves a new canonical AutoBean to represent the given id in * the returned payload. */ - private <Q extends EntityProxy> AutoBean<Q> getProxyForReturnPayloadGraph( - SimpleEntityProxyId<Q> id) { - assert !id.isEphemeral(); - + private <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph( + SimpleProxyId<Q> id) { @SuppressWarnings("unchecked") AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id); if (bean == null) { Class<Q> proxyClass = id.getProxyClass(); - bean = requestFactory.createEntityProxy(proxyClass, id); + bean = requestFactory.createProxy(proxyClass, id); returnedProxies.put(id, bean); } @@ -417,11 +541,11 @@ /** * Make an EntityProxy immutable. */ - private void makeImmutable(final AutoBean<? extends EntityProxy> toMutate) { + private void makeImmutable(final AutoBean<? extends BaseProxy> toMutate) { // Always diff'ed against itself, producing a no-op toMutate.setTag(PARENT_OBJECT, toMutate); // Act with entity-identity semantics - toMutate.setTag(EntityProxyCategory.REQUEST_CONTEXT, null); + toMutate.setTag(REQUEST_CONTEXT, null); toMutate.setFrozen(true); } @@ -442,37 +566,46 @@ // Compute deltas for each entity seen by the context for (AutoBean<?> currentView : editedProxies.values()) { @SuppressWarnings("unchecked") - SimpleEntityProxyId<EntityProxy> stableId = EntityProxyCategory.stableId((AutoBean<EntityProxy>) currentView); + SimpleProxyId<BaseProxy> stableId = stableId((AutoBean<BaseProxy>) currentView); + assert !stableId.isSynthetic() : "Should not send synthetic id to server"; // The OperationMessages describes operations on exactly one entity OperationMessage operation = f.operation().as(); operation.setTypeToken(requestFactory.getTypeToken(stableId.getProxyClass())); // Find the object to compare against - AutoBean<?> parent = currentView.getTag(PARENT_OBJECT); - if (parent == null) { + AutoBean<?> parent; + if (stableId.isEphemeral()) { // Newly-created object, use a blank object to compare against - parent = requestFactory.createEntityProxy(stableId.getProxyClass(), - stableId); + parent = requestFactory.createProxy(stableId.getProxyClass(), stableId); // 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()); } else { + parent = currentView.getTag(PARENT_OBJECT); // Requests involving existing objects use the persisted id operation.setServerId(stableId.getServerId()); operation.setOperation(WriteOperation.UPDATE); } + assert parent != null; - // Send our version number to the server to cut down on payload size + // Send our version number to the server to cut down on future payloads Integer version = currentView.getTag(Constants.ENCODED_VERSION_PROPERTY); if (version != null) { operation.setVersion(version); } - // Compute what's changed on the client - Map<String, Object> diff = AutoBeanUtils.diff(parent, currentView); + Map<String, Object> diff = Collections.emptyMap(); + if (isEntityType(stableId.getProxyClass())) { + // Compute what's changed on the client + diff = AutoBeanUtils.diff(parent, currentView); + } else if (isValueType(stableId.getProxyClass())) { + // Send everything + diff = AutoBeanUtils.getAllProperties(currentView); + } + if (!diff.isEmpty()) { Map<String, Splittable> propertyMap = new HashMap<String, Splittable>(); for (Map.Entry<String, Object> entry : diff.entrySet()) { @@ -530,9 +663,8 @@ * @param returnRecord the JSON map containing property/value pairs * @param operations the WriteOperation eventns to broadcast over the EventBus */ - private <Q extends EntityProxy> Q processReturnOperation( - SimpleEntityProxyId<Q> id, OperationMessage op, - WriteOperation... operations) { + private <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, + OperationMessage op, WriteOperation... operations) { AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id); toMutate.setTag(Constants.ENCODED_VERSION_PROPERTY, op.getVersion()); @@ -564,6 +696,10 @@ if (properties.containsKey(propertyName)) { Splittable raw = properties.get(propertyName); Object decoded = ValueCodex.decode(ctx.getType(), raw); + // Hack for Date, consider generalizing for "custom serializers" + if (Date.class.equals(ctx.getType())) { + decoded = new DatePoser((Date) decoded); + } ctx.set(decoded); } } @@ -580,7 +716,7 @@ * Notify subscribers if the object differs from when it first came into the * RequestContext. */ - if (operations != null) { + if (operations != null && requestFactory.isEntityType(id.getProxyClass())) { for (WriteOperation writeOperation : operations) { if (writeOperation.equals(WriteOperation.UPDATE) && !requestFactory.hasVersionChanged(id, op.getVersion())) { @@ -588,8 +724,8 @@ continue; } requestFactory.getEventBus().fireEventFromSource( - new EntityProxyChange<EntityProxy>(proxy, writeOperation), - id.getProxyClass()); + new EntityProxyChange<EntityProxy>((EntityProxy) proxy, + writeOperation), id.getProxyClass()); } } return proxy; @@ -607,8 +743,10 @@ } for (OperationMessage op : records) { - SimpleEntityProxyId<EntityProxy> id = getId(op); - if (id.wasEphemeral()) { + SimpleProxyId<?> id = getId(op); + if (id.isEphemeral()) { + processReturnOperation(id, op); + } else if (id.wasEphemeral()) { processReturnOperation(id, op, WriteOperation.PERSIST, WriteOperation.UPDATE); } else { @@ -643,10 +781,9 @@ /** * Make the EnityProxy bean edited and owned by this RequestContext. */ - private <T extends EntityProxy> T takeOwnership(AutoBean<T> bean) { - editedProxies.put(EntityProxyCategory.stableId(bean), bean); - bean.setTag(EntityProxyCategory.REQUEST_CONTEXT, - AbstractRequestContext.this); + private <T extends BaseProxy> T takeOwnership(AutoBean<T> bean) { + editedProxies.put(stableId(bean), bean); + bean.setTag(REQUEST_CONTEXT, AbstractRequestContext.this); return bean.as(); } }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java index 3689365..48cfab3 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -15,9 +15,12 @@ */ package com.google.gwt.requestfactory.shared.impl; +import static com.google.gwt.requestfactory.shared.impl.Constants.STABLE_ID; + import com.google.gwt.autobean.shared.AutoBean; import com.google.gwt.autobean.shared.AutoBeanFactory; import com.google.gwt.event.shared.EventBus; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; import com.google.gwt.requestfactory.shared.Request; @@ -30,7 +33,7 @@ import java.util.Map.Entry; /** - * + * Base type for generated RF interfaces. */ public abstract class AbstractRequestFactory extends IdFactory implements RequestFactory { @@ -49,17 +52,16 @@ private RequestTransport transport; /** - * Creates a new EntityProxy with an assigned ID. + * Creates a new proxy with an assigned ID. */ - public <T extends EntityProxy> AutoBean<T> createEntityProxy(Class<T> clazz, - SimpleEntityProxyId<T> id) { + public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, + SimpleProxyId<T> id) { AutoBean<T> created = getAutoBeanFactory().create(clazz); if (created == null) { throw new IllegalArgumentException("Unknown EntityProxy type " + clazz.getName()); } - created.setTag(EntityProxyCategory.REQUEST_FACTORY, this); - created.setTag(EntityProxyCategory.STABLE_ID, id); + created.setTag(STABLE_ID, id); return created; } @@ -79,7 +81,7 @@ protected RequestData makeRequestData() { return new RequestData( "com.google.gwt.requestfactory.shared.impl.FindRequest::find", - new Object[]{proxyId}, propertyRefs, proxyId.getProxyClass(), null); + new Object[] {proxyId}, propertyRefs, proxyId.getProxyClass(), null); } }; } @@ -88,6 +90,14 @@ return eventBus; } + public String getHistoryToken(Class<? extends EntityProxy> clazz) { + return getTypeToken(clazz); + } + + public String getHistoryToken(EntityProxyId<?> proxy) { + return getHistoryToken((SimpleProxyId<?>) proxy); + } + public Class<? extends EntityProxy> getProxyClass(String historyToken) { String typeToken = IdUtil.getTypeToken(historyToken); if (typeToken != null) { @@ -96,6 +106,11 @@ return getTypeFromToken(historyToken); } + @SuppressWarnings("unchecked") + public <T extends EntityProxy> EntityProxyId<T> getProxyId(String historyToken) { + return (EntityProxyId<T>) getBaseProxyId(historyToken); + } + public RequestTransport getRequestTransport() { return transport; } @@ -120,8 +135,7 @@ * Used by {@link AbstractRequestContext} to quiesce update events for objects * that haven't truly changed. */ - protected boolean hasVersionChanged(SimpleEntityProxyId<?> id, - int observedVersion) { + protected boolean hasVersionChanged(SimpleProxyId<?> id, int observedVersion) { String key = getHistoryToken(id); Integer existingVersion = version.get(key); // Return true if we haven't seen this before or the versions differ @@ -132,4 +146,5 @@ } return toReturn; } + }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/BaseProxyCategory.java b/user/src/com/google/gwt/requestfactory/shared/impl/BaseProxyCategory.java new file mode 100644 index 0000000..9782088 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/BaseProxyCategory.java
@@ -0,0 +1,81 @@ +/* + * 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.gwt.requestfactory.shared.impl; + +import static com.google.gwt.requestfactory.shared.impl.Constants.REQUEST_CONTEXT; +import static com.google.gwt.requestfactory.shared.impl.Constants.STABLE_ID; + +import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanUtils; +import com.google.gwt.requestfactory.shared.BaseProxy; + +/** + * Contains behaviors common to all proxy instances. + */ +public class BaseProxyCategory { + /** + * Sniff all return values and ensure that if the current bean is a mutable + * EntityProxy, that its return values are mutable. + */ + // CHECKSTYLE_OFF + public static <T> T __intercept(AutoBean<?> bean, T returnValue) { + // CHECKSTYLE_ON + + AbstractRequestContext context = requestContext(bean); + + /* + * The context will be null if the bean is immutable. If the context is + * locked, don't try to edit. + */ + if (context == null || context.isLocked()) { + return returnValue; + } + + /* + * EntityProxies need to be recorded specially by the RequestContext, so + * delegate to the edit() method for wiring up the context. + */ + if (returnValue instanceof BaseProxy) { + @SuppressWarnings("unchecked") + T toReturn = (T) context.editProxy((BaseProxy) returnValue); + return toReturn; + } + + if (returnValue instanceof Poser) { + ((Poser<?>) returnValue).setFrozen(false); + } + + /* + * We're returning some object that's not an EntityProxy, most likely a + * Collection type. At the very least, propagate the current RequestContext + * so that editable chains can be constructed. + */ + AutoBean<T> otherBean = AutoBeanUtils.getAutoBean(returnValue); + if (otherBean != null) { + otherBean.setTag(REQUEST_CONTEXT, bean.getTag(REQUEST_CONTEXT)); + } + return returnValue; + } + + public static AbstractRequestContext requestContext(AutoBean<?> bean) { + return bean.getTag(REQUEST_CONTEXT); + } + + public static <T extends BaseProxy> SimpleProxyId<T> stableId( + AutoBean<? extends T> bean) { + return bean.getTag(STABLE_ID); + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/CollectionProperty.java b/user/src/com/google/gwt/requestfactory/shared/impl/CollectionProperty.java deleted file mode 100644 index 7c2ff23..0000000 --- a/user/src/com/google/gwt/requestfactory/shared/impl/CollectionProperty.java +++ /dev/null
@@ -1,42 +0,0 @@ -/* - * 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.gwt.requestfactory.shared.impl; - -import java.util.Collection; - -/** - * @param <C> the type of the Container, must be List or Set - * @param <E> the type of the element the container contains - */ -public class CollectionProperty<C extends Collection<?>, E> extends Property<C> { - - private Class<E> leafType; - - public CollectionProperty(String name, String displayName, Class<C> colType, - Class<E> type) { - super(name, displayName, colType); - this.leafType = type; - } - - public CollectionProperty(String name, Class<C> colType, Class<E> type) { - super(name, colType); - this.leafType = type; - } - - public Class<E> getLeafType() { - return leafType; - } -}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java b/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java index 9ebc01a..58f3543 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java
@@ -16,25 +16,12 @@ package com.google.gwt.requestfactory.shared.impl; /** - * Contains a variety of contstants shared between client and server code. + * Contains a variety of AutoBean tag constants to prevent typos. */ public interface Constants { - - /** - * Property on a proxy JSO that holds its encoded server side data store id. - */ - String ENCODED_ID_PROPERTY = "!id"; - /** - * Property on a proxy JSO that holds its server side version data. - */ - String ENCODED_VERSION_PROPERTY = "!version"; - /** - * Id property that server entity objects are required to define. - */ - String ENTITY_ID_PROPERTY = "id"; - /** - * Version property that server entity objects are required to define. - */ - Property<Integer> ENTITY_VERSION_PROPERTY = new Property<Integer>("version", - Integer.class); + String DOMAIN_OBJECT = "domainObject"; + String ENCODED_VERSION_PROPERTY = "version"; + String IN_RESPONSE = "inResponse"; + String REQUEST_CONTEXT = "requestContext"; + String STABLE_ID = "stableId"; }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/EntityProxyCategory.java b/user/src/com/google/gwt/requestfactory/shared/impl/EntityProxyCategory.java index 6fe67c6..26ed7f2 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/EntityProxyCategory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/EntityProxyCategory.java
@@ -15,19 +15,22 @@ */ package com.google.gwt.requestfactory.shared.impl; +import static com.google.gwt.requestfactory.shared.impl.BaseProxyCategory.requestContext; +import static com.google.gwt.requestfactory.shared.impl.Constants.STABLE_ID; + import com.google.gwt.autobean.shared.AutoBean; import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.requestfactory.shared.EntityProxy; -import com.google.gwt.requestfactory.shared.RequestFactory; /** - * Contains static implementation methods used by the AutoBean generator. + * Contains static implementation of EntityProxy-specific methods. */ public class EntityProxyCategory { - static final String REQUEST_CONTEXT = "requestContext"; - static final String REQUEST_FACTORY = "requestFactory"; - static final String STABLE_ID = "stableId"; + /** + * EntityProxies are equal if they are from the same RequestContext and their + * stableIds are equal. + */ public static boolean equals(AutoBean<? extends EntityProxy> bean, Object o) { if (!(o instanceof EntityProxy)) { return false; @@ -50,59 +53,12 @@ return stableId(bean).hashCode(); } - public static AbstractRequestContext requestContext(AutoBean<?> bean) { - return (AbstractRequestContext) bean.getTag(REQUEST_CONTEXT); - } - - public static RequestFactory requestFactory( - AutoBean<? extends EntityProxy> bean) { - return (RequestFactory) bean.getTag(REQUEST_FACTORY); - } - - @SuppressWarnings("unchecked") + /** + * Effectively overrides {@link BaseProxyCategory#stableId(AutoBean)} to + * return a narrower bound. + */ public static <T extends EntityProxy> SimpleEntityProxyId<T> stableId( AutoBean<? extends T> bean) { - return (SimpleEntityProxyId<T>) bean.getTag(STABLE_ID); - } - - /** - * Sniff all return values and ensure that if the current bean is a mutable - * EntityProxy, that its return values are mutable. - */ - // CHECKSTYLE_OFF - public static <T> T __intercept(AutoBean<?> bean, T returnValue) { - // CHECKSTYLE_ON - - AbstractRequestContext context = requestContext(bean); - - /* - * The context will be null if the bean is immutable. If the context is - * locked, don't try to edit. - */ - if (context == null || context.isLocked()) { - return returnValue; - } - - /* - * EntityProxies need to be recorded specially by the RequestContext, so - * delegate to the edit() method for wiring up the context. - */ - if (returnValue instanceof EntityProxy) { - @SuppressWarnings("unchecked") - T toReturn = (T) context.edit((EntityProxy) returnValue); - return toReturn; - } - - /* - * We're returning some object that's not an EntityProxy, most likely a - * Collection type. At the very least, propagate the current RequestContext - * so that editable chains can be constructed. - */ - AutoBean<T> otherBean = AutoBeanUtils.getAutoBean(returnValue); - if (otherBean != null) { - otherBean.setTag(EntityProxyCategory.REQUEST_CONTEXT, - bean.getTag(EntityProxyCategory.REQUEST_CONTEXT)); - } - return returnValue; + return bean.getTag(STABLE_ID); } }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/EnumProperty.java b/user/src/com/google/gwt/requestfactory/shared/impl/EnumProperty.java deleted file mode 100644 index 842d745..0000000 --- a/user/src/com/google/gwt/requestfactory/shared/impl/EnumProperty.java +++ /dev/null
@@ -1,43 +0,0 @@ -/* - * 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.gwt.requestfactory.shared.impl; - -/** - * Defines a property of a {@link com.google.gwt.requestfactory.shared.EntityProxy}. - * - * @param <V> the type of the property's value - */ -public class EnumProperty<V> extends Property<V> { - - private V[] values; - - /** - * @param name the property's name and displayName - * @param type the class of the property's value - * @param values the result of Enum.values() method - */ - public EnumProperty(String name, Class<V> type, V[] values) { - super(name, type); - this.values = values; - } - - /** - * Returns the values that the enum may take on. - */ - public V[] getValues() { - return values; - } -}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java index cc106e4..2d9ac74 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
@@ -15,15 +15,16 @@ */ package com.google.gwt.requestfactory.shared.impl; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxy; -import com.google.gwt.requestfactory.shared.EntityProxyId; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.messages.IdUtil; import java.util.HashMap; import java.util.Map; /** - * Handles common code for creating SimpleEntityProxyIds. + * Handles common code for creating SimpleProxyIds. */ public abstract class IdFactory { /** @@ -31,57 +32,114 @@ * resolves the problem of EntityProxyIds hashcodes changing after persist. * Only ids that are created in the RequestFactory are stored here. */ - private final Map<String, SimpleEntityProxyId<?>> ephemeralIds = new HashMap<String, SimpleEntityProxyId<?>>(); + private final Map<String, SimpleProxyId<?>> ephemeralIds = new HashMap<String, SimpleProxyId<?>>(); /** * Allocates an ephemeral proxy id. This object is only valid for the lifetime * of the RequestFactory. */ - public <P extends EntityProxy> SimpleEntityProxyId<P> allocateId( - Class<P> clazz) { - SimpleEntityProxyId<P> toReturn = new SimpleEntityProxyId<P>(clazz, - ephemeralIds.size() + 1); + public <P extends BaseProxy> SimpleProxyId<P> allocateId(Class<P> clazz) { + SimpleProxyId<P> toReturn = createId(clazz, ephemeralIds.size() + 1); ephemeralIds.put(getHistoryToken(toReturn), toReturn); return toReturn; } - public String getHistoryToken(Class<? extends EntityProxy> clazz) { - return getTypeToken(clazz); + /** + * Allocates a synthetic proxy id. This object is only valid for the lifetime + * of a request. + */ + public <P extends BaseProxy> SimpleProxyId<P> allocateSyntheticId( + Class<P> clazz, int syntheticId) { + SimpleProxyId<P> toReturn = createId(clazz, "%" + syntheticId); + toReturn.setSyntheticId(syntheticId); + return toReturn; } - public String getHistoryToken(EntityProxyId<?> proxy) { - SimpleEntityProxyId<?> id = (SimpleEntityProxyId<?>) proxy; + /** + * A utility function to handle generic type conversion. This method will also + * assert that {@code clazz} is actually an EntityProxy type. + */ + @SuppressWarnings("unchecked") + public <P extends EntityProxy> Class<P> asEntityProxy( + Class<? extends BaseProxy> clazz) { + assert isEntityType(clazz); + return (Class<P>) clazz; + } + + /** + * A utility function to handle generic type conversion. This method will also + * assert that {@code clazz} is actually a ValueProxy type. + */ + @SuppressWarnings("unchecked") + public <P extends ValueProxy> Class<P> asValueProxy( + Class<? extends BaseProxy> clazz) { + assert isEntityType(clazz); + return (Class<P>) clazz; + } + + public <P extends BaseProxy> SimpleProxyId<P> getBaseProxyId( + String historyToken) { + assert !IdUtil.isSynthetic(historyToken) : "Synthetic id resolution" + + " should be handled by AbstractRequestContext"; + if (IdUtil.isPersisted(historyToken)) { + return getId(IdUtil.getTypeToken(historyToken), + IdUtil.getServerId(historyToken)); + } + if (IdUtil.isEphemeral(historyToken)) { + @SuppressWarnings("unchecked") + SimpleProxyId<P> toReturn = (SimpleProxyId<P>) ephemeralIds.get(historyToken); + + /* + * This is tested in FindServiceTest.testFetchUnpersistedFutureId. In + * order to get here, the user would have to get an unpersisted history + * token and attempt to use it with a different RequestFactory instance. + * This could occur if an ephemeral token were bookmarked. In this case, + * we'll create a token, however it will never match anything. + */ + if (toReturn == null) { + Class<P> clazz = checkTypeToken(IdUtil.getTypeToken(historyToken)); + toReturn = createId(clazz, -1 * ephemeralIds.size()); + ephemeralIds.put(historyToken, toReturn); + } + + return toReturn; + } + throw new IllegalArgumentException(historyToken); + } + + public String getHistoryToken(SimpleProxyId<?> proxy) { + SimpleProxyId<?> id = (SimpleProxyId<?>) proxy; + String token = getTypeToken(proxy.getProxyClass()); if (id.isEphemeral()) { - return IdUtil.ephemeralId(id.getClientId(), - getHistoryToken(proxy.getProxyClass())); + return IdUtil.ephemeralId(id.getClientId(), token); + } else if (id.isSynthetic()) { + return IdUtil.syntheticId(id.getSyntheticId(), token); } else { - return IdUtil.persistedId(id.getServerId(), - getHistoryToken(proxy.getProxyClass())); + return IdUtil.persistedId(id.getServerId(), token); } } /** - * Create or retrieve a SimpleEntityProxyId. + * Create or retrieve a SimpleProxyId. */ - public <P extends EntityProxy> SimpleEntityProxyId<P> getId(Class<P> clazz, + public <P extends BaseProxy> SimpleProxyId<P> getId(Class<P> clazz, String serverId) { return getId(getTypeToken(clazz), serverId); } /** - * Create or retrieve a SimpleEntityProxyId. If both the serverId and clientId - * are specified and the id is ephemeral, it will be updated with the server - * id. + * Create or retrieve a SimpleProxyId. If both the serverId and clientId are + * specified and the id is ephemeral, it will be updated with the server id. */ - public <P extends EntityProxy> SimpleEntityProxyId<P> getId(Class<P> clazz, + public <P extends BaseProxy> SimpleProxyId<P> getId(Class<P> clazz, String serverId, Integer clientId) { return getId(getTypeToken(clazz), serverId, clientId); } /** - * Create or retrieve a SimpleEntityProxyId. + * Create or retrieve a SimpleProxyId. */ - public <P extends EntityProxy> SimpleEntityProxyId<P> getId(String typeToken, + public <P extends BaseProxy> SimpleProxyId<P> getId(String typeToken, String serverId) { return getId(typeToken, serverId, null); } @@ -91,7 +149,7 @@ * are specified and the id is ephemeral, it will be updated with the server * id. */ - public <P extends EntityProxy> SimpleEntityProxyId<P> getId(String typeToken, + public <P extends BaseProxy> SimpleProxyId<P> getId(String typeToken, String serverId, Integer clientId) { /* * If there's a clientId, that probably means we've just created a brand-new @@ -101,12 +159,12 @@ // Try a cache lookup for the ephemeral key String ephemeralKey = IdUtil.ephemeralId(clientId, typeToken); @SuppressWarnings("unchecked") - SimpleEntityProxyId<P> toReturn = (SimpleEntityProxyId<P>) ephemeralIds.get(ephemeralKey); + SimpleProxyId<P> toReturn = (SimpleProxyId<P>) ephemeralIds.get(ephemeralKey); // Do we need to allocate an ephemeral id? if (toReturn == null) { Class<P> clazz = getTypeFromToken(typeToken); - toReturn = new SimpleEntityProxyId<P>(clazz, clientId); + toReturn = createId(clazz, clientId); ephemeralIds.put(ephemeralKey, toReturn); } @@ -133,7 +191,7 @@ String serverKey = IdUtil.persistedId(serverId, typeToken); @SuppressWarnings("unchecked") - SimpleEntityProxyId<P> toReturn = (SimpleEntityProxyId<P>) ephemeralIds.get(serverKey); + SimpleProxyId<P> toReturn = (SimpleProxyId<P>) ephemeralIds.get(serverKey); if (toReturn != null) { // A cache hit for a locally-created object that has been persisted return toReturn; @@ -145,38 +203,14 @@ * case for read-dominated applications. */ Class<P> clazz = getTypeFromToken(typeToken); - return new SimpleEntityProxyId<P>(clazz, serverId); + return createId(clazz, serverId); } - public <P extends EntityProxy> SimpleEntityProxyId<P> getProxyId( - String historyToken) { - if (IdUtil.isPersisted(historyToken)) { - return getId(IdUtil.getTypeToken(historyToken), - IdUtil.getServerId(historyToken)); - } - if (IdUtil.isEphemeral(historyToken)) { - @SuppressWarnings("unchecked") - SimpleEntityProxyId<P> toReturn = (SimpleEntityProxyId<P>) ephemeralIds.get(historyToken); + public abstract boolean isEntityType(Class<?> clazz); - /* - * This is tested in FindServiceTest.testFetchUnpersistedFutureId. In - * order to get here, the user would have to get an unpersisted history - * token and attempt to use it with a different RequestFactory instance. - * This could occur if an ephemeral token was bookmarked. In this case, - * we'll create a token, however it will never match anything. - */ - if (toReturn == null) { - Class<P> clazz = checkTypeToken(IdUtil.getTypeToken(historyToken)); - toReturn = new SimpleEntityProxyId<P>(clazz, -1 * ephemeralIds.size()); - ephemeralIds.put(historyToken, toReturn); - } + public abstract boolean isValueType(Class<?> clazz); - return toReturn; - } - throw new IllegalArgumentException(historyToken); - } - - protected abstract <P extends EntityProxy> Class<P> getTypeFromToken( + protected abstract <P extends BaseProxy> Class<P> getTypeFromToken( String typeToken); protected abstract String getTypeToken(Class<?> clazz); @@ -190,4 +224,32 @@ return clazz; } + private <P extends BaseProxy> SimpleProxyId<P> createId(Class<P> clazz, + int clientId) { + SimpleProxyId<P> toReturn; + if (isValueType(clazz)) { + toReturn = new SimpleProxyId<P>(clazz, clientId); + } else { + @SuppressWarnings("unchecked") + SimpleProxyId<P> temp = (SimpleProxyId<P>) new SimpleEntityProxyId<EntityProxy>( + asEntityProxy(clazz), clientId); + toReturn = (SimpleProxyId<P>) temp; + } + return toReturn; + } + + private <P extends BaseProxy> SimpleProxyId<P> createId(Class<P> clazz, + String serverId) { + SimpleProxyId<P> toReturn; + if (isValueType(clazz)) { + toReturn = new SimpleProxyId<P>(clazz, serverId); + } else { + @SuppressWarnings("unchecked") + SimpleProxyId<P> temp = (SimpleProxyId<P>) new SimpleEntityProxyId<EntityProxy>( + asEntityProxy(clazz), serverId); + toReturn = (SimpleProxyId<P>) temp; + } + return toReturn; + } + } \ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/Poser.java b/user/src/com/google/gwt/requestfactory/shared/impl/Poser.java new file mode 100644 index 0000000..0c16b50 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/Poser.java
@@ -0,0 +1,30 @@ +/* + * 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.gwt.requestfactory.shared.impl; + +/** + * Used to lock down mutable, non-proxy, value objects when their owning proxy + * is frozen. + * + * @param <T> the type of simple value the Poser is standing in for + */ +public interface Poser<T> { + T getPosedValue(); + + boolean isFrozen(); + + void setFrozen(boolean frozen); +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/Property.java b/user/src/com/google/gwt/requestfactory/shared/impl/Property.java deleted file mode 100644 index 15e8b84..0000000 --- a/user/src/com/google/gwt/requestfactory/shared/impl/Property.java +++ /dev/null
@@ -1,83 +0,0 @@ -/* - * 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.gwt.requestfactory.shared.impl; - -/** - * Defines a property of an {@link EntityProxy}. - * - * @param <V> the type of the property's value - */ -public class Property<V> { - private final String name; - private final Class<V> type; - private final String displayName; - - /** - * @param name the property's name and displayName - * @param type the class of the property's value - */ - public Property(String name, Class<V> type) { - this(name, name, type); - } - - /** - * @param name the property's name - * @param displayName the property's user visible name - * @param type the class of the property's value - */ - public Property(String name, String displayName, Class<V> type) { - this.name = name; - this.displayName = displayName; - this.type = type; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Property<?>)) { - return false; - } - Property<?> property = (Property<?>) o; - if (name != null ? !name.equals(property.name) : property.name != null) { - return false; - } - if (type != null ? !type.equals(property.type) : property.type != null) { - return false; - } - return true; - } - - public String getDisplayName() { - return displayName; - } - - public String getName() { - return name; - } - - public Class<V> getType() { - return type; - } - - @Override - public int hashCode() { - int nameHash = name == null ? 0 : name.hashCode(); - int typeHash = type == null ? 0 : type.hashCode(); - return (nameHash * 31) ^ typeHash; - } -}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/SimpleEntityProxyId.java b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleEntityProxyId.java index 5a98ae6..027a1d6 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/SimpleEntityProxyId.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleEntityProxyId.java
@@ -17,144 +17,28 @@ import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; -import com.google.gwt.requestfactory.shared.messages.IdUtil; /** - * Nothing fancy. + * Extends SimpleProxyId with the correct parameterization to implement + * EntityProxyId. * * @param <P> the type of EntityProxy object the id describes */ -public class SimpleEntityProxyId<P extends EntityProxy> implements - EntityProxyId<P> { - /** - * A placeholder value for {@link #clientId} to indicate the id was not - * created locally. - */ - public static final int NEVER_EPHEMERAL = -1; - - /** - * The client-side id is ephemeral, and is valid only during the lifetime of a - * module. Any use of the client-side id except to send to the server as a - * bookkeeping exercise is wrong. - */ - private final int clientId; - - /** - * The hashcode of the id must remain stable, even if the server id is later - * assigned. - */ - private final int hashCode; - - private final Class<P> proxyClass; - - /** - * The serverId is totally opaque to the client. It's probably a - * base64-encoded string, but it could be digits of pi. Any code that does - * anything other than send the contents of this field back to the server is - * wrong. - */ - private String serverId; +public class SimpleEntityProxyId<P extends EntityProxy> extends + SimpleProxyId<P> implements EntityProxyId<P> { /** * Construct an ephemeral id. May be called only from * {@link IdFactory#getId()}. */ SimpleEntityProxyId(Class<P> proxyClass, int clientId) { - assert proxyClass != null; - this.clientId = clientId; - this.proxyClass = proxyClass; - hashCode = clientId; + super(proxyClass, clientId); } /** * Construct a stable id. May only be called from {@link IdFactory#getId()} */ - SimpleEntityProxyId(Class<P> proxyClass, String serverId) { - assert proxyClass != null; - assert serverId != null && !serverId.contains("@") - && !"null".equals(serverId); - setServerId(serverId); - clientId = NEVER_EPHEMERAL; - hashCode = serverId.hashCode(); - this.proxyClass = proxyClass; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof SimpleEntityProxyId<?>)) { - return false; - } - SimpleEntityProxyId<?> other = (SimpleEntityProxyId<?>) o; - if (!proxyClass.equals(other.proxyClass)) { - return false; - } - - if (clientId != NEVER_EPHEMERAL && clientId == other.clientId) { - /* - * Unexpected: It should be the case that locally-created ids are never - * aliased and will be caught by the first if statement. - */ - return true; - } - - if (serverId != null && serverId.equals(other.serverId)) { - return true; - } - return false; - } - - public int getClientId() { - return clientId; - } - - public Class<P> getProxyClass() { - return proxyClass; - } - - public String getServerId() { - return serverId; - } - - @Override - public int hashCode() { - return hashCode; - } - - public boolean isEphemeral() { - return serverId == null; - } - - /** - * Allows the server id token to be set. This method may be called exactly - * once over the lifetime of an id. - */ - public void setServerId(String serverId) { - if (this.serverId != null) { - throw new IllegalStateException(); - } - assert !"null".equals(serverId); - this.serverId = serverId; - } - - /** - * For debugging use only. - */ - @Override - public String toString() { - if (isEphemeral()) { - return IdUtil.ephemeralId(clientId, proxyClass.getName()); - } else { - return IdUtil.persistedId(serverId, proxyClass.getName()); - } - } - - /** - * Returns <code>true</code> if the id was created as an ephemeral id. - */ - public boolean wasEphemeral() { - return clientId != NEVER_EPHEMERAL; + SimpleEntityProxyId(Class<P> proxyClass, String encodedAddress) { + super(proxyClass, encodedAddress); } }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java new file mode 100644 index 0000000..943c970 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java
@@ -0,0 +1,192 @@ +/* + * 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.gwt.requestfactory.shared.impl; + +import com.google.gwt.requestfactory.shared.BaseProxy; +import com.google.gwt.requestfactory.shared.messages.IdUtil; + +/** + * The base implementation of id objects in the RequestFactory system. This type + * exists to allow ValueProxies to be implemented in the same manner as an + * EntityProxy as far as metadata maintenance is concerned. There is a specific + * subtype {@link SimpleEntityProxyId} which implements the requisite public + * interface for EntityProxy types. + * + * @param <P> the type of BaseProxy object the id describes + */ +public class SimpleProxyId<P extends BaseProxy> { + /** + * A placeholder value for {@link #clientId} to indicate the id was not + * created locally. + */ + public static final int NEVER_EPHEMERAL = -1; + + /** + * The client-side id is ephemeral, and is valid only during the lifetime of a + * module. Any use of the client-side id except to send to the server as a + * bookkeeping exercise is wrong. + */ + private final int clientId; + + /** + * The encodedAddress is totally opaque to the client. It's probably a + * base64-encoded string, but it could be digits of pi. Any code that does + * anything other than send the contents of this field back to the server is + * wrong. + */ + private String encodedAddress; + + /** + * The hashcode of the id must remain stable, even if the server id is later + * assigned. + */ + private final int hashCode; + + /** + * The EntityProxy type. + */ + private final Class<P> proxyClass; + + /** + * A flag to indicate that the id is synthetic, that it is not valid beyond + * the duration of the request. + */ + private int syntheticId; + + /** + * Construct an ephemeral id. May be called only from + * {@link IdFactory#createId()}. + */ + SimpleProxyId(Class<P> proxyClass, int clientId) { + assert proxyClass != null; + this.clientId = clientId; + this.proxyClass = proxyClass; + hashCode = clientId; + } + + /** + * Construct a stable id. May only be called from {@link IdFactory#createId()} + */ + SimpleProxyId(Class<P> proxyClass, String encodedAddress) { + assert proxyClass != null; + assert encodedAddress != null && !encodedAddress.contains("@") + && !"null".equals(encodedAddress); + setServerId(encodedAddress); + clientId = NEVER_EPHEMERAL; + hashCode = encodedAddress.hashCode(); + this.proxyClass = proxyClass; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SimpleProxyId<?>)) { + return false; + } + SimpleProxyId<?> other = (SimpleProxyId<?>) o; + if (!proxyClass.equals(other.proxyClass)) { + return false; + } + + if (clientId != NEVER_EPHEMERAL && clientId == other.clientId) { + /* + * Unexpected: It should be the case that locally-created ids are never + * aliased and will be caught by the first if statement. + */ + return true; + } + + if (encodedAddress != null && encodedAddress.equals(other.encodedAddress)) { + return true; + } + return false; + } + + public int getClientId() { + return clientId; + } + + public Class<P> getProxyClass() { + return proxyClass; + } + + /** + * TODO: Rename to getAddress(). + */ + public String getServerId() { + return encodedAddress; + } + + /** + * A flag to indicate that the id is synthetic, that it is not valid beyond + * the duration of the request. + */ + public int getSyntheticId() { + return syntheticId; + } + + @Override + public int hashCode() { + return hashCode; + } + + public boolean isEphemeral() { + return encodedAddress == null; + } + + public boolean isSynthetic() { + return syntheticId > 0; + } + + /** + * Allows the server address token to be set. This method may be called + * exactly once over the lifetime of an id. + */ + public void setServerId(String encodedAddress) { + if (this.encodedAddress != null) { + throw new IllegalStateException(); + } + assert !"null".equals(encodedAddress); + this.encodedAddress = encodedAddress; + } + + public void setSyntheticId(int syntheticId) { + this.syntheticId = syntheticId; + } + + /** + * For debugging use only. + */ + @Override + public String toString() { + if (isEphemeral()) { + return IdUtil.ephemeralId(clientId, proxyClass.getName()); + } else if (isSynthetic()) { + return IdUtil.syntheticId(syntheticId, proxyClass.getName()); + } else { + return IdUtil.persistedId(encodedAddress, proxyClass.getName()); + } + } + + /** + * Returns <code>true</code> if the id was created as an ephemeral id. + */ + public boolean wasEphemeral() { + return clientId != NEVER_EPHEMERAL; + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java new file mode 100644 index 0000000..b2b3422 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java
@@ -0,0 +1,61 @@ +/* + * 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.gwt.requestfactory.shared.impl; + +import static com.google.gwt.requestfactory.shared.impl.BaseProxyCategory.stableId; + +import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanUtils; +import com.google.gwt.requestfactory.shared.ValueProxy; + +/** + * Contains static implementation of EntityProxy-specific methods. + */ +public class ValueProxyCategory { + + /** + * ValueProxies are equal if they are from the same RequestContext and all of + * their properties are equal. + */ + public static boolean equals(AutoBean<? extends ValueProxy> bean, Object o) { + if (!(o instanceof ValueProxy)) { + return false; + } + AutoBean<ValueProxy> other = AutoBeanUtils.getAutoBean((ValueProxy) o); + if (other == null) { + // Unexpected, could be an user-provided implementation? + return false; + } + if (!stableId(bean).getProxyClass().equals(stableId(other).getProxyClass())) { + // Compare AppleProxies to AppleProxies + return false; + } + + /* + * Comparison of ValueProxies is based solely on property values. Unlike an + * EntityProxy, neither the id nor the RequestContext is used + */ + return AutoBeanUtils.getAllProperties(bean).equals( + AutoBeanUtils.getAllProperties(other)); + } + + /** + * Hashcode depends on property values. + */ + public static int hashCode(AutoBean<? extends ValueProxy> bean) { + return AutoBeanUtils.getAllProperties(bean).hashCode(); + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/posers/DatePoser.java b/user/src/com/google/gwt/requestfactory/shared/impl/posers/DatePoser.java new file mode 100644 index 0000000..403ebc3 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/posers/DatePoser.java
@@ -0,0 +1,93 @@ +/* + * 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.gwt.requestfactory.shared.impl.posers; + +import com.google.gwt.requestfactory.shared.impl.Poser; + +import java.util.Date; + +/** + * A sometimes-mutable implementation of {@link Date}. + */ +@SuppressWarnings("deprecation") +public class DatePoser extends Date implements Poser<Date> { + private boolean frozen; + + public DatePoser(Date copy) { + super(copy.getTime()); + setFrozen(true); + } + + public Date getPosedValue() { + return new Date(getTime()); + } + + public boolean isFrozen() { + return frozen; + } + + @Override + public void setDate(int date) { + checkFrozen(); + super.setDate(date); + } + + public void setFrozen(boolean frozen) { + this.frozen = frozen; + } + + @Override + public void setHours(int hours) { + checkFrozen(); + super.setHours(hours); + } + + @Override + public void setMinutes(int minutes) { + checkFrozen(); + super.setMinutes(minutes); + } + + @Override + public void setMonth(int month) { + checkFrozen(); + super.setMonth(month); + } + + @Override + public void setSeconds(int seconds) { + checkFrozen(); + super.setSeconds(seconds); + } + + @Override + public void setTime(long time) { + checkFrozen(); + super.setTime(time); + } + + @Override + public void setYear(int year) { + checkFrozen(); + super.setYear(year); + } + + private void checkFrozen() { + if (frozen) { + throw new IllegalStateException("The Date has been frozen"); + } + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java b/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java index 09b8aac..bcf5245 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java
@@ -16,12 +16,16 @@ package com.google.gwt.requestfactory.shared.messages; import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.ValueCodex; import com.google.gwt.autobean.shared.impl.LazySplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; -import com.google.gwt.requestfactory.shared.EntityProxy; +import com.google.gwt.requestfactory.shared.BaseProxy; import com.google.gwt.requestfactory.shared.EntityProxyId; +import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory; +import com.google.gwt.requestfactory.shared.impl.Poser; +import com.google.gwt.requestfactory.shared.impl.SimpleProxyId; import java.util.ArrayList; import java.util.Collection; @@ -37,12 +41,13 @@ * Abstracts the process by which EntityProxies are created. */ public interface EntitySource { - <Q extends EntityProxy> AutoBean<Q> getBeanForPayload( - String serializedProxyId); + <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(String serializedProxyId); - String getSerializedProxyId(EntityProxyId<?> stableId); + String getSerializedProxyId(SimpleProxyId<?> stableId); boolean isEntityType(Class<?> clazz); + + boolean isValueType(Class<?> clazz); } /** @@ -88,7 +93,8 @@ return collection; } - if (source.isEntityType(type) || EntityProxyId.class.equals(type)) { + if (source.isEntityType(type) || source.isValueType(type) + || EntityProxyId.class.equals(type)) { return source.getBeanForPayload(split.asString()).as(); } @@ -133,12 +139,17 @@ return new LazySplittable(toReturn.toString()); } - if (value instanceof EntityProxy) { - value = source.getSerializedProxyId(((EntityProxy) value).stableId()); + if (value instanceof BaseProxy) { + AutoBean<BaseProxy> autoBean = AutoBeanUtils.getAutoBean((BaseProxy) value); + value = BaseProxyCategory.stableId(autoBean); } - if (value instanceof EntityProxyId<?>) { - value = source.getSerializedProxyId((EntityProxyId<?>) value); + if (value instanceof SimpleProxyId<?>) { + value = source.getSerializedProxyId((SimpleProxyId<?>) value); + } + + if (value instanceof Poser<?>) { + value = ((Poser<?>) value).getPosedValue(); } return ValueCodex.encode(value);
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java b/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java index 11c758f..7be22c8 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java
@@ -24,6 +24,7 @@ String CLIENT_ID = "C"; String SERVER_ID = "S"; String TYPE_TOKEN = "T"; + String SYNTHETIC_ID = "Y"; @PropertyName(CLIENT_ID) int getClientId(); @@ -31,6 +32,9 @@ @PropertyName(SERVER_ID) String getServerId(); + @PropertyName(SYNTHETIC_ID) + int getSyntheticId(); + @PropertyName(TYPE_TOKEN) String getTypeToken(); @@ -40,6 +44,9 @@ @PropertyName(SERVER_ID) void setServerId(String value); + @PropertyName(SYNTHETIC_ID) + void setSyntheticId(int value); + @PropertyName(TYPE_TOKEN) void setTypeToken(String value); } \ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java b/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java index de8c906..5efd614 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java
@@ -19,9 +19,11 @@ * Common functions for slicing and dicing EntityProxy ids. */ public class IdUtil { - private static final String ANY_SEPARATOR_PATTERN = "@[01]@"; + private static final String ANY_SEPARATOR_PATTERN = "@[012]@"; private static final String EPHEMERAL_SEPARATOR = "@1@"; private static final String TOKEN_SEPARATOR = "@0@"; + private static final String SYNTHETIC_SEPARATOR = "@2@"; + private static final int ID_TOKEN_INDEX = 0; private static final int TYPE_TOKEN_INDEX = 1; @@ -37,6 +39,10 @@ return asPersisted(encodedId)[ID_TOKEN_INDEX]; } + public static int getSyntheticId(String encodedId) { + return Integer.valueOf(asSynthetic(encodedId)[ID_TOKEN_INDEX]); + } + public static String getTypeToken(String encodedId) { String[] split = asAny(encodedId); if (split.length == 2) { @@ -53,10 +59,18 @@ return encodedId.contains(TOKEN_SEPARATOR); } + public static boolean isSynthetic(String encodedId) { + return encodedId.contains(SYNTHETIC_SEPARATOR); + } + public static String persistedId(String serverId, String typeToken) { return serverId + TOKEN_SEPARATOR + typeToken; } + public static String syntheticId(int syntheticId, String historyToken) { + return syntheticId + SYNTHETIC_SEPARATOR + historyToken; + } + private static String[] asAny(String encodedId) { return encodedId.split(ANY_SEPARATOR_PATTERN); } @@ -69,8 +83,12 @@ return encodedId.split(TOKEN_SEPARATOR); } + private static String[] asSynthetic(String encodedId) { + return encodedId.split(SYNTHETIC_SEPARATOR); + } + /** - * Utility class + * Utility class. */ private IdUtil() { }
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java index cde822c..5742010 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
@@ -22,10 +22,10 @@ * The factory for creating RequestFactory wire messages. */ public interface MessageFactory extends AutoBeanFactory { - AutoBean<InvocationMessage> invocation(); - AutoBean<ServerFailureMessage> failure(); + AutoBean<InvocationMessage> invocation(); + AutoBean<OperationMessage> operation(); AutoBean<RequestMessage> request();
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/ResponseMessage.java b/user/src/com/google/gwt/requestfactory/shared/messages/ResponseMessage.java index e4135c1..2555ea5 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/ResponseMessage.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/ResponseMessage.java
@@ -21,7 +21,7 @@ import java.util.List; /** - * The result of fulfilling a request on the server; + * The result of fulfilling a request on the server. */ public interface ResponseMessage extends VersionedMessage { String GENERAL_FAILURE = "F";
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/ViolationMessage.java b/user/src/com/google/gwt/requestfactory/shared/messages/ViolationMessage.java index ea57a59..b891ac0 100644 --- a/user/src/com/google/gwt/requestfactory/shared/messages/ViolationMessage.java +++ b/user/src/com/google/gwt/requestfactory/shared/messages/ViolationMessage.java
@@ -18,7 +18,7 @@ import com.google.gwt.autobean.shared.AutoBean.PropertyName; /** - * Represents a ConstraintViolation + * Represents a ConstraintViolation. */ public interface ViolationMessage extends IdMessage { String MESSAGE = "M";
diff --git a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java index 956bfd4..d7164d4 100644 --- a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java +++ b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
@@ -43,7 +43,6 @@ import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Violation; -import com.google.gwt.requestfactory.shared.impl.Property; import com.google.gwt.user.client.TakesValue; import com.google.gwt.user.client.ui.HasText; @@ -260,9 +259,10 @@ EditorData[] data = m.getEditorData(); assertNotNull(data); assertEquals(2, data.length); - assertEquals(Arrays.asList("b", "b.string"), Arrays.asList( - data[0].getPath(), data[1].getPath())); - assertEquals(Arrays.asList("bEditor().asEditor()", "stringEditor()"), + assertEquals(Arrays.asList("b", "b.string"), + Arrays.asList(data[0].getPath(), data[1].getPath())); + assertEquals( + Arrays.asList("bEditor().asEditor()", "stringEditor()"), Arrays.asList(data[0].getSimpleExpression(), data[1].getSimpleExpression())); } @@ -285,9 +285,11 @@ "bEditor().viewEditor()"), Arrays.asList(data[0].getExpression(), data[1].getExpression(), data[2].getExpression(), data[3].getExpression())); - assertEquals(Arrays.asList(true, false, true, false), Arrays.asList( - data[0].isDelegateRequired(), data[1].isDelegateRequired(), - data[2].isDelegateRequired(), data[3].isDelegateRequired())); + assertEquals( + Arrays.asList(true, false, true, false), + Arrays.asList(data[0].isDelegateRequired(), + data[1].isDelegateRequired(), data[2].isDelegateRequired(), + data[3].isDelegateRequired())); } public void testListDriver() throws UnableToCompleteException { @@ -333,10 +335,12 @@ public void testMissingGetter() { UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder(); builder.setLowestLogLevel(TreeLogger.ERROR); - builder.expectError(EditorModel.noGetterMessage("missing", - types.findType("t.MissingGetterEditorDriver.AProxy")), null); - builder.expectError(EditorModel.noGetterMessage("yetAgain", - types.findType("t.MissingGetterEditorDriver.AProxy")), null); + builder.expectError( + EditorModel.noGetterMessage("missing", + types.findType("t.MissingGetterEditorDriver.AProxy")), null); + builder.expectError( + EditorModel.noGetterMessage("yetAgain", + types.findType("t.MissingGetterEditorDriver.AProxy")), null); builder.expectError(EditorModel.poisonedMessage(), null); UnitTestTreeLogger testLogger = builder.createLogger(); try { @@ -354,8 +358,9 @@ public void testSanityErrorMessages() { UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder(); builder.setLowestLogLevel(TreeLogger.ERROR); - builder.expectError(EditorModel.unexpectedInputTypeMessage(rfedType, - types.getJavaLangObject()), null); + builder.expectError( + EditorModel.unexpectedInputTypeMessage(rfedType, + types.getJavaLangObject()), null); builder.expectError(EditorModel.mustExtendMessage(rfedType), null); builder.expectError( EditorModel.tooManyInterfacesMessage(types.findType("t.TooManyInterfacesEditorDriver")), @@ -384,9 +389,11 @@ public void testUnparameterizedEditor() { UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder(); builder.setLowestLogLevel(TreeLogger.ERROR); - builder.expectError(EditorModel.noEditorParameterizationMessage( - types.findType(Editor.class.getName()), types.findType( - SimpleEditor.class.getName()).isGenericType().getRawType()), null); + builder.expectError( + EditorModel.noEditorParameterizationMessage( + types.findType(Editor.class.getName()), + types.findType(SimpleEditor.class.getName()).isGenericType().getRawType()), + null); UnitTestTreeLogger testLogger = builder.createLogger(); try { new EditorModel(testLogger, @@ -486,7 +493,7 @@ return code; } }, new MockJavaResource("t.CyclicEditorDriver") { - // Tests error-detection when the editor graph isn't a DAG + // Tests error-detection when the editor graph isn't a DAG @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -510,7 +517,7 @@ return code; } }, new MockJavaResource("t.DottedPathEditorDriver") { - // Tests error-detection when the editor graph isn't a DAG + // Tests error-detection when the editor graph isn't a DAG @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -532,7 +539,7 @@ return code; } }, new MockJavaResource("t.ListEditor") { - // Tests error-detection when the editor graph isn't a DAG + // Tests error-detection when the editor graph isn't a DAG @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -555,7 +562,8 @@ return code; } }, new MockJavaResource("t.MissingGetterEditorDriver") { - // Tests error-detection when the editor structure doesn't match the proxy + // Tests error-detection when the editor structure doesn't match the + // proxy @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -657,7 +665,7 @@ return code; } }, new MockJavaResource("t.TooManyInterfacesEditorDriver") { - // Tests a Driver interface that extends more than RFED + // Tests a Driver interface that extends more than RFED @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -671,7 +679,8 @@ return code; } }, new MockJavaResource("t.UnparameterizedEditorEditorDriver") { - // Tests error-detection when the editor structure doesn't match the proxy + // Tests error-detection when the editor structure doesn't match the + // proxy @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -692,7 +701,8 @@ return code; } }, new MockJavaResource("t.UsesIsEditorDriver") { - // Tests error-detection when the editor structure doesn't match the proxy + // Tests error-detection when the editor structure doesn't match the + // proxy @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -721,7 +731,8 @@ return code; } }, new MockJavaResource("t.UsesIsEditorAndEditorDriver") { - // Tests error-detection when the editor structure doesn't match the proxy + // Tests error-detection when the editor structure doesn't match the + // proxy @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -751,7 +762,7 @@ return code; } }, new MockJavaResource("java.util.List") { - // Tests a Driver interface that extends more than RFED + // Tests a Driver interface that extends more than RFED @Override protected CharSequence getContent() { StringBuilder code = new StringBuilder(); @@ -774,7 +785,6 @@ new RealJavaResource(IsEditor.class), new EmptyMockJavaResource(Iterable.class), new RealJavaResource(LeafValueEditor.class), - new EmptyMockJavaResource(Property.class), new EmptyMockJavaResource(EntityProxy.class), new EmptyMockJavaResource(RequestFactory.class), new RealJavaResource(RequestFactoryEditorDriver.class),
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java index 642d96a..19d5130 100644 --- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java +++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -27,6 +27,7 @@ import com.google.gwt.requestfactory.shared.SimpleEnum; import com.google.gwt.requestfactory.shared.SimpleFooProxy; import com.google.gwt.requestfactory.shared.SimpleFooRequest; +import com.google.gwt.requestfactory.shared.SimpleValueProxy; import com.google.gwt.requestfactory.shared.Violation; import com.google.gwt.requestfactory.shared.impl.SimpleEntityProxyId; @@ -187,48 +188,6 @@ } } - public void disabled_testEchoComplexFutures() { - // relate futures on the server. Check if the relationship is still present - // on the client. - delayTestFinish(DELAY_TEST_FINISH); - final SimpleFooEventHandler<SimpleFooProxy> handler = new SimpleFooEventHandler<SimpleFooProxy>(); - EntityProxyChange.registerForProxyType(req.getEventBus(), - SimpleFooProxy.class, handler); - SimpleFooRequest context = req.simpleFooRequest(); - final SimpleFooProxy simpleFoo = context.create(SimpleFooProxy.class); - final SimpleBarProxy simpleBar = context.create(SimpleBarProxy.class); - context.echoComplex(simpleFoo, simpleBar).fire( - new Receiver<SimpleFooProxy>() { - @Override - public void onSuccess(SimpleFooProxy response) { - assertEquals(0, handler.totalEventCount); - checkStableIdEquals(simpleFoo, response); - SimpleBarProxy responseBar = response.getBarField(); - assertNotNull(responseBar); - checkStableIdEquals(simpleBar, responseBar); - finishTestAndReset(); - } - }); - } - - public void disabled_testEchoSimpleFutures() { - // tests if futureIds can be echoed back. - delayTestFinish(DELAY_TEST_FINISH); - final SimpleFooEventHandler<SimpleFooProxy> handler = new SimpleFooEventHandler<SimpleFooProxy>(); - EntityProxyChange.registerForProxyType(req.getEventBus(), - SimpleFooProxy.class, handler); - SimpleFooRequest context = req.simpleFooRequest(); - final SimpleFooProxy simpleFoo = context.create(SimpleFooProxy.class); - context.echo(simpleFoo).fire(new Receiver<SimpleFooProxy>() { - @Override - public void onSuccess(SimpleFooProxy response) { - assertEquals(0, handler.totalEventCount); - checkStableIdEquals(simpleFoo, response); - finishTestAndReset(); - } - }); - } - /** * Test that removing a parent entity and implicitly removing the child sends * an event to the client that the child was removed. @@ -925,7 +884,7 @@ */ public void testNullValueInIntegerListRequest() { delayTestFinish(DELAY_TEST_FINISH); - List<Integer> list = Arrays.asList(new Integer[]{1, 2, null}); + List<Integer> list = Arrays.asList(new Integer[] {1, 2, null}); final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInIntegerList( list); fooReq.fire(new Receiver<Void>() { @@ -941,7 +900,7 @@ */ public void testNullValueInStringListRequest() { delayTestFinish(DELAY_TEST_FINISH); - List<String> list = Arrays.asList(new String[]{"nonnull", "null", null}); + List<String> list = Arrays.asList(new String[] {"nonnull", "null", null}); final Request<Void> fooReq = req.simpleFooRequest().receiveNullValueInStringList( list); fooReq.fire(new Receiver<Void>() { @@ -986,11 +945,6 @@ }); } - /* - * TODO: all these tests should check the final values. It will be easy when - * we have better persistence than the singleton pattern. - */ - public void testPersistExistingEntityExistingRelation() { delayTestFinish(DELAY_TEST_FINISH); @@ -1123,6 +1077,11 @@ } /* + * TODO: all these tests should check the final values. It will be easy when + * we have better persistence than the singleton pattern. + */ + + /* * Find Entity2 Create Entity, Persist Entity Relate Entity2 to Entity Persist * Entity */ @@ -1268,11 +1227,6 @@ }); } - /* - * TODO: all these tests should check the final values. It will be easy when - * we have better persistence than the singleton pattern. - */ - public void testPersistRelation() { delayTestFinish(DELAY_TEST_FINISH); @@ -1316,11 +1270,6 @@ }); } - /* - * TODO: all these tests should check the final values. It will be easy when - * we have better persistence than the singleton pattern. - */ - public void testPersistSelfOneToManyExistingEntityExistingRelation() { delayTestFinish(DELAY_TEST_FINISH); @@ -1374,6 +1323,11 @@ * TODO: all these tests should check the final values. It will be easy when * we have better persistence than the singleton pattern. */ + + /* + * TODO: all these tests should check the final values. It will be easy when + * we have better persistence than the singleton pattern. + */ public void testPersistValueListNull() { delayTestFinish(DELAY_TEST_FINISH); simpleFooRequest().findSimpleFooById(999L).fire( @@ -1402,6 +1356,11 @@ * TODO: all these tests should check the final values. It will be easy when * we have better persistence than the singleton pattern. */ + + /* + * TODO: all these tests should check the final values. It will be easy when + * we have better persistence than the singleton pattern. + */ public void testPersistValueListRemove() { delayTestFinish(DELAY_TEST_FINISH); simpleFooRequest().findSimpleFooById(999L).fire( @@ -1617,7 +1576,7 @@ Set<SimpleBarProxy> setField = fooProxy.getOneToManySetField(); final int listCount = setField.size(); assertContains(setField, barProxy); - setField.remove(barProxy); + setField.remove(context.edit(barProxy)); assertNotContains(setField, barProxy); updReq.fire(new Receiver<SimpleFooProxy>() { @Override @@ -1962,6 +1921,80 @@ } /** + * Check if a graph of unpersisted objects can be echoed. + */ + public void testUnpersistedEchoComplexGraph() { + delayTestFinish(DELAY_TEST_FINISH); + final SimpleFooEventHandler<SimpleFooProxy> handler = new SimpleFooEventHandler<SimpleFooProxy>(); + EntityProxyChange.registerForProxyType(req.getEventBus(), + SimpleFooProxy.class, handler); + SimpleFooRequest context = req.simpleFooRequest(); + final SimpleBarProxy simpleBar = context.create(SimpleBarProxy.class); + simpleBar.setUnpersisted(true); + final SimpleFooProxy simpleFoo = context.create(SimpleFooProxy.class); + simpleFoo.setUnpersisted(true); + simpleFoo.setBarField(simpleBar); + context.echoComplex(simpleFoo, simpleBar).with("barField").fire( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + assertEquals(0, handler.totalEventCount); + checkStableIdEquals(simpleFoo, response); + SimpleBarProxy responseBar = response.getBarField(); + assertNotNull(responseBar); + checkStableIdEquals(simpleBar, responseBar); + finishTestAndReset(); + } + }); + } + + /** + * Check if an unpersisted object can be echoed. + */ + public void testUnpersistedEchoObject() { + delayTestFinish(DELAY_TEST_FINISH); + final SimpleFooEventHandler<SimpleFooProxy> handler = new SimpleFooEventHandler<SimpleFooProxy>(); + EntityProxyChange.registerForProxyType(req.getEventBus(), + SimpleFooProxy.class, handler); + SimpleFooRequest context = req.simpleFooRequest(); + final SimpleFooProxy simpleFoo = context.create(SimpleFooProxy.class); + simpleFoo.setUnpersisted(true); + context.echo(simpleFoo).fire(new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + assertEquals(0, handler.totalEventCount); + checkStableIdEquals(simpleFoo, response); + finishTestAndReset(); + } + }); + } + + /** + * Return an unpersisted object from a service method and echo it. + */ + public void testUnpersistedObjectFetch() { + delayTestFinish(DELAY_TEST_FINISH); + req.simpleFooRequest().getUnpersistedInstance().fire( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(final SimpleFooProxy created) { + assertNotNull(created); + assertTrue(created.getUnpersisted()); + req.simpleFooRequest().echo(created).fire( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + assertNotNull(response); + assertEquals(created.stableId(), response.stableId()); + assertTrue(response.getUnpersisted()); + finishTestAndReset(); + } + }); + } + }); + } + + /** * This is analagous to FindServiceTest.testFetchDeletedEntity() only we're * trying to invoke a method on the deleted entity using a stale EntityProxy * reference on the client. @@ -2019,6 +2052,228 @@ }); } + public void testValueObjectCreateSetRetrieveUpdate() { + delayTestFinish(DELAY_TEST_FINISH); + SimpleFooRequest req = simpleFooRequest(); + req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + SimpleFooRequest req = simpleFooRequest(); + + // Create + final SimpleValueProxy created = req.create(SimpleValueProxy.class); + created.setNumber(42); + created.setString("Hello world!"); + created.setSimpleFoo(response); + // Test cycles in value + created.setSimpleValue(Arrays.asList(created)); + + // Set + response = req.edit(response); + response.setSimpleValue(created); + + // Retrieve + req.persistAndReturnSelf().using(response).with( + "simpleValue.simpleFoo", "simpleValue.simpleValue").to( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + SimpleValueProxy value = response.getSimpleValue(); + assertEquals(42, value.getNumber()); + assertEquals("Hello world!", value.getString()); + assertSame(response, value.getSimpleFoo()); + assertSame(value, value.getSimpleValue().get(0)); + + try { + // Require owning object to be editable + response.getSimpleValue().setNumber(43); + fail("Should have thrown exception"); + } catch (IllegalStateException expected) { + } + + // Update + SimpleFooRequest req = simpleFooRequest(); + response = req.edit(response); + response.getSimpleValue().setNumber(43); + req.persistAndReturnSelf().using(response).with("simpleValue").to( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + assertEquals(43, response.getSimpleValue().getNumber()); + finishTestAndReset(); + } + }).fire(); + } + }).fire(); + } + }); + } + + public void testValueObjectCreateSetRetrieveUpdateViaList() { + delayTestFinish(DELAY_TEST_FINISH); + SimpleFooRequest req = simpleFooRequest(); + req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + SimpleFooRequest req = simpleFooRequest(); + + // Create + final SimpleValueProxy created = req.create(SimpleValueProxy.class); + created.setNumber(42); + created.setString("Hello world!"); + created.setSimpleFoo(response); + + // Set + response = req.edit(response); + response.setSimpleValues(Arrays.asList(created)); + + // Retrieve + req.persistAndReturnSelf().using(response).with("simpleValues").to( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + SimpleValueProxy value = response.getSimpleValues().get(0); + assertEquals(42, value.getNumber()); + + try { + // Require owning object to be editable + response.getSimpleValues().get(0).setNumber(43); + fail("Should have thrown exception"); + } catch (IllegalStateException expected) { + } + + // Update + SimpleFooRequest req = simpleFooRequest(); + response = req.edit(response); + response.getSimpleValues().get(0).setNumber(43); + req.persistAndReturnSelf().using(response).with("simpleValues").to( + new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + assertEquals(43, + response.getSimpleValues().get(0).getNumber()); + finishTestAndReset(); + } + }).fire(); + } + }).fire(); + } + }); + } + + public void testValueObjectEquality() { + SimpleFooRequest req = simpleFooRequest(); + SimpleValueProxy a = req.create(SimpleValueProxy.class); + SimpleValueProxy b = req.create(SimpleValueProxy.class); + checkEqualityAndHashcode(a, b); + + a.setString("Hello"); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + + b.setString("Hello"); + checkEqualityAndHashcode(a, b); + } + + public void testValueObjectViolationsOnCreate() { + delayTestFinish(DELAY_TEST_FINISH); + SimpleFooRequest req = simpleFooRequest(); + final SimpleValueProxy value = req.create(SimpleValueProxy.class); + value.setShouldBeNull("Hello world"); + + SimpleFooProxy foo = req.create(SimpleFooProxy.class); + foo.setSimpleValue(value); + req.echo(foo).fire(new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + fail(); + } + + @Override + public void onViolation(Set<Violation> errors) { + assertEquals(1, errors.size()); + Violation v = errors.iterator().next(); + assertEquals(value, v.getInvalidProxy()); + assertNull(v.getOriginalProxy()); + assertEquals("shouldBeNull", v.getPath()); + assertNull(v.getProxyId()); + finishTestAndReset(); + } + }); + } + + public void testValueObjectViolationsOnEdit() { + delayTestFinish(DELAY_TEST_FINISH); + simpleFooRequest().returnValueProxy().fire( + new Receiver<SimpleValueProxy>() { + @Override + public void onSuccess(final SimpleValueProxy response) { + SimpleFooRequest req = simpleFooRequest(); + final SimpleValueProxy value = req.edit(response); + value.setShouldBeNull("Hello world"); + SimpleFooProxy foo = req.create(SimpleFooProxy.class); + foo.setSimpleValue(value); + req.echo(foo).fire(new Receiver<SimpleFooProxy>() { + @Override + public void onSuccess(SimpleFooProxy response) { + fail(); + } + + @Override + public void onViolation(Set<Violation> errors) { + assertEquals(1, errors.size()); + Violation v = errors.iterator().next(); + assertEquals(value, v.getInvalidProxy()); + assertEquals(response, v.getOriginalProxy()); + assertEquals("shouldBeNull", v.getPath()); + assertNull(v.getProxyId()); + finishTestAndReset(); + } + }); + } + }); + } + + /** + * Since a ValueProxy cannot be passed into RequestContext edit, a proxy + * returned from a service method should be mutable by default. + */ + public void testValueObjectReturnedFromRequestIsImmutable() { + delayTestFinish(DELAY_TEST_FINISH); + simpleFooRequest().returnValueProxy().fire( + new Receiver<SimpleValueProxy>() { + @Override + public void onSuccess(SimpleValueProxy a) { + try { + a.setNumber(77); + fail(); + } catch (IllegalStateException expected) { + } + try { + // Ensure Dates comply with ValueProxy mutation behaviors + a.getDate().setTime(1); + fail(); + } catch (IllegalStateException expected) { + } + SimpleFooRequest ctx = simpleFooRequest(); + final SimpleValueProxy toCheck = ctx.edit(a); + toCheck.setNumber(77); + toCheck.getDate().setTime(1); + ctx.returnValueProxy().fire(new Receiver<SimpleValueProxy>() { + @Override + public void onSuccess(SimpleValueProxy b) { + b = simpleFooRequest().edit(b); + // Now check that same value is equal across contexts + b.setNumber(77); + b.setDate(new Date(1)); + checkEqualityAndHashcode(toCheck, b); + finishTestAndReset(); + } + }); + } + }); + } + public void testViolationAbsent() { delayTestFinish(DELAY_TEST_FINISH);
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java index c162c63..ef88d8b 100644 --- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java +++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java
@@ -69,6 +69,14 @@ eventBus = req.getEventBus(); } + protected void checkEqualityAndHashcode(Object a, Object b) { + assertNotNull(a); + assertNotNull(b); + assertEquals(a.hashCode(), b.hashCode()); + assertEquals(a, b); + assertEquals(b, a); + } + protected void checkStableIdEquals(EntityProxy expected, EntityProxy actual) { assertEquals(expected.stableId(), actual.stableId()); assertEquals(expected.stableId().hashCode(), actual.stableId().hashCode());
diff --git a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java index 74d268a..3e160e5 100644 --- a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java +++ b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
@@ -34,7 +34,7 @@ import com.google.gwt.requestfactory.shared.RequestContext; import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Service; -import com.google.gwt.requestfactory.shared.impl.Property; +import com.google.gwt.requestfactory.shared.ValueProxy; import junit.framework.TestCase; @@ -267,11 +267,11 @@ toReturn.addAll(Arrays.asList(new Resource[] { new EmptyMockJavaResource(Iterable.class), - new EmptyMockJavaResource(Property.class), new EmptyMockJavaResource(EntityProxy.class), new EmptyMockJavaResource(InstanceRequest.class), new EmptyMockJavaResource(RequestFactory.class), new EmptyMockJavaResource(Receiver.class), + new EmptyMockJavaResource(ValueProxy.class), new RealJavaResource(Request.class), new RealJavaResource(Service.class),
diff --git a/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java index 24739f7..46218c4 100644 --- a/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java +++ b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
@@ -24,10 +24,12 @@ import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.Service; import com.google.gwt.requestfactory.shared.SimpleRequestFactory; +import com.google.gwt.requestfactory.shared.ValueProxy; import com.google.gwt.requestfactory.shared.impl.FindRequest; import junit.framework.TestCase; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -101,7 +103,6 @@ return 0; } } - @ProxyFor(DomainWithOverloads.class) interface DomainWithOverloadsProxy extends EntityProxy { void foo(); @@ -142,6 +143,13 @@ Request<Integer> doesNotExist(int a); } + static class Value { + } + + @ProxyFor(value = Value.class) + interface MyValueProxy extends ValueProxy { + } + RequestFactoryInterfaceValidator v; /** @@ -164,6 +172,51 @@ v.validateRequestContext(FindRequest.class.getName()); } + @ProxyFor(HasListDomain.class) + interface HasList extends EntityProxy { + List<ReachableOnlyThroughReturnedList> getList(); + + void setList(List<ReachableOnlyThroughParamaterList> list); + } + + static class HasListDomain extends Domain { + List<Domain> getList() { + return null; + } + + void setList(List<Domain> value) { + } + + public String getId() { + return null; + } + + public int getVersion() { + return 0; + } + } + + @ProxyFor(Domain.class) + interface ReachableOnlyThroughReturnedList extends EntityProxy { + }; + @ProxyFor(Domain.class) + interface ReachableOnlyThroughParamaterList extends EntityProxy { + }; + + /** + * Make sure that proxy types referenced through type parameters of method + * return types and paramaters types are examined. + */ + public void testFollowingTypeParameters() { + v.validateEntityProxy(HasList.class.getName()); + assertNotNull(v.getEntityProxyTypeName(HasListDomain.class.getName(), + HasList.class.getName())); + assertNotNull(v.getEntityProxyTypeName(Domain.class.getName(), + ReachableOnlyThroughParamaterList.class.getName())); + assertNotNull(v.getEntityProxyTypeName(Domain.class.getName(), + ReachableOnlyThroughReturnedList.class.getName())); + } + /** * Ensure that the <clinit> methods don't interfere with validation. */ @@ -225,6 +278,11 @@ assertFalse(v.isPoisoned()); } + public void testValueType() { + v.validateValueProxy(MyValueProxy.class.getName()); + assertFalse(v.isPoisoned()); + } + @Override protected void setUp() throws Exception { Logger logger = Logger.getLogger("");
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleBar.java b/user/test/com/google/gwt/requestfactory/server/SimpleBar.java index d75ff3b..d0a625c 100644 --- a/user/test/com/google/gwt/requestfactory/server/SimpleBar.java +++ b/user/test/com/google/gwt/requestfactory/server/SimpleBar.java
@@ -1,12 +1,12 @@ /* * 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 @@ -88,12 +88,7 @@ public static SimpleBar getSingleton() { return findSimpleBar("1L"); } - - public static SimpleBar returnFirst(List<SimpleBar> list) { - SimpleBar toReturn = list.get(0); - return toReturn; - } - + public static void reset() { resetImpl(); } @@ -121,11 +116,17 @@ return instance; } + public static SimpleBar returnFirst(List<SimpleBar> list) { + SimpleBar toReturn = list.get(0); + return toReturn; + } + Integer version = 1; private String id = "999L"; private boolean findFails; private boolean isNew = true; + private boolean unpersisted; private String userName; public SimpleBar() { @@ -142,7 +143,11 @@ } public String getId() { - return id; + return unpersisted ? null : id; + } + + public Boolean getUnpersisted() { + return unpersisted; } public String getUserName() { @@ -150,7 +155,7 @@ } public Integer getVersion() { - return version; + return unpersisted ? null : version; } public void persist() { @@ -175,6 +180,10 @@ this.id = id; } + public void setUnpersisted(Boolean unpersisted) { + this.unpersisted = unpersisted; + } + public void setUserName(String userName) { this.userName = userName; }
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java index 417aef2..460621d 100644 --- a/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java +++ b/user/test/com/google/gwt/requestfactory/server/SimpleFoo.java
@@ -1,12 +1,12 @@ /* * 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 @@ -151,6 +151,12 @@ return foo1; } + public static SimpleFoo getUnpersistedInstance() { + SimpleFoo foo = new SimpleFoo(); + foo.setUnpersisted(true); + return foo; + } + public static Boolean processBooleanList(List<Boolean> values) { return values.get(0); } @@ -273,6 +279,14 @@ return null; } + public static SimpleValue returnValueProxy() { + SimpleValue toReturn = new SimpleValue(); + toReturn.setNumber(42); + toReturn.setString("Hello world!"); + toReturn.setDate(new Date()); + return toReturn; + } + @SuppressWarnings("unused") private static Integer privateMethod() { return 0; @@ -321,6 +335,8 @@ private List<Integer> numberListField; + private SimpleValue simpleValueField; + /* * isChanged is just a quick-and-dirty way to get version-ing for now. * Currently, only set by setUserName and setIntId. TODO for later: Use a @@ -328,6 +344,8 @@ */ boolean isChanged; + private boolean unpersisted; + public SimpleFoo() { intId = 42; version = 1; @@ -433,7 +451,7 @@ } public Long getId() { - return id; + return unpersisted ? null : id; } public Integer getIntId() { @@ -486,12 +504,24 @@ return shortField; } + public SimpleValue getSimpleValue() { + return simpleValueField; + } + + public List<SimpleValue> getSimpleValues() { + return Arrays.asList(simpleValueField); + } + + public boolean getUnpersisted() { + return unpersisted; + } + public String getUserName() { return userName; } public Integer getVersion() { - return version; + return unpersisted ? null : version; } public String hello(SimpleBar bar) { @@ -665,6 +695,18 @@ this.shortField = shortField; } + public void setSimpleValue(SimpleValue simpleValueField) { + this.simpleValueField = simpleValueField; + } + + public void setSimpleValues(List<SimpleValue> simpleValueField) { + this.simpleValueField = simpleValueField.get(0); + } + + public void setUnpersisted(Boolean unpersisted) { + this.unpersisted = unpersisted; + } + public void setUserName(String userName) { if (!this.userName.equals(userName)) { this.userName = userName; @@ -686,7 +728,7 @@ /** * Persist this entity and all child entities. This method can handle loops. - * + * * @param processed the entities that have been processed */ private void persistCascadingAndReturnSelfImpl(Set<SimpleFoo> processed) {
diff --git a/user/test/com/google/gwt/requestfactory/server/SimpleValue.java b/user/test/com/google/gwt/requestfactory/server/SimpleValue.java new file mode 100644 index 0000000..57c2119 --- /dev/null +++ b/user/test/com/google/gwt/requestfactory/server/SimpleValue.java
@@ -0,0 +1,85 @@ +/* + * 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.gwt.requestfactory.server; + +import java.util.Date; +import java.util.List; + +import javax.validation.constraints.Null; + +/** + * A domain object that is used to demonstrate value-object behaviors. + */ +public class SimpleValue { + private Date date; + private int number; + private SimpleFoo simpleFoo; + /** + * Constraint violation testing. + */ + @Null + private String shouldBeNull; + private List<SimpleValue> simpleValue; + private String string; + + public Date getDate() { + return date; + } + + public int getNumber() { + return number; + } + + public String getShouldBeNull() { + return shouldBeNull; + } + + public SimpleFoo getSimpleFoo() { + return simpleFoo; + } + + public List<SimpleValue> getSimpleValue() { + return simpleValue; + } + + public String getString() { + return string; + } + + public void setDate(Date date) { + this.date = date; + } + + public void setShouldBeNull(String value) { + this.shouldBeNull = value; + } + + public void setNumber(int number) { + this.number = number; + } + + public void setSimpleFoo(SimpleFoo simpleFoo) { + this.simpleFoo = simpleFoo; + } + + public void setSimpleValue(List<SimpleValue> simpleValue) { + this.simpleValue = simpleValue; + } + + public void setString(String string) { + this.string = string; + } +}
diff --git a/user/test/com/google/gwt/requestfactory/shared/BaseFooProxy.java b/user/test/com/google/gwt/requestfactory/shared/BaseFooProxy.java index 2a40ca3..a628bf3 100644 --- a/user/test/com/google/gwt/requestfactory/shared/BaseFooProxy.java +++ b/user/test/com/google/gwt/requestfactory/shared/BaseFooProxy.java
@@ -20,6 +20,7 @@ import java.util.Date; import java.util.List; import java.util.Set; + /** * A simple proxy used for testing. Has an int field and date field. Add other * data types as their support gets built in. @@ -27,59 +28,57 @@ public interface BaseFooProxy extends EntityProxy { SimpleBarProxy getBarField(); - + SimpleBarProxy getBarNullField(); - + BigDecimal getBigDecimalField(); - + BigInteger getBigIntField(); - + Boolean getBoolField(); - + Byte getByteField(); - + Character getCharField(); - + Date getCreated(); - + Double getDoubleField(); - + SimpleEnum getEnumField(); - + Float getFloatField(); - + Integer getIntId(); - + Long getLongField(); - + String getNullField(); - - Boolean getOtherBoolField(); - - Integer getPleaseCrash(); - - String getPassword(); - - Short getShortField(); - - String getUserName(); - - List<SimpleBarProxy> getOneToManyField(); - - List<SimpleFooProxy> getSelfOneToManyField(); List<Integer> getNumberListField(); + List<SimpleBarProxy> getOneToManyField(); + Set<SimpleBarProxy> getOneToManySetField(); - void setOneToManyField(List<SimpleBarProxy> field); + Boolean getOtherBoolField(); - void setOneToManySetField(Set<SimpleBarProxy> field); + String getPassword(); - void setSelfOneToManyField(List<SimpleFooProxy> field); + Integer getPleaseCrash(); - void setNumberListField(List<Integer> field); - + List<SimpleFooProxy> getSelfOneToManyField(); + + Short getShortField(); + + SimpleValueProxy getSimpleValue(); + + List<SimpleValueProxy> getSimpleValues(); + + Boolean getUnpersisted(); + + String getUserName(); + void setBarField(SimpleBarProxy barField); void setBarNullField(SimpleBarProxy barNullField); @@ -99,21 +98,34 @@ void setDoubleField(Double d); void setFloatField(Float f); - + void setIntId(Integer intId); void setLongField(Long longField); void setNullField(String nullField); + void setNumberListField(List<Integer> field); + + void setOneToManyField(List<SimpleBarProxy> field); + + void setOneToManySetField(Set<SimpleBarProxy> field); + void setOtherBoolField(Boolean boolField); void setPassword(String password); void setPleaseCrash(Integer dummy); + void setSelfOneToManyField(List<SimpleFooProxy> field); + void setShortField(Short s); + void setSimpleValue(SimpleValueProxy value); + + void setSimpleValues(List<SimpleValueProxy> value); + + void setUnpersisted(Boolean unpersisted); + void setUserName(String userName); - }
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java b/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java index 6c8c1ae..e3fd376 100644 --- a/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java +++ b/user/test/com/google/gwt/requestfactory/shared/SimpleBarProxy.java
@@ -23,6 +23,8 @@ public interface SimpleBarProxy extends EntityProxy { Boolean getFindFails(); + Boolean getUnpersisted(); + /* * NB: The lack of a getId() here is intentional, to ensure that the system * does not assume that the id property is available to the client. @@ -32,6 +34,8 @@ void setFindFails(Boolean fails); + void setUnpersisted(Boolean b); + void setUserName(String userName); EntityProxyId<SimpleBarProxy> stableId();
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java b/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java index 9ae1831..a166f9e 100644 --- a/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java +++ b/user/test/com/google/gwt/requestfactory/shared/SimpleFooRequest.java
@@ -51,6 +51,8 @@ Request<SimpleFooProxy> getTripletReference(); + Request<SimpleFooProxy> getUnpersistedInstance(); + InstanceRequest<SimpleFooProxy, String> hello(SimpleBarProxy proxy); InstanceRequest<SimpleFooProxy, Void> persist(); @@ -92,5 +94,7 @@ Request<String> returnNullString(); + Request<SimpleValueProxy> returnValueProxy(); + InstanceRequest<SimpleFooProxy, Integer> sum(List<Integer> values); }
diff --git a/user/test/com/google/gwt/requestfactory/shared/SimpleValueProxy.java b/user/test/com/google/gwt/requestfactory/shared/SimpleValueProxy.java new file mode 100644 index 0000000..67b6d43 --- /dev/null +++ b/user/test/com/google/gwt/requestfactory/shared/SimpleValueProxy.java
@@ -0,0 +1,51 @@ +/* + * 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.gwt.requestfactory.shared; + +import com.google.gwt.requestfactory.server.SimpleValue; + +import java.util.Date; +import java.util.List; + +/** + * A proxy for a non-addressable value object. + */ +@ProxyFor(value = SimpleValue.class) +public interface SimpleValueProxy extends ValueProxy { + Date getDate(); + + int getNumber(); + + String getShouldBeNull(); + + SimpleFooProxy getSimpleFoo(); + + List<SimpleValueProxy> getSimpleValue(); + + String getString(); + + void setDate(Date value); + + void setNumber(int value); + + void setShouldBeNull(String value); + + void setSimpleFoo(SimpleFooProxy value); + + void setSimpleValue(List<SimpleValueProxy> value); + + void setString(String value); +}