Add ProxySerializer API to provide EntityProxy persistence primitives. Issue 5523. http://code.google.com/p/google-web-toolkit/wiki/RequestFactory_2_1_1 Patch by: bobv Review by: rchandia, cramsdale Review at http://gwt-code-reviews.appspot.com/1148801 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9294 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 e8aa4bb..c7d227f 100644 --- a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java +++ b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
@@ -57,6 +57,14 @@ return Collections.emptyList(); } + public boolean isIndexed() { + return false; + } + + public boolean isKeyed() { + return false; + } + public boolean isNull(int index) { throw new UnsupportedOperationException(); } @@ -65,6 +73,10 @@ throw new UnsupportedOperationException(); } + public boolean isString() { + return true; + } + public int size() { return 0; } @@ -107,6 +119,14 @@ return Collections.unmodifiableList(toReturn); } + public native boolean isIndexed() /*-{ + return this instanceof Array; + }-*/; + + public boolean isKeyed() { + return !isString() && !isIndexed(); + } + public native boolean isNull(int index) /*-{ return this[index] == null; }-*/; @@ -115,6 +135,10 @@ return this[key] == null; }-*/; + public native boolean isString() /*-{ + return typeof(this) == 'string' || this instanceof String; + }-*/; + public native int size() /*-{ return this.length; }-*/;
diff --git a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java index b331ad5..9f0dc3f 100644 --- a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java +++ b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java
@@ -15,8 +15,6 @@ */ package com.google.gwt.autobean.server; -import com.google.gwt.autobean.shared.AutoBeanCodex; - import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -55,6 +53,6 @@ */ @Override public String toString() { - return AutoBeanCodex.encode(bean).getPayload(); + return bean.getPropertyMap().toString(); } } \ 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 6550f82..6dd80e3 100644 --- a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java +++ b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
@@ -62,7 +62,6 @@ private final JSONArray array; private final JSONObject obj; - private final String string; private JsonSplittable(JSONArray array) { @@ -125,6 +124,14 @@ } } + public boolean isIndexed() { + return array != null; + } + + public boolean isKeyed() { + return obj != null; + } + public boolean isNull(int index) { return array.isNull(index); } @@ -134,6 +141,10 @@ return !obj.has(key) || obj.isNull(key); } + public boolean isString() { + return string != null; + } + public int size() { return array.length(); }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java index 129892d..ed1930f 100644 --- a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java +++ b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
@@ -17,22 +17,99 @@ import com.google.gwt.core.client.impl.WeakMapping; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** * Utility methods for working with AutoBeans. */ public final class AutoBeanUtils { + /* + * TODO(bobv): Make Comparison a real type that holds a map contain the diff + * between the two objects. Then export a Map of PendingComparison to + * Comparisons as a public API to make it easy for developers to perform deep + * diffs across a graph structure. + * + * Three-way merge... + */ + + private enum Comparison { + TRUE, FALSE, PENDING; + } /** - * Returns a map of properties that differ between two AutoBeans. The keys are - * property names and the values are the value of the property in - * <code>b</code>. Properties present in <code>a</code> but missing in - * <code>b</code> will be represented by <code>null</code> values. This - * implementation will compare AutoBeans of different parameterizations, - * although the diff produced is likely meaningless. + * A Pair where order does not matter and the objects are compared by + * identity. + */ + private static class PendingComparison { + private final AutoBean<?> a; + private final AutoBean<?> b; + private final int hashCode; + + public PendingComparison(AutoBean<?> a, AutoBean<?> b) { + this.a = a; + this.b = b; + // Don't make relatively prime since order does not matter + hashCode = System.identityHashCode(a) + System.identityHashCode(b); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PendingComparison)) { + return false; + } + PendingComparison other = (PendingComparison) o; + return a == other.a && b == other.b || // Direct match + a == other.b && b == other.a; // Swapped + } + + @Override + public int hashCode() { + return hashCode; + } + } + + /** + * Compare two graphs of AutoBeans based on values. + * <p> + * <ul> + * <li>AutoBeans are compared based on type and property values</li> + * <li>Lists are compared with element-order equality</li> + * <li>Sets and all other Collection types are compare with bag equality</li> + * <li>Maps are compared as a lists of keys-value pairs</li> + * <li>{@link Splittable Splittables} are compared by value</li> + * </ul> + * <p> + * This will work for both simple and wrapper AutoBeans. + * <p> + * This method may crawl the entire object graph reachable from the input + * parameters and may be arbitrarily expensive to compute. + * + * @param a an {@link AutoBean} + * @param b an {@link AutoBean} + * @return {@code false} if any values in the graph reachable through + * <code>a</code> are different from those reachable from + * <code>b</code> + */ + public static boolean deepEquals(AutoBean<?> a, AutoBean<?> b) { + return sameOrEquals(a, b, new HashMap<PendingComparison, Comparison>()); + } + + /** + * Returns a map of properties that differ (via {@link Object#equals(Object)}) + * between two AutoBeans. The keys are property names and the values are the + * value of the property in <code>b</code>. Properties present in + * <code>a</code> but missing in <code>b</code> will be represented by + * <code>null</code> values. This implementation will compare AutoBeans of + * different parameterizations, although the diff produced is likely + * meaningless. * <p> * This will work for both simple and wrapper AutoBeans. * @@ -137,6 +214,251 @@ } /** + * Compare two AutoBeans, this method has the type fan-out. + */ + static boolean sameOrEquals(Object value, Object otherValue, + Map<PendingComparison, Comparison> pending) { + if (value == otherValue) { + // Fast exit + return true; + } + + if (value instanceof Collection<?> && otherValue instanceof Collection<?>) { + // Check collections + return sameOrEquals((Collection<?>) value, (Collection<?>) otherValue, + pending, null); + } + + if (value instanceof Map<?, ?> && otherValue instanceof Map<?, ?>) { + // Check maps + return sameOrEquals((Map<?, ?>) value, (Map<?, ?>) otherValue, pending); + } + + if (value instanceof Splittable && otherValue instanceof Splittable) { + return sameOrEquals((Splittable) value, (Splittable) otherValue, pending); + } + + // Possibly substitute the AutoBean for its shim + { + AutoBean<?> maybeValue = AutoBeanUtils.getAutoBean(value); + AutoBean<?> maybeOther = AutoBeanUtils.getAutoBean(otherValue); + if (maybeValue != null && maybeOther != null) { + value = maybeValue; + otherValue = maybeOther; + } + } + + if (value instanceof AutoBean<?> && otherValue instanceof AutoBean<?>) { + // Check ValueProxies + return sameOrEquals((AutoBean<?>) value, (AutoBean<?>) otherValue, + pending); + } + + if (value == null ^ otherValue == null) { + // One is null, the other isn't + return false; + } + + if (value != null && !value.equals(otherValue)) { + // Regular object equality + return false; + } + return true; + } + + /** + * If a comparison between two AutoBeans is currently pending, this method + * will skip their comparison. + */ + private static boolean sameOrEquals(AutoBean<?> value, + AutoBean<?> otherValue, Map<PendingComparison, Comparison> pending) { + if (value == otherValue) { + // Simple case + return true; + } else if (!value.getType().equals(otherValue.getType())) { + // Beans of different types + return false; + } + + /* + * The PendingComparison key allows us to break reference cycles when + * crawling the graph. Since the entire operation is essentially a + * concatenated && operation, it's ok to speculatively return true for + * repeated a.equals(b) tests. + */ + PendingComparison key = new PendingComparison(value, otherValue); + Comparison previous = pending.get(key); + if (previous == null) { + // Prevent the same comparison from being made + pending.put(key, Comparison.PENDING); + + // Compare each property + Map<String, Object> beanProperties = AutoBeanUtils.getAllProperties(value); + Map<String, Object> otherProperties = AutoBeanUtils.getAllProperties(otherValue); + for (Map.Entry<String, Object> entry : beanProperties.entrySet()) { + Object property = entry.getValue(); + Object otherProperty = otherProperties.get(entry.getKey()); + if (!sameOrEquals(property, otherProperty, pending)) { + pending.put(key, Comparison.FALSE); + return false; + } + } + pending.put(key, Comparison.TRUE); + return true; + } else { + // Return true for TRUE or PENDING + return !Comparison.FALSE.equals(previous); + } + } + + /** + * Compare two collections by size, then by contents. List comparisons will + * preserve order. All other collections will be treated with bag semantics. + */ + private static boolean sameOrEquals(Collection<?> collection, + Collection<?> otherCollection, + Map<PendingComparison, Comparison> pending, Map<Object, Object> pairs) { + if (collection.size() != otherCollection.size() + || !collection.getClass().equals(otherCollection.getClass())) { + return false; + } + + if (collection instanceof List<?>) { + // Lists we can simply iterate over + Iterator<?> it = collection.iterator(); + Iterator<?> otherIt = otherCollection.iterator(); + while (it.hasNext()) { + assert otherIt.hasNext(); + Object element = it.next(); + Object otherElement = otherIt.next(); + if (!sameOrEquals(element, otherElement, pending)) { + return false; + } + if (pairs != null) { + pairs.put(element, otherElement); + } + } + } else { + // Do an n*m comparison on any other collection type + List<Object> values = new ArrayList<Object>(collection); + List<Object> otherValues = new ArrayList<Object>(otherCollection); + it : for (Iterator<Object> it = values.iterator(); it.hasNext();) { + Object value = it.next(); + for (Iterator<Object> otherIt = otherValues.iterator(); otherIt.hasNext();) { + Object otherValue = otherIt.next(); + if (sameOrEquals(value, otherValue, pending)) { + if (pairs != null) { + pairs.put(value, otherValue); + } + // If a match is found, remove both values from their lists + it.remove(); + otherIt.remove(); + continue it; + } + } + // A match for the value wasn't found + return false; + } + assert values.isEmpty() && otherValues.isEmpty(); + } + return true; + } + + /** + * Compare two Maps by size, and key-value pairs. + */ + private static boolean sameOrEquals(Map<?, ?> map, Map<?, ?> otherMap, + Map<PendingComparison, Comparison> pending) { + if (map.size() != otherMap.size()) { + return false; + } + Map<Object, Object> pairs = new IdentityHashMap<Object, Object>(); + if (!sameOrEquals(map.keySet(), otherMap.keySet(), pending, pairs)) { + return false; + } + for (Map.Entry<?, ?> entry : map.entrySet()) { + Object otherValue = otherMap.get(pairs.get(entry.getKey())); + if (!sameOrEquals(entry.getValue(), otherValue, pending)) { + return false; + } + } + return true; + } + + /** + * Compare Splittables by kind and values. + */ + private static boolean sameOrEquals(Splittable value, Splittable otherValue, + Map<PendingComparison, Comparison> pending) { + if (value == otherValue) { + return true; + } + + // Strings + if (value.isString()) { + if (!otherValue.isString()) { + return false; + } + return value.asString().equals(otherValue.asString()); + } + + // Arrays + if (value.isIndexed()) { + if (!otherValue.isIndexed()) { + return false; + } + + if (value.size() != otherValue.size()) { + return false; + } + + for (int i = 0, j = value.size(); i < j; i++) { + if (!sameOrEquals(value.get(i), otherValue.get(i), pending)) { + return false; + } + } + return true; + } + + // Objects + if (value.isKeyed()) { + if (!otherValue.isKeyed()) { + return false; + } + /* + * We want to treat a missing property key as a null value, so we can't + * just compare the key lists. + */ + List<String> keys = value.getPropertyKeys(); + for (String key : keys) { + if (value.isNull(key)) { + // If value['foo'] is null, other['foo'] must also be null + if (!otherValue.isNull(key)) { + return false; + } + } else if (otherValue.isNull(key) + || !sameOrEquals(value.get(key), otherValue.get(key), pending)) { + return false; + } + } + + // Look at keys only in otherValue, and ensure nullness + List<String> otherKeys = new ArrayList<String>( + otherValue.getPropertyKeys()); + otherKeys.removeAll(keys); + for (String key : otherKeys) { + if (!value.isNull(key)) { + return false; + } + } + return true; + } + + // Unexpected + throw new UnsupportedOperationException("Splittable of unknown type"); + } + + /** * Utility class. */ private AutoBeanUtils() {
diff --git a/user/src/com/google/gwt/autobean/shared/Splittable.java b/user/src/com/google/gwt/autobean/shared/Splittable.java index 0059d3c..65a2286 100644 --- a/user/src/com/google/gwt/autobean/shared/Splittable.java +++ b/user/src/com/google/gwt/autobean/shared/Splittable.java
@@ -50,6 +50,18 @@ List<String> getPropertyKeys(); /** + * Returns {@code} true if {@link #size()} and {@link #get(int)} can be + * expected to return meaningful values. + */ + boolean isIndexed(); + + /** + * Returns {@code} true if {@link #getPropertyKeys()} and {@link #get(String)} + * can be expected to return meaningful values. + */ + boolean isKeyed(); + + /** * Indicates if the nth element of a list is null. */ boolean isNull(int index); @@ -60,6 +72,12 @@ boolean isNull(String key); /** + * Returns {@code} true if {@link #asString()} can be expected to return a + * meaningful value. + */ + boolean isString(); + + /** * Returns the size of the list. */ int size();
diff --git a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java index 3e804d1..2fb4749 100644 --- a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java +++ b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
@@ -57,6 +57,14 @@ return split.getPropertyKeys(); } + public boolean isIndexed() { + return payload.charAt(0) == '['; + } + + public boolean isKeyed() { + return payload.charAt(0) == '{'; + } + public boolean isNull(int index) { maybeSplit(); return split.isNull(index); @@ -67,6 +75,10 @@ return split.isNull(key); } + public boolean isString() { + return payload.charAt(0) == '\"'; + } + public int size() { maybeSplit(); return split.size();
diff --git a/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java b/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java new file mode 100644 index 0000000..26d9eef --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java
@@ -0,0 +1,89 @@ +/* + * 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.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanCodex; +import com.google.gwt.autobean.shared.Splittable; +import com.google.gwt.requestfactory.shared.impl.MessageFactoryHolder; +import com.google.gwt.requestfactory.shared.messages.OperationMessage; + +import java.util.HashMap; +import java.util.Map; + +/** + * An in-memory ProxyStore store that can encode its state as a JSON object + * literal. + */ +public class DefaultProxyStore implements ProxyStore { + /** + * Provide a little bit of future-proofing to at least detect an encoded + * payload that can't be decoded. + */ + private static final String EXPECTED_VERSION = "211"; + private final AutoBean<OperationMessage> messageBean; + private final Map<String, Splittable> map; + + /** + * Construct an empty DefaultProxyStore. + */ + public DefaultProxyStore() { + messageBean = MessageFactoryHolder.FACTORY.operation(); + map = new HashMap<String, Splittable>(); + + OperationMessage message = messageBean.as(); + message.setPropertyMap(map); + message.setVersion(EXPECTED_VERSION); + } + + /** + * Construct a DefaultProxyStore using the a value returned from + * {@link #encode()}. + * + * @param payload a String previously returned from {@link #encode()} + * @throws IllegalArgumentException if the payload cannot be parsed + */ + public DefaultProxyStore(String payload) throws IllegalArgumentException { + messageBean = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, + OperationMessage.class, payload); + + OperationMessage message = messageBean.as(); + if (!EXPECTED_VERSION.equals(message.getVersion())) { + throw new IllegalArgumentException( + "Unexpected version string in payload " + message.getVersion()); + } + map = message.getPropertyMap(); + } + + /** + * Return a JSON object literal with the contents of the store. + */ + public String encode() { + return AutoBeanCodex.encode(messageBean).getPayload(); + } + + public Splittable get(String key) { + return map.get(key); + } + + public int nextId() { + return map.size(); + } + + public void put(String key, Splittable value) { + map.put(key, value); + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java b/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java new file mode 100644 index 0000000..3cfcafd --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java
@@ -0,0 +1,87 @@ +/* + * 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; + +/** + * Serializes graphs of EntityProxy objects. A ProxySerializer is associated + * with an instance of a {@link ProxyStore} when it is created via + * {@link RequestFactory#getSerializer(ProxyStore)}. + * <p> + * The {@link EntityProxy#stableId()} of non-persisted (i.e. newly + * {@link RequestContext#create(Class) created}) {@link EntityProxy} instances + * are not stable. + * <p> + * To create a self-contained message that encapsulates a proxy: + * + * <pre> + * RequestFactory myFactory = ...; + * MyFooProxy someProxy = ...; + * + * DefaultProxyStore store = new DefaultProxyStore(); + * ProxySerializer ser = myFactory.getSerializer(store); + * // More than one proxy could be serialized + * String key = ser.serialize(someProxy); + * // Create the flattened representation + * String payload = store.encode(); + * </pre> + * + * To recreate the object: + * + * <pre> + * ProxyStore store = new DefaultProxyStore(payload); + * ProxySerializer ser = myFactory.getSerializer(store); + * MyFooProxy someProxy = ser.deserialize(MyFooProxy.class, key); + * </pre> + * + * If two objects refer to different EntityProxy instances that have the same + * stableId(), the last mutable proxy encountered will be preferred, otherwise + * the first immutable proxy will be used. + * + * @see DefaultProxyStore + */ +public interface ProxySerializer { + /** + * Recreate a proxy instance that was previously passed to + * {@link #serialize(BaseProxy)}. + * + * @param <T> the type of proxy object to create + * @param proxyType the type of proxy object to create + * @param key a value previously returned from {@link #serialize(BaseProxy)} + * @return a new, immutable instance of the proxy or {@code null} if the data + * needed to deserialize the proxy is not present in the ProxyStore + */ + <T extends BaseProxy> T deserialize(Class<T> proxyType, String key); + + /** + * Recreate a {@link EntityProxy} instance that was previously passed to + * {@link #serialize(BaseProxy)}. + * + * @param <T> the type of proxy object to create + * @param id the {@link EntityProxyId} of the desired entity + * @return a new, immutable instance of the proxy or {@code null} if the data + * needed to deserialize the proxy is not present in the ProxyStore + */ + <T extends EntityProxy> T deserialize(EntityProxyId<T> id); + + /** + * Store a proxy into the backing store. + * + * @param proxy the proxy to store + * @return a key value that can be passed to + * {@link #deserialize(Class, String)} + */ + String serialize(BaseProxy proxy); +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java b/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java new file mode 100644 index 0000000..412b070 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java
@@ -0,0 +1,53 @@ +/* + * 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.autobean.shared.Splittable; + +/** + * A ProxyStore provides a {@link ProxySerializer} with access to a low-level + * persistence mechanism. The ProxyStore does not need to be able to interpret + * the data sent to it by the ProxySerializer; it is merely a wrapper around a + * persistence mechanism. + * + * @see DefaultProxyStore + */ +public interface ProxyStore { + /** + * Called by {@link ProxySerializer} to retrieve a value previously provided + * to {@link #put(String, Splittable)}. + * + * @param key the key + * @return the associated value or {@code null} if {@code key} is unknown + */ + Splittable get(String key); + + /** + * Returns a non-negative sequence number. The actual sequence of values + * returned by this method is unimportant, as long as the numbers in the + * sequence are unique. + */ + int nextId(); + + /** + * Called by {@link ProxySerializer} to store a value. + * + * @param key a key value that will be passed to {@link #get(String)} + * @param value the data to store + * @see Splittable#getPayload() + */ + void put(String key, Splittable value); +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java index 8e5778c..16d116c 100644 --- a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
@@ -114,6 +114,17 @@ <T extends EntityProxy> EntityProxyId<T> getProxyId(String historyToken); /** + * Returns a ProxySerializer that can encode and decode the various + * EntityProxy and ValueProxy types reachable from the RequestFactory. + * + * @param store a helper object for the ProxySerializer to provide low-level + * storage access + * @return a new ProxySerializer + * @see DefaultProxyStore + */ + ProxySerializer getSerializer(ProxyStore store); + + /** * Start this request factory with a * {@link com.google.gwt.requestfactory.client.DefaultRequestTransport}. *
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 43c766e..2a45831 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -123,7 +123,7 @@ * Objects are placed into this map by being passed into {@link #edit} or as * an invocation argument. */ - private final Map<SimpleProxyId<?>, AutoBean<?>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<?>>(); + private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>(); /** * A map that contains the canonical instance of an entity to return in the * return graph, since this is built from scratch. @@ -300,6 +300,176 @@ } /** + * Resolves an IdMessage into an SimpleProxyId. + */ + SimpleProxyId<BaseProxy> getId(IdMessage op) { + if (Strength.SYNTHETIC.equals(op.getStrength())) { + return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId()); + } + return requestFactory.getId(op.getTypeToken(), op.getServerId(), + op.getClientId()); + } + + /** + * Creates or retrieves a new canonical AutoBean to represent the given id in + * the returned payload. + */ + <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph( + SimpleProxyId<Q> id) { + @SuppressWarnings("unchecked") + AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id); + if (bean == null) { + Class<Q> proxyClass = id.getProxyClass(); + bean = requestFactory.createProxy(proxyClass, id); + returnedProxies.put(id, bean); + } + + return bean; + } + + /** + * Create a single OperationMessage that encapsulates the state of a proxy + * AutoBean. + */ + AutoBean<OperationMessage> makeOperationMessage( + SimpleProxyId<BaseProxy> stableId, AutoBean<?> proxyBean, boolean useDelta) { + + // The OperationMessages describes operations on exactly one entity + AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation(); + OperationMessage operation = toReturn.as(); + operation.setTypeToken(requestFactory.getTypeToken(stableId.getProxyClass())); + + // Find the object to compare against + AutoBean<?> parent; + if (stableId.isEphemeral()) { + // Newly-created object, use a blank object to compare against + parent = 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()); + operation.setStrength(Strength.EPHEMERAL); + } else if (stableId.isSynthetic()) { + // Newly-created object, use a blank object to compare against + 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.setSyntheticId(stableId.getSyntheticId()); + operation.setStrength(Strength.SYNTHETIC); + } else { + parent = proxyBean.getTag(PARENT_OBJECT); + // Requests involving existing objects use the persisted id + operation.setServerId(stableId.getServerId()); + operation.setOperation(WriteOperation.UPDATE); + } + assert !useDelta || parent != null; + + // Send our version number to the server to cut down on future payloads + String version = proxyBean.getTag(Constants.VERSION_PROPERTY_B64); + if (version != null) { + operation.setVersion(version); + } + + Map<String, Object> diff = Collections.emptyMap(); + if (isEntityType(stableId.getProxyClass())) { + // Compute what's changed on the client + diff = useDelta ? AutoBeanUtils.diff(parent, proxyBean) + : AutoBeanUtils.getAllProperties(proxyBean); + } else if (isValueType(stableId.getProxyClass())) { + // Send everything + diff = AutoBeanUtils.getAllProperties(proxyBean); + } + + if (!diff.isEmpty()) { + Map<String, Splittable> propertyMap = new HashMap<String, Splittable>(); + for (Map.Entry<String, Object> entry : diff.entrySet()) { + propertyMap.put(entry.getKey(), + EntityCodex.encode(this, entry.getValue())); + } + operation.setPropertyMap(propertyMap); + } + return toReturn; + } + + /** + * Create a new EntityProxy from a snapshot in the return payload. + * + * @param id the EntityProxyId of the object + * @param returnRecord the JSON map containing property/value pairs + * @param operations the WriteOperation eventns to broadcast over the EventBus + */ + <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, + OperationMessage op, WriteOperation... operations) { + + AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id); + toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion()); + + final Map<String, Splittable> properties = op.getPropertyMap(); + if (properties != null) { + // Apply updates + toMutate.accept(new AutoBeanVisitor() { + @Override + public boolean visitReferenceProperty(String propertyName, + AutoBean<?> value, PropertyContext ctx) { + if (ctx.canSet()) { + if (properties.containsKey(propertyName)) { + Splittable raw = properties.get(propertyName); + Class<?> elementType = ctx instanceof CollectionPropertyContext + ? ((CollectionPropertyContext) ctx).getElementType() : null; + Object decoded = EntityCodex.decode(AbstractRequestContext.this, + ctx.getType(), elementType, raw); + ctx.set(decoded); + } + } + return false; + } + + @Override + public boolean visitValueProperty(String propertyName, Object value, + PropertyContext ctx) { + if (ctx.canSet()) { + if (properties.containsKey(propertyName)) { + Splittable raw = properties.get(propertyName); + Object decoded = ValueCodex.decode(ctx.getType(), raw); + // Hack for Date, consider generalizing for "custom serializers" + if (decoded != null && Date.class.equals(ctx.getType())) { + decoded = new DatePoser((Date) decoded); + } + ctx.set(decoded); + } + } + return false; + } + }); + } + + // Finished applying updates, freeze the bean + makeImmutable(toMutate); + Q proxy = toMutate.as(); + + /* + * Notify subscribers if the object differs from when it first came into the + * RequestContext. + */ + if (operations != null && requestFactory.isEntityType(id.getProxyClass())) { + for (WriteOperation writeOperation : operations) { + if (writeOperation.equals(WriteOperation.UPDATE) + && !requestFactory.hasVersionChanged(id, op.getVersion())) { + // No updates if the server reports no change + continue; + } + requestFactory.getEventBus().fireEventFromSource( + new EntityProxyChange<EntityProxy>((EntityProxy) proxy, + writeOperation), id.getProxyClass()); + } + } + return proxy; + } + + /** * Get-or-create method for synthetic ids. * * @see #syntheticIds @@ -515,34 +685,6 @@ } /** - * Resolves an IdMessage into an SimpleProxyId. - */ - private SimpleProxyId<BaseProxy> getId(IdMessage op) { - if (Strength.SYNTHETIC.equals(op.getStrength())) { - return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId()); - } - return requestFactory.getId(op.getTypeToken(), op.getServerId(), - op.getClientId()); - } - - /** - * Creates or retrieves a new canonical AutoBean to represent the given id in - * the returned payload. - */ - 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.createProxy(proxyClass, id); - returnedProxies.put(id, bean); - } - - return bean; - } - - /** * Make an EntityProxy immutable. */ private void makeImmutable(final AutoBean<? extends BaseProxy> toMutate) { @@ -566,88 +708,8 @@ // Get the factory from the runtime-specific holder. MessageFactory f = MessageFactoryHolder.FACTORY; - List<OperationMessage> operations = new ArrayList<OperationMessage>(); - // Compute deltas for each entity seen by the context - for (AutoBean<?> currentView : editedProxies.values()) { - @SuppressWarnings("unchecked") - 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; - if (stableId.isEphemeral()) { - // Newly-created object, use a blank object to compare against - 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()); - operation.setStrength(Strength.EPHEMERAL); - } 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 future payloads - String version = currentView.getTag(Constants.VERSION_PROPERTY_B64); - if (version != null) { - operation.setVersion(version); - } - - Map<String, Object> diff = Collections.emptyMap(); - if (isEntityType(stableId.getProxyClass())) { - // Compute what's changed on the client - diff = 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()) { - propertyMap.put(entry.getKey(), - EntityCodex.encode(this, entry.getValue())); - } - operation.setPropertyMap(propertyMap); - } - operations.add(operation); - } - - List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>(); - for (AbstractRequest<?> invocation : invocations) { - RequestData data = invocation.getRequestData(); - InvocationMessage message = f.invocation().as(); - - String opsToSend = data.getOperation(); - if (!opsToSend.isEmpty()) { - message.setOperation(opsToSend); - } - - Set<String> refsToSend = data.getPropertyRefs(); - if (!refsToSend.isEmpty()) { - message.setPropertyRefs(refsToSend); - } - - List<Splittable> parameters = new ArrayList<Splittable>( - data.getParameters().length); - for (Object param : data.getParameters()) { - parameters.add(EntityCodex.encode(this, param)); - } - if (!parameters.isEmpty()) { - message.setParameters(parameters); - } - - invocationMessages.add(message); - } + List<OperationMessage> operations = makePayloadOperations(); + List<InvocationMessage> invocationMessages = makePayloadInvocations(); // Create the outer envelope message AutoBean<RequestMessage> bean = f.request(); @@ -662,78 +724,53 @@ } /** - * Create a new EntityProxy from a snapshot in the return payload. - * - * @param id the EntityProxyId of the object - * @param returnRecord the JSON map containing property/value pairs - * @param operations the WriteOperation eventns to broadcast over the EventBus + * Create an InvocationMessage for each remote method call being made by the + * context. */ - private <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, - OperationMessage op, WriteOperation... operations) { + private List<InvocationMessage> makePayloadInvocations() { + MessageFactory f = MessageFactoryHolder.FACTORY; - AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id); - toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion()); + List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>(); + for (AbstractRequest<?> invocation : invocations) { + // RequestData is produced by the generated subclass + RequestData data = invocation.getRequestData(); + InvocationMessage message = f.invocation().as(); - final Map<String, Splittable> properties = op.getPropertyMap(); - if (properties != null) { - // Apply updates - toMutate.accept(new AutoBeanVisitor() { - @Override - public boolean visitReferenceProperty(String propertyName, - AutoBean<?> value, PropertyContext ctx) { - if (ctx.canSet()) { - if (properties.containsKey(propertyName)) { - Splittable raw = properties.get(propertyName); - Class<?> elementType = ctx instanceof CollectionPropertyContext - ? ((CollectionPropertyContext) ctx).getElementType() : null; - Object decoded = EntityCodex.decode(AbstractRequestContext.this, - ctx.getType(), elementType, raw); - ctx.set(decoded); - } - } - return false; - } + // Operation; essentially a method descriptor + message.setOperation(data.getOperation()); - @Override - public boolean visitValueProperty(String propertyName, Object value, - PropertyContext ctx) { - if (ctx.canSet()) { - if (properties.containsKey(propertyName)) { - Splittable raw = properties.get(propertyName); - Object decoded = ValueCodex.decode(ctx.getType(), raw); - // Hack for Date, consider generalizing for "custom serializers" - if (Date.class.equals(ctx.getType())) { - decoded = new DatePoser((Date) decoded); - } - ctx.set(decoded); - } - } - return false; - } - }); - } - - // Finished applying updates, freeze the bean - makeImmutable(toMutate); - Q proxy = toMutate.as(); - - /* - * Notify subscribers if the object differs from when it first came into the - * RequestContext. - */ - if (operations != null && requestFactory.isEntityType(id.getProxyClass())) { - for (WriteOperation writeOperation : operations) { - if (writeOperation.equals(WriteOperation.UPDATE) - && !requestFactory.hasVersionChanged(id, op.getVersion())) { - // No updates if the server reports no change - continue; - } - requestFactory.getEventBus().fireEventFromSource( - new EntityProxyChange<EntityProxy>((EntityProxy) proxy, - writeOperation), id.getProxyClass()); + // The arguments to the with() calls + Set<String> refsToSend = data.getPropertyRefs(); + if (!refsToSend.isEmpty()) { + message.setPropertyRefs(refsToSend); } + + // Parameter values or references + List<Splittable> parameters = new ArrayList<Splittable>( + data.getParameters().length); + for (Object param : data.getParameters()) { + parameters.add(EntityCodex.encode(this, param)); + } + if (!parameters.isEmpty()) { + message.setParameters(parameters); + } + + invocationMessages.add(message); } - return proxy; + return invocationMessages; + } + + /** + * Compute deltas for each entity seen by the context. + */ + private List<OperationMessage> makePayloadOperations() { + List<OperationMessage> operations = new ArrayList<OperationMessage>(); + for (AutoBean<? extends BaseProxy> currentView : editedProxies.values()) { + OperationMessage operation = makeOperationMessage( + BaseProxyCategory.stableId(currentView), currentView, true).as(); + operations.add(operation); + } + return operations; } /**
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 b9c95ce..b5ffeda 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -23,6 +23,8 @@ 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.ProxySerializer; +import com.google.gwt.requestfactory.shared.ProxyStore; import com.google.gwt.requestfactory.shared.Request; import com.google.gwt.requestfactory.shared.RequestFactory; import com.google.gwt.requestfactory.shared.RequestTransport; @@ -114,6 +116,10 @@ return transport; } + public ProxySerializer getSerializer(ProxyStore store) { + return new ProxySerializerImpl(this, store); + } + /** * The choice of a default request transport is runtime-specific. */
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 0c16cae..aa3254d 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
@@ -49,6 +49,7 @@ */ public <P extends BaseProxy> SimpleProxyId<P> allocateSyntheticId( Class<P> clazz, int syntheticId) { + assert syntheticId > 0; SimpleProxyId<P> toReturn = createId(clazz, "%" + syntheticId); toReturn.setSyntheticId(syntheticId); return toReturn;
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java new file mode 100644 index 0000000..73b9c55 --- /dev/null +++ b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -0,0 +1,234 @@ +/* + * 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.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.requestfactory.shared.BaseProxy; +import com.google.gwt.requestfactory.shared.EntityProxy; +import com.google.gwt.requestfactory.shared.EntityProxyId; +import com.google.gwt.requestfactory.shared.ProxySerializer; +import com.google.gwt.requestfactory.shared.ProxyStore; +import com.google.gwt.requestfactory.shared.messages.IdMessage; +import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength; +import com.google.gwt.requestfactory.shared.messages.OperationMessage; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * The default implementation of ProxySerializer. + */ +class ProxySerializerImpl extends AbstractRequestContext implements + ProxySerializer { + + /** + * Used internally to unwind the stack if data cannot be found in the backing + * store. + */ + private static class NoDataException extends RuntimeException { + } + + private final ProxyStore store; + /** + * If the user wants to serialize a proxy with a non-persistent id (including + * ValueProxy), we'll assign a synthetic id that is local to the store being + * used. + */ + private final Map<SimpleProxyId<?>, SimpleProxyId<?>> syntheticIds = new HashMap<SimpleProxyId<?>, SimpleProxyId<?>>(); + + /** + * The ids of proxies whose content has been reloaded. + */ + private final Set<SimpleProxyId<?>> restored = new HashSet<SimpleProxyId<?>>(); + private final Map<SimpleProxyId<?>, AutoBean<?>> serialized = new HashMap<SimpleProxyId<?>, AutoBean<?>>(); + + public ProxySerializerImpl(AbstractRequestFactory factory, ProxyStore store) { + super(factory); + this.store = store; + } + + public <T extends BaseProxy> T deserialize(Class<T> proxyType, String key) { + // Fast exit to prevent getOperation from throwing an exception + if (store.get(key) == null) { + return null; + } + OperationMessage op = getOperation(proxyType, key); + @SuppressWarnings("unchecked") + SimpleProxyId<T> id = (SimpleProxyId<T>) getId(op); + return doDeserialize(id); + } + + public <T extends EntityProxy> T deserialize(EntityProxyId<T> id) { + return doDeserialize((SimpleEntityProxyId<T>) id); + } + + /** + * Replace non-persistent ids with store-local ids. + */ + @Override + public Splittable getSerializedProxyId(SimpleProxyId<?> stableId) { + return super.getSerializedProxyId(serializedId(stableId)); + } + + public String serialize(BaseProxy rootObject) { + final AutoBean<? extends BaseProxy> root = AutoBeanUtils.getAutoBean(rootObject); + if (root == null) { + // Unexpected, some kind of foreign implementation of the BaseProxy? + throw new IllegalArgumentException(); + } + + final SimpleProxyId<?> id = serializedId(BaseProxyCategory.stableId(root)); + // Only persistent and synthetic ids expected + assert !id.isEphemeral() : "Unexpected ephemeral id " + id.toString(); + + /* + * Don't repeatedly serialize the same proxy, unless we're looking at a + * mutable instance. + */ + AutoBean<?> previous = serialized.get(id); + if (previous == null || !previous.isFrozen()) { + serialized.put(id, root); + serializeOneProxy(id, root); + root.accept(new AutoBeanVisitor() { + @Override + public void endVisit(AutoBean<?> bean, Context ctx) { + // Avoid unnecessary method call + if (bean == root) { + return; + } + if (isEntityType(bean.getType()) || isValueType(bean.getType())) { + serialize((BaseProxy) bean.as()); + } + } + + @Override + public void endVisitCollectionProperty(String propertyName, + AutoBean<Collection<?>> value, CollectionPropertyContext ctx) { + if (value == null) { + return; + } + if (isEntityType(ctx.getElementType()) + || isValueType(ctx.getElementType())) { + for (Object o : value.as()) { + serialize((BaseProxy) o); + } + } + } + }); + } + + return getRequestFactory().getHistoryToken(id); + } + + @Override + SimpleProxyId<BaseProxy> getId(IdMessage op) { + if (Strength.SYNTHETIC.equals(op.getStrength())) { + return getRequestFactory().allocateSyntheticId( + getRequestFactory().getTypeFromToken(op.getTypeToken()), + op.getSyntheticId()); + } + return super.getId(op); + } + + @Override + <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph( + SimpleProxyId<Q> id) { + AutoBean<Q> toReturn = super.getProxyForReturnPayloadGraph(id); + if (restored.add(id)) { + /* + * If we haven't seen the id before, use the data in the OperationMessage + * to repopulate the properties of the canonical bean for this id. + */ + OperationMessage op = getOperation(id.getProxyClass(), + getRequestFactory().getHistoryToken(id)); + this.processReturnOperation(id, op); + toReturn.setTag(Constants.STABLE_ID, super.getId(op)); + } + return toReturn; + } + + /** + * Reset all temporary state. + */ + private void clear() { + syntheticIds.clear(); + restored.clear(); + serialized.clear(); + } + + private <T extends BaseProxy> T doDeserialize(SimpleProxyId<T> id) { + try { + return getProxyForReturnPayloadGraph(id).as(); + } catch (NoDataException e) { + return null; + } finally { + clear(); + } + } + + /** + * Load the OperationMessage containing the object state from the backing + * store. + */ + private <T> OperationMessage getOperation(Class<T> proxyType, String key) { + Splittable data = store.get(key); + if (data == null) { + throw new NoDataException(); + } + + OperationMessage op = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, + OperationMessage.class, data).as(); + return op; + } + + /** + * Convert any non-persistent ids into store-local synthetic ids. + */ + private <T extends BaseProxy> SimpleProxyId<T> serializedId( + SimpleProxyId<T> stableId) { + assert !stableId.isSynthetic(); + if (stableId.isEphemeral()) { + @SuppressWarnings("unchecked") + SimpleProxyId<T> syntheticId = (SimpleProxyId<T>) syntheticIds.get(stableId); + if (syntheticId == null) { + int nextId = store.nextId(); + assert nextId >= 0 : "ProxyStore.nextId() returned a negative number " + + nextId; + syntheticId = getRequestFactory().allocateSyntheticId( + stableId.getProxyClass(), nextId + 1); + syntheticIds.put(stableId, syntheticId); + } + return syntheticId; + } + return stableId; + } + + private void serializeOneProxy(SimpleProxyId<?> idForSerialization, + AutoBean<? extends BaseProxy> bean) { + AutoBean<OperationMessage> op = makeOperationMessage( + serializedId(BaseProxyCategory.stableId(bean)), bean, false); + + store.put(getRequestFactory().getHistoryToken(idForSerialization), + AutoBeanCodex.encode(op)); + } +}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java index b2b3422..5550921 100644 --- a/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java +++ b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java
@@ -22,7 +22,7 @@ import com.google.gwt.requestfactory.shared.ValueProxy; /** - * Contains static implementation of EntityProxy-specific methods. + * Contains static implementation of ValueProxy-specific methods. */ public class ValueProxyCategory { @@ -44,12 +44,8 @@ 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)); + // Compare the entire object graph + return AutoBeanUtils.deepEquals(bean, other); } /**
diff --git a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java index 7346bec..3f1c489 100644 --- a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java +++ b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -29,6 +29,14 @@ } public static Splittable split(String payload) { - return JsoSplittable.create(JsonUtils.safeEval(payload)); + boolean isString = payload.charAt(0) == '\"'; + if (isString) { + payload = "[" + payload + "]"; + } + Splittable toReturn = JsoSplittable.create(JsonUtils.safeEval(payload)); + if (isString) { + toReturn = toReturn.get(0); + } + return toReturn; } }
diff --git a/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java b/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java index 3e5d3ec..b3729d6 100644 --- a/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java +++ b/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
@@ -21,6 +21,6 @@ /** * This a super-source version with a client-only implementation. */ -interface MessageFactoryHolder { +public interface MessageFactoryHolder { MessageFactory FACTORY = GWT.create(MessageFactory.class); }
diff --git a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java index 54c7767..c9484a1 100644 --- a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java +++ b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
@@ -73,11 +73,11 @@ boolean getGet(); boolean hasHas(); - + void setIs(boolean value); void setGet(boolean value); - + void setHas(boolean value); } @@ -189,6 +189,7 @@ assertNotSame(i1, i2); assertFalse(i1.equals(i2)); assertEquals(i1.getInt(), i2.getInt()); + assertTrue(AutoBeanUtils.deepEquals(a1, a2)); // Cloned instances do not affect one another i1.setInt(41); @@ -204,6 +205,8 @@ o1.getIntf().setInt(42); AutoBean<OtherIntf> a2 = a1.clone(true); + assertTrue(AutoBeanUtils.deepEquals(a1, a2)); + OtherIntf o2 = a2.as(); assertNotSame(o1.getIntf(), o2.getIntf());
diff --git a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java index 5b4d0d6..027d454 100644 --- a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java +++ b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
@@ -101,9 +101,6 @@ Map<String, Simple> getSimpleMap(); - @AutoBean.PropertyName("simpleMap") - Map<String, ReachableOnlyFromParameterization> getSimpleMapAltType(); - void setComplexMap(Map<Simple, Simple> map); void setSimpleMap(Map<String, Simple> map); @@ -146,7 +143,7 @@ AutoBean<HasCycle> bean = f.hasCycle(); bean.as().setCycle(Arrays.asList(bean.as())); try { - AutoBeanCodex.encode(bean); + checkEncode(bean); fail("Should not have encoded"); } catch (UnsupportedOperationException expected) { } @@ -155,13 +152,18 @@ public void testEmptyList() { AutoBean<HasList> bean = f.hasList(); bean.as().setList(Collections.<Simple> emptyList()); - Splittable split = AutoBeanCodex.encode(bean); - AutoBean<HasList> decodedBean = AutoBeanCodex.decode(f, HasList.class, - split); + AutoBean<HasList> decodedBean = checkEncode(bean); assertNotNull(decodedBean.as().getList()); assertTrue(decodedBean.as().getList().isEmpty()); } + private <T> AutoBean<T> checkEncode(AutoBean<T> bean) { + Splittable split = AutoBeanCodex.encode(bean); + AutoBean<T> decoded = AutoBeanCodex.decode(f, bean.getType(), split); + assertTrue(AutoBeanUtils.deepEquals(bean, decoded)); + return decoded; + } + public void testEnum() { EnumMap map = (EnumMap) f; assertEquals("BAR", map.getToken(MyEnum.BAR)); @@ -184,7 +186,7 @@ // Make sure the overridden form is always used assertFalse(split.getPayload().contains("BAZ")); - AutoBean<HasEnum> decoded = AutoBeanCodex.decode(f, HasEnum.class, split); + AutoBean<HasEnum> decoded = checkEncode(bean); assertEquals(MyEnum.BAZ, decoded.as().getEnum()); assertEquals(arrayValue, decoded.as().getEnums()); assertEquals(mapValue, decoded.as().getMap()); @@ -207,8 +209,7 @@ complex.put(key, s); } - Splittable split = AutoBeanCodex.encode(bean); - AutoBean<HasMap> decoded = AutoBeanCodex.decode(f, HasMap.class, split); + AutoBean<HasMap> decoded = checkEncode(bean); map = decoded.as().getSimpleMap(); complex = decoded.as().getComplexMap(); assertEquals(5, map.size()); @@ -226,8 +227,7 @@ public void testNull() { AutoBean<Simple> bean = f.simple(); - Splittable split = AutoBeanCodex.encode(bean); - AutoBean<Simple> decodedBean = AutoBeanCodex.decode(f, Simple.class, split); + AutoBean<Simple> decodedBean = checkEncode(bean); assertNull(decodedBean.as().getString()); } @@ -237,17 +237,13 @@ simple.setInt(42); simple.setString("Hello World!"); - Splittable split = AutoBeanCodex.encode(bean); - - AutoBean<Simple> decodedBean = AutoBeanCodex.decode(f, Simple.class, split); + AutoBean<Simple> decodedBean = checkEncode(bean); assertTrue(AutoBeanUtils.diff(bean, decodedBean).isEmpty()); AutoBean<HasSimple> bean2 = f.hasSimple(); bean2.as().setSimple(simple); - split = AutoBeanCodex.encode(bean2); - AutoBean<HasSimple> decodedBean2 = AutoBeanCodex.decode(f, HasSimple.class, - split); + AutoBean<HasSimple> decodedBean2 = checkEncode(bean2); assertNotNull(decodedBean2.as().getSimple()); assertTrue(AutoBeanUtils.diff(bean, AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple())).isEmpty()); @@ -255,10 +251,8 @@ AutoBean<HasList> bean3 = f.hasList(); bean3.as().setIntList(Arrays.asList(1, 2, 3, null, 4, 5)); bean3.as().setList(Arrays.asList(simple)); - split = AutoBeanCodex.encode(bean3); - AutoBean<HasList> decodedBean3 = AutoBeanCodex.decode(f, HasList.class, - split); + AutoBean<HasList> decodedBean3 = checkEncode(bean3); assertNotNull(decodedBean3.as().getIntList()); assertEquals(Arrays.asList(1, 2, 3, null, 4, 5), decodedBean3.as().getIntList()); @@ -277,10 +271,8 @@ List<Splittable> testList = Arrays.asList(AutoBeanCodex.encode(simple), null, AutoBeanCodex.encode(simple)); bean.as().setSimpleList(testList); - Splittable split = AutoBeanCodex.encode(bean); - AutoBean<HasAutoBean> decoded = AutoBeanCodex.decode(f, HasAutoBean.class, - split); + AutoBean<HasAutoBean> decoded = checkEncode(bean); Splittable toDecode = decoded.as().getSimple(); AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class, toDecode);
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java index a52ab68..b512857 100644 --- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java +++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -44,9 +44,11 @@ * Tests for {@link com.google.gwt.requestfactory.shared.RequestFactory}. */ public class RequestFactoryTest extends RequestFactoryTestBase { - /* * DO NOT USE finishTest(). Instead, call finishTestAndReset(); + * + * When possible, pass any returned proxies to checkSerialization() and use + * the return value in the place of the returned object. */ class FooReciever extends Receiver<SimpleFooProxy> { @@ -80,6 +82,7 @@ persistRequest.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); finishTestAndReset(); } }); @@ -206,6 +209,7 @@ new Receiver<SimpleBarProxy>() { @Override public void onSuccess(SimpleBarProxy persistentBar) { + persistentBar = checkSerialization(persistentBar); // Persist foo with bar as a child. SimpleFooRequest context = req.simpleFooRequest(); SimpleFooProxy foo = context.create(SimpleFooProxy.class); @@ -217,6 +221,7 @@ persistRequest.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy persistentFoo) { + persistentFoo = checkSerialization(persistentFoo); // Handle changes to SimpleFooProxy. final SimpleFooEventHandler<SimpleFooProxy> fooHandler = new SimpleFooEventHandler<SimpleFooProxy>(); EntityProxyChange.registerForProxyType(req.getEventBus(), @@ -265,6 +270,7 @@ "selfOneToManyField").fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertNotNull(response.getFooField()); assertSame(response.getFooField(), response.getSelfOneToManyField().get(0)); @@ -297,6 +303,7 @@ request.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertFalse(((SimpleEntityProxyId<SimpleFooProxy>) response.stableId()).isEphemeral()); assertEquals(2, handler.persistEventCount); // two bars persisted. assertEquals(2, handler.updateEventCount); // two bars persisted. @@ -329,6 +336,7 @@ @Override public void onSuccess(SimpleFooProxy foo) { + foo = checkSerialization(foo); SimpleFooRequest context = simpleFooRequest(); // edit() doesn't cause a change @@ -376,6 +384,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals( "I'm here", response.getSelfOneToManyField().get(0).getFooField().getUserName()); @@ -390,6 +399,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(42, response.getIntId().intValue()); finishTestAndReset(); } @@ -411,7 +421,8 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy returned) { + public void onSuccess(SimpleFooProxy returned) { + returned = checkSerialization(returned); EntityProxyId<SimpleFooProxy> returnedId = returned.stableId(); assertEquals(futureId, returnedId); assertFalse((((SimpleEntityProxyId<?>) returnedId).isEphemeral())); @@ -435,7 +446,8 @@ fooReq.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy returned) { + public void onSuccess(SimpleBarProxy returned) { + returned = checkSerialization(returned); assertFalse(((SimpleEntityProxyId<?>) foo.stableId()).isEphemeral()); checkStableIdEquals(foo, returned); @@ -454,7 +466,8 @@ fooReq.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy returned) { + public void onSuccess(SimpleBarProxy returned) { + returned = checkSerialization(returned); assertFalse(((SimpleEntityProxyId<?>) bar.stableId()).isEphemeral()); assertFalse(((SimpleEntityProxyId<?>) returned.stableId()).isEphemeral()); @@ -496,6 +509,7 @@ contextA.findSimpleFooById(999L).fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); // The response shouldn't be associated with a RequestContext contextB.edit(response); finishTestAndReset(); @@ -509,6 +523,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(42, (int) response.getIntId()); assertEquals("GWT", response.getUserName()); assertEquals(8L, (long) response.getLongField()); @@ -526,6 +541,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(42, (int) response.getIntId()); assertEquals("GWT", response.getUserName()); assertEquals(8L, (long) response.getLongField()); @@ -542,7 +558,7 @@ simpleFooRequest().findAll().fire(new Receiver<List<SimpleFooProxy>>() { @Override public void onSuccess(List<SimpleFooProxy> responseList) { - SimpleFooProxy response = responseList.get(0); + SimpleFooProxy response = checkSerialization(responseList.get(0)); assertEquals(42, (int) response.getIntId()); assertEquals("GWT", response.getUserName()); assertEquals(8L, (long) response.getLongField()); @@ -576,6 +592,7 @@ @Override public void onSuccess(SimpleFooProxy newFoo) { + newFoo = checkSerialization(newFoo); assertEquals(1, handler.updateEventCount); assertEquals(1, handler.totalEventCount); @@ -584,6 +601,7 @@ @Override public void onSuccess(SimpleFooProxy newFoo) { + newFoo = checkSerialization(newFoo); // no events are fired second time. assertEquals(1, handler.updateEventCount); assertEquals(1, handler.totalEventCount); @@ -614,6 +632,7 @@ "selfOneToManyField.selfOneToManyField.fooField").fire( new Receiver<SimpleFooProxy>() { public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertNotNull(response.getSelfOneToManyField().get(0)); assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField()); assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField().get( @@ -639,6 +658,7 @@ public void onSuccess(List<SimpleFooProxy> response) { assertEquals(2, response.size()); for (SimpleFooProxy foo : response) { + foo = checkSerialization(foo); assertNotNull(foo.stableId()); assertEquals("FOO", foo.getBarField().getUserName()); } @@ -656,6 +676,7 @@ public void onSuccess(List<SimpleBarProxy> response) { assertEquals(2, response.size()); for (SimpleBarProxy bar : response) { + bar = checkSerialization(bar); assertNotNull(bar.stableId()); finishTestAndReset(); } @@ -678,7 +699,8 @@ Request<SimpleBarProxy> fooReq = context.persistAndReturnSelf().using(foo); fooReq.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy returned) { + public void onSuccess(SimpleBarProxy returned) { + returned = checkSerialization(returned); EntityProxyId<SimpleBarProxy> persistedId = returned.stableId(); String persistedToken = req.getHistoryToken(returned.stableId()); @@ -703,18 +725,19 @@ } }); } - + /** * Make sure our stock RF logging service keeps receiving. */ public void testLoggingService() { - String logRecordJson = new StringBuilder("{").append("\"level\": \"ALL\", ") - .append("\"loggerName\": \"logger\", ") - .append("\"msg\": \"Hi mom\", ") - .append("\"timestamp\": \"1234567890\",") - .append("\"thrown\": {}") - .append("}") - .toString(); + String logRecordJson = new StringBuilder("{") // + .append("\"level\": \"ALL\", ") // + .append("\"loggerName\": \"logger\", ") // + .append("\"msg\": \"Hi mom\", ") // + .append("\"timestamp\": \"1234567890\",") // + .append("\"thrown\": {}") // + .append("}") // + .toString(); req.loggingRequest().logMessage(logRecordJson).fire(new Receiver<Void>() { @Override @@ -741,6 +764,7 @@ @Override public void onSuccess(SimpleFooProxy newFoo) { + newFoo = checkSerialization(newFoo); assertEquals(1, handler.updateEventCount); assertEquals(1, handler.totalEventCount); SimpleFooRequest context = simpleFooRequest(); @@ -762,6 +786,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy finalFoo) { + finalFoo = checkSerialization(finalFoo); assertEquals("Ray", finalFoo.getUserName()); assertEquals(2, handler.updateEventCount); assertEquals(2, handler.totalEventCount); @@ -853,8 +878,9 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { - final Request<Void> fooReq = req.simpleFooRequest().receiveNull( - null).using(response); + response = checkSerialization(response); + Request<Void> fooReq = req.simpleFooRequest().receiveNull(null).using( + response); fooReq.fire(new Receiver<Void>() { @Override public void onSuccess(Void v) { @@ -898,6 +924,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); List<SimpleFooProxy> list = new ArrayList<SimpleFooProxy>(); list.add(response); // non-null list.add(null); // null @@ -964,6 +991,7 @@ r.persistAndReturnSelf().using(f).fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy f) { + f = checkSerialization(f); assertEquals("user name", f.getUserName()); assertEquals(Byte.valueOf((byte) 100), f.getByteField()); assertEquals(Short.valueOf((short) 12345), f.getShortField()); @@ -986,12 +1014,14 @@ simpleBarRequest().findSimpleBarById("999L").fire( new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy barProxy) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy barProxy = checkSerialization(response); // Retrieve a Foo simpleFooRequest().findSimpleFooById(999L).fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); fooProxy = context.edit(fooProxy); // Make the Foo point to the Bar @@ -1002,6 +1032,7 @@ "barField").fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy received) { + received = checkSerialization(received); // Check that Foo points to Bar assertNotNull(received.getBarField()); assertEquals(barProxy.stableId(), @@ -1019,6 +1050,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertNull(response.getBarField()); assertNull(response.getUserName()); assertNull(response.getByteField()); @@ -1048,13 +1080,15 @@ persistRequest.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy persistedBar) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy persistedBar = checkSerialization(response); // It was made, now find a foo to assign it to simpleFooRequest().findSimpleFooById(999L).fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); // Found the foo, edit it SimpleFooRequest context = simpleFooRequest(); @@ -1073,6 +1107,7 @@ // Here it is @Override public void onSuccess(SimpleFooProxy finalFooProxy) { + finalFooProxy = checkSerialization(finalFooProxy); assertEquals("Amit", finalFooProxy.getBarField().getUserName()); finishTestAndReset(); @@ -1103,6 +1138,7 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertNotNull(response.getBarField()); assertEquals(newBar.stableId(), response.getBarField().stableId()); finishTestAndReset(); @@ -1133,6 +1169,7 @@ new Receiver<SimpleBarProxy>() { @Override public void onSuccess(SimpleBarProxy response) { + response = checkSerialization(response); finalFoo.setBarField(response); fooReq.fire(new Receiver<Void>() { @Override @@ -1141,6 +1178,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy finalFooProxy) { + finalFooProxy = checkSerialization(finalFooProxy); // newFoo hasn't been persisted, so userName is the old // value. assertEquals("GWT", finalFooProxy.getUserName()); @@ -1176,10 +1214,13 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy persistedFoo) { + public void onSuccess(final SimpleFooProxy response) { + final SimpleFooProxy persistedFoo = checkSerialization(response); barReq.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy persistedBar) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy persistedBar = checkSerialization(response); + assertEquals("Ray", persistedFoo.getUserName()); SimpleFooRequest context = simpleFooRequest(); final Request<Void> fooReq2 = context.persist().using(persistedFoo); @@ -1192,6 +1233,7 @@ "barField.userName").fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy finalFooProxy) { + finalFooProxy = checkSerialization(finalFooProxy); assertEquals("Amit", finalFooProxy.getBarField().getUserName()); finishTestAndReset(); @@ -1215,11 +1257,14 @@ simpleBarRequest().findSimpleBarById("999L").fire( new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy barProxy) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy barProxy = checkSerialization(response); simpleFooRequest().findSimpleFooById(999L).with("oneToManyField").fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); + SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy).with("oneToManyField"); @@ -1231,6 +1276,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(response.getOneToManyField().size(), listCount + 1); assertContains(response.getOneToManyField(), barProxy); @@ -1255,7 +1301,8 @@ rayFoo.setFooField(rayFoo); persistRay.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy persistedRay) { + public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); finishTestAndReset(); } }); @@ -1273,7 +1320,8 @@ persistRay.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy persistedRay) { + public void onSuccess(SimpleFooProxy response) { + final SimpleFooProxy persistedRay = checkSerialization(response); SimpleBarRequest context = simpleBarRequest(); SimpleBarProxy amitBar = context.create(SimpleBarProxy.class); final Request<SimpleBarProxy> persistAmit = context.persistAndReturnSelf().using( @@ -1283,18 +1331,20 @@ persistAmit.fire(new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(SimpleBarProxy persistedAmit) { + public void onSuccess(SimpleBarProxy response) { + response = checkSerialization(response); SimpleFooRequest context = simpleFooRequest(); final Request<SimpleFooProxy> persistRelationship = context.persistAndReturnSelf().using( persistedRay).with("barField"); SimpleFooProxy newRec = context.edit(persistedRay); - newRec.setBarField(persistedAmit); + newRec.setBarField(response); persistRelationship.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(SimpleFooProxy relatedRay) { - assertEquals("Amit", relatedRay.getBarField().getUserName()); + public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); + assertEquals("Amit", response.getBarField().getUserName()); finishTestAndReset(); } }); @@ -1311,6 +1361,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy).with("selfOneToManyField"); @@ -1321,6 +1372,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(response.getSelfOneToManyField().size(), listCount + 1); assertContains(response.getSelfOneToManyField(), response); @@ -1337,6 +1389,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1345,6 +1398,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertTrue(response.getNumberListField().contains(100)); finishTestAndReset(); } @@ -1368,6 +1422,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1377,6 +1432,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); List<Integer> list = response.getNumberListField(); assertNull(list); finishTestAndReset(); @@ -1401,6 +1457,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1409,6 +1466,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertFalse(response.getNumberListField().contains(oldValue)); finishTestAndReset(); } @@ -1427,6 +1485,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1439,6 +1498,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); List<Integer> list = response.getNumberListField(); assertEquals(5, (int) list.get(0)); assertEquals(8, (int) list.get(1)); @@ -1460,6 +1520,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1471,6 +1532,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); Collections.reverse(al); assertTrue(response.getNumberListField().equals(al)); finishTestAndReset(); @@ -1490,6 +1552,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy); @@ -1498,6 +1561,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertTrue(response.getNumberListField().get(0) == 10); finishTestAndReset(); } @@ -1518,11 +1582,13 @@ context.persistAndReturnSelf().using(newBar).fire( new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy barProxy) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy barProxy = checkSerialization(response); simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy).with("oneToManySetField"); @@ -1534,6 +1600,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(listCount + 1, response.getOneToManySetField().size()); assertContains(response.getOneToManySetField(), @@ -1557,11 +1624,14 @@ simpleBarRequest().findSimpleBarById("1L").fire( new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy barProxy) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy barProxy = checkSerialization(response); + simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy).with("oneToManySetField"); @@ -1574,6 +1644,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(response.getOneToManySetField().size(), listCount); assertContains(response.getOneToManySetField(), @@ -1597,11 +1668,13 @@ simpleBarRequest().findSimpleBarById("1L").fire( new Receiver<SimpleBarProxy>() { @Override - public void onSuccess(final SimpleBarProxy barProxy) { + public void onSuccess(SimpleBarProxy response) { + final SimpleBarProxy barProxy = checkSerialization(response); simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy fooProxy) { + fooProxy = checkSerialization(fooProxy); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using( fooProxy).with("oneToManySetField"); @@ -1615,6 +1688,7 @@ updReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(listCount - 1, response.getOneToManySetField().size()); assertNotContains(response.getOneToManySetField(), @@ -1650,6 +1724,7 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); final Request<Integer> sumReq = simpleFooRequest().sum( Arrays.asList(1, 2, 3)).using(response); sumReq.fire(new Receiver<Integer>() { @@ -1755,6 +1830,7 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(2, response.getOneToManyField().size()); // Check lists of proxies returned from a mutable object are mutable @@ -1771,7 +1847,8 @@ 999L).with("selfOneToManyField"); fooReq.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy fooProxy) { + public void onSuccess(SimpleFooProxy response) { + final SimpleFooProxy fooProxy = checkSerialization(response); final Request<String> procReq = simpleFooRequest().processList( fooProxy.getSelfOneToManyField()).using(fooProxy); procReq.fire(new Receiver<String>() { @@ -1791,6 +1868,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); SimpleFooRequest context = simpleFooRequest(); SimpleBarProxy bar = context.create(SimpleBarProxy.class); Request<String> helloReq = context.hello(bar).using(response); @@ -1925,7 +2003,8 @@ fooReq.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy returned) { + public void onSuccess(SimpleFooProxy response) { + final SimpleFooProxy returned = checkSerialization(response); assertEquals(futureId, foo.getId()); assertFalse(((SimpleEntityProxyId<?>) foo.stableId()).isEphemeral()); assertEquals(futureId, newFoo.getId()); @@ -1943,10 +2022,11 @@ editRequest.fire(new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(SimpleFooProxy returnedAfterEdit) { - assertEquals(returnedAfterEdit.getId(), returned.getId()); - assertEquals("GWT power user", returnedAfterEdit.getUserName()); - checkStableIdEquals(editableFoo, returnedAfterEdit); + public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); + assertEquals(response.getId(), returned.getId()); + assertEquals("GWT power user", response.getUserName()); + checkStableIdEquals(editableFoo, response); finishTestAndReset(); } }); @@ -1958,17 +2038,18 @@ * We provide a simple UserInformation class to give GAE developers a hand, * and other developers a hint. Make sure RF doesn't break it (it relies on * server side upcasting, and a somewhat sleazey reflective lookup mechanism - * in a static method on UserInformation). + * in a static method on UserInformation). */ public void testUserInfo() { req.userInformationRequest().getCurrentUserInformation("").fire( new Receiver<UserInformationProxy>() { @Override - public void onSuccess(UserInformationProxy getResponse) { - assertEquals("Dummy Email", getResponse.getEmail()); - assertEquals("Dummy User", getResponse.getName()); - assertEquals("", getResponse.getLoginUrl()); - assertEquals("", getResponse.getLogoutUrl()); + public void onSuccess(UserInformationProxy response) { + response = checkSerialization(response); + assertEquals("Dummy Email", response.getEmail()); + assertEquals("Dummy User", response.getName()); + assertEquals("", response.getLoginUrl()); + assertEquals("", response.getLogoutUrl()); } }); } @@ -1991,11 +2072,13 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + // The reconstituted object may not have the same stable id + checkStableIdEquals(simpleBar, response.getBarField()); + response = checkSerialization(response); assertEquals(0, handler.totalEventCount); checkStableIdEquals(simpleFoo, response); SimpleBarProxy responseBar = response.getBarField(); assertNotNull(responseBar); - checkStableIdEquals(simpleBar, responseBar); finishTestAndReset(); } }); @@ -2015,6 +2098,7 @@ context.echo(simpleFoo).fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(0, handler.totalEventCount); checkStableIdEquals(simpleFoo, response); finishTestAndReset(); @@ -2030,13 +2114,15 @@ req.simpleFooRequest().getUnpersistedInstance().fire( new Receiver<SimpleFooProxy>() { @Override - public void onSuccess(final SimpleFooProxy created) { + public void onSuccess(SimpleFooProxy response) { + final SimpleFooProxy created = checkSerialization(response); assertNotNull(created); assertTrue(created.getUnpersisted()); req.simpleFooRequest().echo(created).fire( new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertNotNull(response); assertEquals(created.stableId(), response.stableId()); assertTrue(response.getUnpersisted()); @@ -2063,6 +2149,7 @@ new Receiver<SimpleBarProxy>() { @Override public void onSuccess(SimpleBarProxy response) { + response = checkSerialization(response); assertEquals("A", response.getUserName()); // Mark the object as deleted SimpleBarRequest context = simpleBarRequest(); @@ -2074,6 +2161,7 @@ @Override public void onSuccess(SimpleBarProxy response) { + response = checkSerialization(response); // The last-known state should be returned assertNotNull(response); assertEquals("B", response.getUserName()); @@ -2096,6 +2184,7 @@ @Override public void onSuccess(SimpleBarProxy response) { + response = checkSerialization(response); fail(); } }); @@ -2111,6 +2200,7 @@ req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); SimpleFooRequest req = simpleFooRequest(); // Create @@ -2131,6 +2221,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); SimpleValueProxy value = response.getSimpleValue(); assertEquals(42, value.getNumber()); assertEquals("Hello world!", value.getString()); @@ -2152,6 +2243,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(43, response.getSimpleValue().getNumber()); finishTestAndReset(); } @@ -2168,6 +2260,7 @@ req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); SimpleFooRequest req = simpleFooRequest(); // Create @@ -2185,6 +2278,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); SimpleValueProxy value = response.getSimpleValues().get(0); assertEquals(42, value.getNumber()); @@ -2203,6 +2297,7 @@ new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy response) { + response = checkSerialization(response); assertEquals(43, response.getSimpleValues().get(0).getNumber()); finishTestAndReset(); @@ -2226,6 +2321,18 @@ b.setString("Hello"); checkEqualityAndHashcode(a, b); + + a.setSimpleValue(Collections.singletonList(req.create(SimpleValueProxy.class))); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + b.setSimpleValue(Collections.singletonList(req.create(SimpleValueProxy.class))); + checkEqualityAndHashcode(a, b); + + a.getSimpleValue().get(0).setNumber(55); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + b.getSimpleValue().get(0).setNumber(55); + checkEqualityAndHashcode(a, b); } public void testValueObjectViolationsOnCreate() { @@ -2260,7 +2367,8 @@ simpleFooRequest().returnValueProxy().fire( new Receiver<SimpleValueProxy>() { @Override - public void onSuccess(final SimpleValueProxy response) { + public void onSuccess(SimpleValueProxy response) { + final SimpleValueProxy original = checkSerialization(response); SimpleFooRequest req = simpleFooRequest(); final SimpleValueProxy value = req.edit(response); value.setShouldBeNull("Hello world"); @@ -2277,7 +2385,7 @@ assertEquals(1, errors.size()); Violation v = errors.iterator().next(); assertEquals(value, v.getInvalidProxy()); - assertEquals(response, v.getOriginalProxy()); + assertEquals(original, v.getOriginalProxy()); assertEquals("shouldBeNull", v.getPath()); assertNull(v.getProxyId()); finishTestAndReset(); @@ -2297,6 +2405,7 @@ new Receiver<SimpleValueProxy>() { @Override public void onSuccess(SimpleValueProxy a) { + a = checkSerialization(a); try { a.setNumber(77); fail(); @@ -2315,6 +2424,7 @@ ctx.returnValueProxy().fire(new Receiver<SimpleValueProxy>() { @Override public void onSuccess(SimpleValueProxy b) { + b = checkSerialization(b); b = simpleFooRequest().edit(b); // Now check that same value is equal across contexts b.setNumber(77); @@ -2370,6 +2480,7 @@ fooCreationRequest().fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy returned) { + returned = checkSerialization(returned); SimpleFooRequest context = simpleFooRequest(); Request<SimpleFooProxy> editRequest = context.persistAndReturnSelf().using( returned); @@ -2384,6 +2495,7 @@ fooCreationRequest().fire(new Receiver<SimpleFooProxy>() { @Override public void onSuccess(SimpleFooProxy returned) { + returned = checkSerialization(returned); SimpleFooRequest context = simpleFooRequest(); Request<Void> editRequest = context.persist().using(returned); new FailFixAndRefire<Void>(returned, context, editRequest).doVoidTest();
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java index ef88d8b..607e4a8 100644 --- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java +++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java
@@ -15,14 +15,22 @@ */ package com.google.gwt.requestfactory.client; +import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.SimpleEventBus; import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.requestfactory.shared.BaseProxy; +import com.google.gwt.requestfactory.shared.DefaultProxyStore; import com.google.gwt.requestfactory.shared.EntityProxy; import com.google.gwt.requestfactory.shared.EntityProxyChange; +import com.google.gwt.requestfactory.shared.ProxySerializer; import com.google.gwt.requestfactory.shared.Receiver; import com.google.gwt.requestfactory.shared.SimpleRequestFactory; +import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory; +import com.google.gwt.requestfactory.shared.impl.Constants; +import com.google.gwt.requestfactory.shared.impl.SimpleProxyId; /** * A base class for anything that makes use of the SimpleRequestFactory. @@ -77,6 +85,44 @@ assertEquals(b, a); } + /** + * Run the given proxy through a ProxySerializer and verify that the + * before-and-after values match. + */ + protected <T extends BaseProxy> T checkSerialization(T proxy) { + AutoBean<T> originalBean = AutoBeanUtils.getAutoBean(proxy); + SimpleProxyId<T> id = BaseProxyCategory.stableId(originalBean); + DefaultProxyStore store = new DefaultProxyStore(); + ProxySerializer s = req.getSerializer(store); + + String key = s.serialize(proxy); + assertNotNull(key); + + // Use a new instance + store = new DefaultProxyStore(store.encode()); + s = req.getSerializer(store); + T restored = s.deserialize(id.getProxyClass(), key); + AutoBean<BaseProxy> restoredBean = AutoBeanUtils.getAutoBean(restored); + assertNotSame(proxy, restored); + /* + * Performing a regular assertEquals() or even an AutoBeanUtils.diff() here + * is wrong. If any of the objects in the graph are unpersisted, it's + * expected that the stable ids would change. Instead, we do a value-based + * check. + */ + assertTrue(AutoBeanUtils.deepEquals(originalBean, restoredBean)); + + if (proxy instanceof EntityProxy && !id.isEphemeral()) { + assertEquals(((EntityProxy) proxy).stableId(), + ((EntityProxy) restored).stableId()); + } + + // In deference to testing stable ids, copy the original id into the clone + restoredBean.setTag(Constants.STABLE_ID, + originalBean.getTag(Constants.STABLE_ID)); + return restored; + } + protected void checkStableIdEquals(EntityProxy expected, EntityProxy actual) { assertEquals(expected.stableId(), actual.stableId()); assertEquals(expected.stableId().hashCode(), actual.stableId().hashCode());