| /* |
| * 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.client.impl; |
| |
| import com.google.gwt.core.client.JsArray; |
| import com.google.gwt.editor.client.AutoBean; |
| import com.google.gwt.editor.client.AutoBeanUtils; |
| import com.google.gwt.editor.client.AutoBeanVisitor; |
| import com.google.gwt.requestfactory.client.impl.messages.RequestContentData; |
| import com.google.gwt.requestfactory.client.impl.messages.ReturnRecord; |
| import com.google.gwt.requestfactory.client.impl.messages.SideEffects; |
| import com.google.gwt.requestfactory.shared.EntityProxy; |
| import com.google.gwt.requestfactory.shared.EntityProxyChange; |
| import com.google.gwt.requestfactory.shared.Receiver; |
| import com.google.gwt.requestfactory.shared.RequestContext; |
| import com.google.gwt.requestfactory.shared.RequestTransport.TransportReceiver; |
| import com.google.gwt.requestfactory.shared.ServerFailure; |
| import com.google.gwt.requestfactory.shared.ValueCodex; |
| import com.google.gwt.requestfactory.shared.Violation; |
| import com.google.gwt.requestfactory.shared.WriteOperation; |
| import com.google.gwt.requestfactory.shared.impl.Constants; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Base implementations for RequestContext services. |
| */ |
| public class AbstractRequestContext implements RequestContext { |
| private static final String PARENT_OBJECT = "parentObject"; |
| |
| private List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>(); |
| private boolean locked; |
| private final AbstractRequestFactory requestFactory; |
| /** |
| * A map of all EntityProxies that the RequestContext has interacted with. |
| * Objects are placed into this map by being passed into {@link #edit} or as |
| * an invocation argument. |
| */ |
| private final Map<SimpleEntityProxyId<?>, AutoBean<?>> editedProxies = new LinkedHashMap<SimpleEntityProxyId<?>, AutoBean<?>>(); |
| private Set<Violation> errors = new LinkedHashSet<Violation>(); |
| /** |
| * A map that contains the canonical instance of an entity to return in the |
| * return graph, since this is built from scratch. |
| */ |
| private final Map<SimpleEntityProxyId<?>, AutoBean<?>> returnedProxies = new HashMap<SimpleEntityProxyId<?>, AutoBean<?>>(); |
| |
| protected AbstractRequestContext(AbstractRequestFactory factory) { |
| this.requestFactory = factory; |
| } |
| |
| /** |
| * Create a new object, with an ephemeral id. |
| */ |
| public <T extends EntityProxy> T create(Class<T> clazz) { |
| checkLocked(); |
| |
| AutoBean<T> created = requestFactory.createEntityProxy(clazz, |
| requestFactory.allocateId(clazz)); |
| return takeOwnership(created); |
| } |
| |
| public <T extends EntityProxy> T edit(T object) { |
| AutoBean<T> bean = checkStreamsNotCrossed(object); |
| |
| checkLocked(); |
| |
| @SuppressWarnings("unchecked") |
| AutoBean<T> previouslySeen = (AutoBean<T>) editedProxies.get(object.stableId()); |
| if (previouslySeen != null && !previouslySeen.isFrozen()) { |
| /* |
| * If we've seen the object before, it might be because it was passed in |
| * as a method argument. This does not guarantee its mutability, so check |
| * that here before returning the cached object. |
| */ |
| return previouslySeen.as(); |
| } |
| |
| // Create editable copies |
| AutoBean<T> parent = bean; |
| bean = cloneBeanAndCollections(bean); |
| bean.setTag(PARENT_OBJECT, parent); |
| return takeOwnership(bean); |
| } |
| |
| /** |
| * Make sure there's a default receiver so errors don't get dropped. This |
| * behavior should be revisited when chaining is supported, depending on |
| * whether or not chained invocations can fail independently. |
| */ |
| public void fire() { |
| boolean needsReceiver = true; |
| for (AbstractRequest<?> request : invocations) { |
| if (request.hasReceiver()) { |
| needsReceiver = false; |
| break; |
| } |
| } |
| |
| if (needsReceiver) { |
| doFire(new Receiver<Void>() { |
| @Override |
| public void onSuccess(Void response) { |
| // Don't care |
| } |
| }); |
| } else { |
| doFire(null); |
| } |
| } |
| |
| public void fire(final Receiver<Void> receiver) { |
| if (receiver == null) { |
| throw new IllegalArgumentException(); |
| } |
| doFire(receiver); |
| } |
| |
| public AbstractRequestFactory getRequestFactory() { |
| return requestFactory; |
| } |
| |
| public boolean isChanged() { |
| /* |
| * NB: Don't use the presence of ephemeral objects for this test. |
| * |
| * Diff the objects until one is found to be different. It's not just a |
| * simple flag-check because of the possibility of "unmaking" a change, per |
| * the JavaDoc. |
| */ |
| for (AutoBean<?> bean : editedProxies.values()) { |
| AutoBean<?> previous = bean.getTag(PARENT_OBJECT); |
| if (previous == null) { |
| // Compare to empty object |
| Class<?> proxyClass = ((EntityProxy) bean.as()).stableId().getProxyClass(); |
| previous = getRequestFactory().getAutoBeanFactory().create(proxyClass); |
| } |
| if (!AutoBeanUtils.diff(previous, bean).isEmpty()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean isLocked() { |
| return locked; |
| } |
| |
| public void reuse() { |
| freezeEntities(false); |
| locked = false; |
| } |
| |
| /** |
| * Called by individual invocations to aggregate all errors. |
| */ |
| protected void addErrors(Collection<Violation> errors) { |
| this.errors.addAll(errors); |
| } |
| |
| /** |
| * Called by generated subclasses to enqueue a method invocation. |
| */ |
| protected void addInvocation(AbstractRequest<?> request) { |
| if (invocations.size() > 0) { |
| // TODO(bobv): Upgrade wire protocol and server to handle chains |
| throw new IllegalStateException("Method chaining not implemented"); |
| } |
| invocations.add(request); |
| for (Object arg : request.getRequestData().getParameters()) { |
| retainArg(arg); |
| } |
| } |
| |
| /** |
| * Creates or retrieves a new canonical AutoBean to represent the given id in |
| * the returned payload. |
| */ |
| <Q extends EntityProxy> AutoBean<Q> getProxyForReturnPayloadGraph( |
| SimpleEntityProxyId<Q> id) { |
| assert !id.isEphemeral(); |
| |
| @SuppressWarnings("unchecked") |
| AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id); |
| if (bean == null) { |
| Class<Q> proxyClass = id.getProxyClass(); |
| bean = requestFactory.createEntityProxy(proxyClass, id); |
| returnedProxies.put(id, bean); |
| } |
| |
| return bean; |
| } |
| |
| /** |
| * 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 EntityProxy> Q processReturnRecord(SimpleEntityProxyId<Q> id, |
| final ReturnRecord returnRecord, WriteOperation... operations) { |
| |
| AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id); |
| |
| // Apply updates |
| toMutate.accept(new AutoBeanVisitor() { |
| @Override |
| public boolean visitReferenceProperty(String propertyName, |
| AutoBean<?> value, PropertyContext ctx) { |
| if (ctx.canSet()) { |
| if (returnRecord.hasProperty(propertyName)) { |
| Object raw = returnRecord.get(propertyName); |
| if (returnRecord.isNull(propertyName)) { |
| ctx.set(null); |
| } else { |
| Object decoded = EntityCodex.decode(ctx.getType(), |
| ctx.getElementType(), AbstractRequestContext.this, raw); |
| ctx.set(decoded); |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean visitValueProperty(String propertyName, Object value, |
| PropertyContext ctx) { |
| if (ctx.canSet()) { |
| if (returnRecord.hasProperty(propertyName)) { |
| Object raw = returnRecord.get(propertyName); |
| if (returnRecord.isNull(propertyName)) { |
| ctx.set(null); |
| } else { |
| Object decoded = ValueCodex.convertFromString(ctx.getType(), |
| String.valueOf(raw)); |
| 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) { |
| for (WriteOperation op : operations) { |
| if (op.equals(WriteOperation.UPDATE) |
| && !requestFactory.hasVersionChanged(id, returnRecord.getVersion())) { |
| // No updates if the server reports no change |
| continue; |
| } |
| requestFactory.getEventBus().fireEventFromSource( |
| new EntityProxyChange<EntityProxy>(proxy, op), id.getProxyClass()); |
| } |
| } |
| return proxy; |
| } |
| |
| /** |
| * Process a SideEffects message. |
| */ |
| void processSideEffects(SideEffects sideEffects) { |
| JsArray<ReturnRecord> persisted = sideEffects.getPersist(); |
| if (persisted != null) { |
| processReturnRecords(persisted, WriteOperation.PERSIST); |
| } |
| JsArray<ReturnRecord> updated = sideEffects.getUpdate(); |
| if (updated != null) { |
| processReturnRecords(updated, WriteOperation.UPDATE); |
| } |
| JsArray<ReturnRecord> deleted = sideEffects.getDelete(); |
| if (deleted != null) { |
| processReturnRecords(deleted, WriteOperation.DELETE); |
| } |
| } |
| |
| private void checkLocked() { |
| if (locked) { |
| throw new IllegalStateException("A request is already in progress"); |
| } |
| } |
| |
| /** |
| * This method checks that a proxy object is either immutable, or already |
| * edited by this context. |
| */ |
| private <T> AutoBean<T> checkStreamsNotCrossed(T object) { |
| AutoBean<T> bean = AutoBeanUtils.getAutoBean(object); |
| if (bean == null) { |
| // Unexpected; some kind of foreign implementation? |
| throw new IllegalArgumentException(object.getClass().getName()); |
| } |
| |
| RequestContext context = bean.getTag(EntityProxyCategory.REQUEST_CONTEXT); |
| if (!bean.isFrozen() && context != this) { |
| /* |
| * This means something is way off in the weeds. If a bean is editable, |
| * it's supposed to be associated with a RequestContext. |
| */ |
| assert context != null : "Unfrozen bean with null RequestContext"; |
| |
| /* |
| * Already editing the object in another context or it would have been in |
| * the editing map. |
| */ |
| throw new IllegalArgumentException("Attempting to edit an EntityProxy" |
| + " previously edited by another RequestContext"); |
| } |
| return bean; |
| } |
| |
| /** |
| * Shallow-clones an autobean and makes duplicates of the collection types. A |
| * regular {@link AutoBean#clone} won't duplicate reference properties. |
| */ |
| private <T> AutoBean<T> cloneBeanAndCollections(AutoBean<T> toClone) { |
| AutoBean<T> clone = toClone.clone(false); |
| clone.accept(new AutoBeanVisitor() { |
| @Override |
| public boolean visitReferenceProperty(String propertyName, |
| AutoBean<?> value, PropertyContext ctx) { |
| if (value != null) { |
| if (List.class == ctx.getType()) { |
| ctx.set(new ArrayList<Object>((List<?>) value.as())); |
| } else if (Set.class == ctx.getType()) { |
| ctx.set(new HashSet<Object>((Set<?>) value.as())); |
| } |
| } |
| return false; |
| } |
| }); |
| return clone; |
| } |
| |
| private void doFire(final Receiver<Void> receiver) { |
| checkLocked(); |
| locked = true; |
| |
| freezeEntities(true); |
| |
| String payload = makePayload(); |
| |
| requestFactory.getRequestTransport().send(payload, new TransportReceiver() { |
| public void onTransportFailure(String message) { |
| ServerFailure failure = new ServerFailure(message, null, null); |
| try { |
| // TODO: chained methods |
| assert invocations.size() == 1; |
| invocations.get(0).fail(failure); |
| if (receiver != null) { |
| receiver.onFailure(failure); |
| } |
| } finally { |
| postRequestCleanup(); |
| } |
| } |
| |
| public void onTransportSuccess(String payload) { |
| try { |
| // TODO: chained methods |
| assert invocations.size() == 1; |
| invocations.get(0).handleResponseText(payload); |
| |
| if (receiver != null) { |
| if (errors.isEmpty()) { |
| receiver.onSuccess(null); |
| // After success, shut down the context |
| editedProxies.clear(); |
| invocations.clear(); |
| returnedProxies.clear(); |
| } else { |
| receiver.onViolation(errors); |
| } |
| } |
| } finally { |
| postRequestCleanup(); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Set the frozen status of all EntityProxies owned by this context. |
| */ |
| private void freezeEntities(boolean frozen) { |
| for (AutoBean<?> bean : editedProxies.values()) { |
| bean.setFrozen(frozen); |
| } |
| } |
| |
| /** |
| * Make an EntityProxy immutable. |
| */ |
| private void makeImmutable(final AutoBean<? extends EntityProxy> toMutate) { |
| // Always diff'ed against itself, producing a no-op |
| toMutate.setTag(PARENT_OBJECT, toMutate); |
| // Act with entity-identity semantics |
| toMutate.setTag(EntityProxyCategory.REQUEST_CONTEXT, null); |
| toMutate.setFrozen(true); |
| } |
| |
| /** |
| * Assemble all of the state that has been accumulated in this context. This |
| * includes: |
| * <ul> |
| * <li>Diffs accumulated on objects passed to {@link #edit}. |
| * <li>Invocations accumulated as Request subtypes passed to |
| * {@link #addInvocation}. |
| * </ul> |
| */ |
| private String makePayload() { |
| // TODO: Chained invocations |
| assert invocations.size() == 1 : "addInvocation() should have failed"; |
| |
| // Produces the contentData payload fragment |
| RequestContentData data = new RequestContentData(); |
| |
| // Compute deltas for each entity seen by the context |
| for (AutoBean<?> currentView : editedProxies.values()) { |
| boolean isPersist = false; |
| @SuppressWarnings("unchecked") |
| SimpleEntityProxyId<EntityProxy> stableId = EntityProxyCategory.stableId((AutoBean<EntityProxy>) currentView); |
| |
| // Encoded string representations of the properties |
| Map<String, String> encoded = new LinkedHashMap<String, String>(); |
| |
| { |
| // Find the object to compare against |
| AutoBean<?> parent = currentView.getTag(PARENT_OBJECT); |
| if (parent == null) { |
| // Newly-created object, use a blank object to compare against |
| parent = requestFactory.createEntityProxy(stableId.getProxyClass(), |
| stableId); |
| |
| // Newly-created objects go into the persist operation bucket |
| isPersist = true; |
| // The ephemeral id is passed to the server |
| String clientId = String.valueOf(stableId.getClientId()); |
| encoded.put(Constants.ENCODED_ID_PROPERTY, |
| ValueCodex.encodeForJsonPayload(clientId)); |
| } else { |
| // Requests involving existing objects use the persisted id |
| encoded.put(Constants.ENCODED_ID_PROPERTY, |
| ValueCodex.encodeForJsonPayload(stableId.getServerId())); |
| } |
| |
| // Compute the diff |
| Map<String, Object> diff = AutoBeanUtils.diff(parent, currentView); |
| for (Map.Entry<String, Object> entry : diff.entrySet()) { |
| // Make JSON representations of the new values |
| encoded.put(entry.getKey(), |
| EntityCodex.encodeForJsonPayload(entry.getValue())); |
| } |
| } |
| |
| // Append the payload fragment to the correct bucket |
| String typeToken = requestFactory.getTypeToken(stableId.getProxyClass()); |
| if (isPersist) { |
| data.addPersist(typeToken, encoded); |
| } else { |
| data.addUpdate(typeToken, encoded); |
| } |
| } |
| |
| AbstractRequest<?> request = invocations.get(0); |
| // Known issue that the data is double-quoted |
| Map<String, String> requestMap = request.getRequestData().getRequestMap( |
| data.toJson()); |
| String string = RequestContentData.flattenKeysToExpressions(requestMap); |
| return string; |
| } |
| |
| /** |
| * Delete state that's no longer required. |
| */ |
| private void postRequestCleanup() { |
| errors.clear(); |
| } |
| |
| /** |
| * Process an array of ReturnRecords, delegating to |
| * {@link #processReturnRecord}. |
| */ |
| private void processReturnRecords(JsArray<ReturnRecord> records, |
| WriteOperation operations) { |
| for (int i = 0, j = records.length(); i < j; i++) { |
| ReturnRecord record = records.get(i); |
| SimpleEntityProxyId<EntityProxy> id = requestFactory.getId( |
| record.getSchema(), record.getEncodedId(), record.getFutureId()); |
| processReturnRecord(id, record, operations); |
| } |
| } |
| |
| /** |
| * Ensures that any method arguments are retained in the context's sphere of |
| * influence. |
| */ |
| private void retainArg(Object arg) { |
| if (arg instanceof Iterable<?>) { |
| for (Object o : (Iterable<?>) arg) { |
| retainArg(o); |
| } |
| } else if (arg instanceof EntityProxy) { |
| // Calling edit will validate and set up the tracking we need |
| edit((EntityProxy) arg); |
| } |
| } |
| |
| /** |
| * Make the EnityProxy bean edited and owned by this RequestContext. |
| */ |
| private <T extends EntityProxy> T takeOwnership(AutoBean<T> bean) { |
| editedProxies.put(EntityProxyCategory.stableId(bean), bean); |
| bean.setTag(EntityProxyCategory.REQUEST_CONTEXT, |
| AbstractRequestContext.this); |
| return bean.as(); |
| } |
| } |