CAY-2373 cayenne-rop-server module
 - move org.apache.cayenne.remote package to cayenne-rop server module
 - remove dependencies from cayenne-server pom.xml
 - update tutorial


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/38f37d79
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/38f37d79
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/38f37d79

Branch: refs/heads/master
Commit: 38f37d79aa6539913b83235fd4fff4ed6f826b85
Parents: eec08b7
Author: Nikita Timofeev <stari...@gmail.com>
Authored: Tue Dec 12 15:52:43 2017 +0300
Committer: Nikita Timofeev <stari...@gmail.com>
Committed: Tue Dec 12 15:52:43 2017 +0300

----------------------------------------------------------------------
 cayenne-rop-server/pom.xml                      |   2 +
 .../java/org/apache/cayenne/CayenneContext.java | 391 +++++++++++
 .../cayenne/CayenneContextChildDiffLoader.java  | 142 ++++
 .../cayenne/CayenneContextGraphManager.java     | 379 +++++++++++
 .../cayenne/CayenneContextMergeHandler.java     | 274 ++++++++
 .../cayenne/CayenneContextQueryAction.java      | 186 ++++++
 .../apache/cayenne/remote/BootstrapMessage.java |  36 +
 .../apache/cayenne/remote/ClientMessage.java    |  31 +
 .../apache/cayenne/remote/IncrementalQuery.java |  73 ++
 .../cayenne/remote/IncrementalSelectQuery.java  | 300 +++++++++
 .../org/apache/cayenne/remote/QueryMessage.java |  55 ++
 .../org/apache/cayenne/remote/RangeQuery.java   | 156 +++++
 .../remote/RemoteIncrementalFaultList.java      | 668 +++++++++++++++++++
 .../apache/cayenne/remote/RemoteService.java    |  54 ++
 .../apache/cayenne/remote/RemoteSession.java    | 145 ++++
 .../org/apache/cayenne/remote/SyncMessage.java  |  91 +++
 .../hessian/CayenneSerializerFactory.java       |  43 ++
 .../cayenne/remote/hessian/HessianConfig.java   | 114 ++++
 .../remote/hessian/service/HessianService.java  |  64 ++
 .../service/ServerDataRowSerializer.java        |  56 ++
 .../ServerPersistentObjectListSerializer.java   |  50 ++
 .../service/ServerSerializerFactory.java        |  71 ++
 .../remote/service/BaseRemoteService.java       | 199 ++++++
 .../cayenne/remote/service/DispatchHelper.java  |  49 ++
 .../remote/service/HttpRemoteService.java       | 134 ++++
 .../remote/service/MissingSessionException.java |  38 ++
 .../cayenne/remote/service/ServerSession.java   |  49 ++
 .../cayenne/CayenneContextGraphManagerTest.java |  67 ++
 .../cayenne/remote/MockRemoteService.java       |  40 ++
 .../cayenne/remote/RemoteSessionTest.java       |  58 ++
 .../remote/hessian/HessianConfigTest.java       |  61 ++
 .../hessian/MockAbstractSerializerFactory.java  |  51 ++
 .../hessian/service/HessianServiceTest.java     |  73 ++
 .../remote/service/BaseRemoteServiceTest.java   | 142 ++++
 .../remote/service/DispatchHelperTest.java      |  54 ++
 .../service/MockUnserializableException.java    |  24 +
 cayenne-server/pom.xml                          |  31 -
 .../java/org/apache/cayenne/CayenneContext.java | 391 -----------
 .../cayenne/CayenneContextChildDiffLoader.java  | 142 ----
 .../cayenne/CayenneContextGraphManager.java     | 379 -----------
 .../cayenne/CayenneContextMergeHandler.java     | 274 --------
 .../cayenne/CayenneContextQueryAction.java      | 186 ------
 .../apache/cayenne/remote/BootstrapMessage.java |  36 -
 .../apache/cayenne/remote/ClientMessage.java    |  31 -
 .../apache/cayenne/remote/IncrementalQuery.java |  73 --
 .../cayenne/remote/IncrementalSelectQuery.java  | 300 ---------
 .../org/apache/cayenne/remote/QueryMessage.java |  55 --
 .../org/apache/cayenne/remote/RangeQuery.java   | 156 -----
 .../remote/RemoteIncrementalFaultList.java      | 668 -------------------
 .../apache/cayenne/remote/RemoteService.java    |  54 --
 .../apache/cayenne/remote/RemoteSession.java    | 145 ----
 .../org/apache/cayenne/remote/SyncMessage.java  |  91 ---
 .../hessian/CayenneSerializerFactory.java       |  43 --
 .../cayenne/remote/hessian/HessianConfig.java   | 114 ----
 .../remote/hessian/service/HessianService.java  |  64 --
 .../service/ServerDataRowSerializer.java        |  56 --
 .../ServerPersistentObjectListSerializer.java   |  50 --
 .../service/ServerSerializerFactory.java        |  71 --
 .../remote/service/BaseRemoteService.java       | 199 ------
 .../cayenne/remote/service/DispatchHelper.java  |  49 --
 .../remote/service/HttpRemoteService.java       | 134 ----
 .../remote/service/MissingSessionException.java |  38 --
 .../cayenne/remote/service/ServerSession.java   |  49 --
 .../cayenne/CayenneContextGraphManagerTest.java |  67 --
 .../cayenne/remote/MockRemoteService.java       |  40 --
 .../cayenne/remote/RemoteSessionTest.java       |  58 --
 .../remote/hessian/HessianConfigTest.java       |  61 --
 .../hessian/MockAbstractSerializerFactory.java  |  51 --
 .../hessian/service/HessianServiceTest.java     |  73 --
 .../remote/service/BaseRemoteServiceTest.java   | 142 ----
 .../remote/service/DispatchHelperTest.java      |  54 --
 .../service/MockUnserializableException.java    |  24 -
 .../tutorial/persistent/client/Main.java        |   2 +-
 tutorials/tutorial-rop-server/pom.xml           |   3 +-
 .../src/main/resources/cayenne-project.xml      |   9 +-
 .../src/main/resources/datamap.map.xml          |  10 +-
 76 files changed, 4433 insertions(+), 4460 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/pom.xml
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/pom.xml b/cayenne-rop-server/pom.xml
index eddbef1..93399a0 100644
--- a/cayenne-rop-server/pom.xml
+++ b/cayenne-rop-server/pom.xml
@@ -28,6 +28,8 @@
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>servlet-api</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.apache.cayenne</groupId>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
new file mode 100644
index 0000000..c352ae0
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
@@ -0,0 +1,391 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne;
+
+import org.apache.cayenne.event.EventManager;
+import org.apache.cayenne.graph.CompoundDiff;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.graph.GraphManager;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.util.EventUtil;
+import org.apache.cayenne.validation.ValidationException;
+import org.apache.cayenne.validation.ValidationResult;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A default generic implementation of ObjectContext suitable for accessing
+ * Cayenne from either an ORM or a client tiers. Communicates with Cayenne via 
a
+ * {@link org.apache.cayenne.DataChannel}.
+ * 
+ * @since 1.2
+ */
+public class CayenneContext extends BaseContext {
+
+    CayenneContextGraphManager graphManager;
+
+    // object that merges "backdoor" changes that come from the channel.
+    CayenneContextMergeHandler mergeHandler;
+
+    /**
+     * Creates a new CayenneContext with no channel and disabled graph events.
+     */
+    public CayenneContext() {
+        this(null);
+    }
+
+    /**
+     * Creates a new CayenneContext, initializing it with a channel instance.
+     * CayenneContext created using this constructor WILL NOT broadcast graph
+     * change events.
+     */
+    public CayenneContext(DataChannel channel) {
+        this(channel, false, false);
+    }
+
+    /**
+     * Creates a new CayenneContext, initializing it with a channel.
+     */
+    public CayenneContext(DataChannel channel, boolean changeEventsEnabled, 
boolean lifecyleEventsEnabled) {
+
+        graphManager = new CayenneContextGraphManager(this, 
changeEventsEnabled, lifecyleEventsEnabled);
+
+        if (channel != null) {
+            attachToChannel(channel);
+        }
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    protected void attachToChannel(DataChannel channel) {
+        super.attachToChannel(channel);
+
+        if (mergeHandler != null) {
+            mergeHandler.active = false;
+            mergeHandler = null;
+        }
+
+        EventManager eventManager = channel.getEventManager();
+        if (eventManager != null) {
+            mergeHandler = new CayenneContextMergeHandler(this);
+
+            // listen to our channel events...
+            // note that we must reset listener on channel switch, as there is
+            // no
+            // guarantee that a new channel uses the same EventManager.
+            EventUtil.listenForChannelEvents(channel, mergeHandler);
+        }
+    }
+
+    /**
+     * Returns true if this context posts individual object modification 
events.
+     * Subject used for these events is
+     * <code>ObjectContext.GRAPH_CHANGED_SUBJECT</code>.
+     */
+    public boolean isChangeEventsEnabled() {
+        return graphManager.changeEventsEnabled;
+    }
+
+    /**
+     * Returns true if this context posts lifecycle events. Subjects used for
+     * these events are
+     * <code>ObjectContext.GRAPH_COMMIT_STARTED_SUBJECT, 
ObjectContext.GRAPH_COMMITTED_SUBJECT,
+     * ObjectContext.GRAPH_COMMIT_ABORTED_SUBJECT, 
ObjectContext.GRAPH_ROLLEDBACK_SUBJECT.</code>
+     * .
+     */
+    public boolean isLifecycleEventsEnabled() {
+        return graphManager.lifecycleEventsEnabled;
+    }
+
+    @Override
+    public GraphManager getGraphManager() {
+        return graphManager;
+    }
+
+    CayenneContextGraphManager internalGraphManager() {
+        return graphManager;
+    }
+
+    /**
+     * Commits changes to uncommitted objects. First checks if there are 
changes
+     * in this context and if any changes are detected, sends a commit message
+     * to remote Cayenne service via an internal instance of CayenneConnector.
+     */
+    @Override
+    public void commitChanges() {
+        doCommitChanges(true);
+    }
+
+    GraphDiff doCommitChanges(boolean cascade) {
+
+        int syncType = cascade ? DataChannel.FLUSH_CASCADE_SYNC : 
DataChannel.FLUSH_NOCASCADE_SYNC;
+
+        GraphDiff commitDiff = null;
+
+        synchronized (graphManager) {
+
+            if (graphManager.hasChanges()) {
+
+                if (isValidatingObjectsOnCommit()) {
+                    ValidationResult result = new ValidationResult();
+                    Iterator<?> it = graphManager.dirtyNodes().iterator();
+                    while (it.hasNext()) {
+                        Persistent p = (Persistent) it.next();
+                        if (p instanceof Validating) {
+                            switch (p.getPersistenceState()) {
+                            case PersistenceState.NEW:
+                                ((Validating) p).validateForInsert(result);
+                                break;
+                            case PersistenceState.MODIFIED:
+                                ((Validating) p).validateForUpdate(result);
+                                break;
+                            case PersistenceState.DELETED:
+                                ((Validating) p).validateForDelete(result);
+                                break;
+                            }
+                        }
+                    }
+
+                    if (result.hasFailures()) {
+                        throw new ValidationException(result);
+                    }
+                }
+
+                graphManager.graphCommitStarted();
+
+                GraphDiff changes = graphManager.getDiffsSinceLastFlush();
+
+                try {
+                    commitDiff = channel.onSync(this, changes, syncType);
+                } catch (Throwable th) {
+                    graphManager.graphCommitAborted();
+
+                    if (th instanceof CayenneRuntimeException) {
+                        throw (CayenneRuntimeException) th;
+                    } else {
+                        throw new CayenneRuntimeException("Commit error", th);
+                    }
+                }
+
+                graphManager.graphCommitted(commitDiff);
+
+                // this event is caught by peer nested ObjectContexts to
+                // synchronize the
+                // state
+                fireDataChannelCommitted(this, changes);
+            }
+        }
+
+        return commitDiff;
+    }
+
+    @Override
+    public void commitChangesToParent() {
+        doCommitChanges(false);
+    }
+
+    @Override
+    public void rollbackChanges() {
+        synchronized (graphManager) {
+            if (graphManager.hasChanges()) {
+
+                GraphDiff diff = graphManager.getDiffs();
+                graphManager.graphReverted();
+
+                channel.onSync(this, diff, DataChannel.ROLLBACK_CASCADE_SYNC);
+                fireDataChannelRolledback(this, diff);
+            }
+        }
+    }
+
+    @Override
+    public void rollbackChangesLocally() {
+        synchronized (graphManager) {
+            if (graphManager.hasChanges()) {
+                GraphDiff diff = graphManager.getDiffs();
+                graphManager.graphReverted();
+
+                fireDataChannelRolledback(this, diff);
+            }
+        }
+    }
+
+    /**
+     * Creates and registers a new Persistent object instance.
+     */
+    @Override
+    public <T> T newObject(Class<T> persistentClass) {
+        if (persistentClass == null) {
+            throw new NullPointerException("Persistent class can't be null.");
+        }
+
+        ObjEntity entity = getEntityResolver().getObjEntity(persistentClass);
+        if (entity == null) {
+            throw new CayenneRuntimeException("No entity mapped for class: 
%s", persistentClass);
+        }
+
+        ClassDescriptor descriptor = 
getEntityResolver().getClassDescriptor(entity.getName());
+        @SuppressWarnings("unchecked")
+        T object = (T) descriptor.createObject();
+        descriptor.injectValueHolders(object);
+        registerNewObject((Persistent) object, entity.getName(), descriptor);
+        return object;
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public void registerNewObject(Object object) {
+        if (object == null) {
+            throw new NullPointerException("An attempt to register null 
object.");
+        }
+
+        ObjEntity entity = getEntityResolver().getObjEntity(object.getClass());
+        ClassDescriptor descriptor = 
getEntityResolver().getClassDescriptor(entity.getName());
+        registerNewObject((Persistent) object, entity.getName(), descriptor);
+    }
+
+    /**
+     * Runs a query, returning result as list.
+     */
+    @Override
+    public List performQuery(Query query) {
+        List result = onQuery(this, query).firstList();
+        return result != null ? result : new ArrayList<>(1);
+    }
+
+    @Override
+    public QueryResponse performGenericQuery(Query query) {
+        return onQuery(this, query);
+    }
+
+    public QueryResponse onQuery(ObjectContext context, Query query) {
+        return new CayenneContextQueryAction(this, context, query).execute();
+    }
+
+    @Override
+    public Collection<?> uncommittedObjects() {
+        synchronized (graphManager) {
+            return graphManager.dirtyNodes();
+        }
+    }
+
+    @Override
+    public Collection<?> deletedObjects() {
+        synchronized (graphManager) {
+            return graphManager.dirtyNodes(PersistenceState.DELETED);
+        }
+    }
+
+    @Override
+    public Collection<?> modifiedObjects() {
+        synchronized (graphManager) {
+            return graphManager.dirtyNodes(PersistenceState.MODIFIED);
+        }
+    }
+
+    @Override
+    public Collection<?> newObjects() {
+        synchronized (graphManager) {
+            return graphManager.dirtyNodes(PersistenceState.NEW);
+        }
+    }
+
+    // ****** non-public methods ******
+
+    void registerNewObject(Persistent object, String entityName, 
ClassDescriptor descriptor) {
+        /**
+         * We should create new id only if it is not set for this object. It
+         * could have been created, for instance, in child context
+         */
+        ObjectId id = object.getObjectId();
+        if (id == null) {
+            id = new ObjectId(entityName);
+            object.setObjectId(id);
+        }
+
+        injectInitialValue(object);
+    }
+
+    Persistent createFault(ObjectId id) {
+        ClassDescriptor descriptor = 
getEntityResolver().getClassDescriptor(id.getEntityName());
+
+        Persistent object = (Persistent) descriptor.createObject();
+
+        object.setPersistenceState(PersistenceState.HOLLOW);
+        object.setObjectContext(this);
+        object.setObjectId(id);
+
+        graphManager.registerNode(id, object);
+
+        return object;
+    }
+
+    @Override
+    protected GraphDiff onContextFlush(ObjectContext originatingContext, 
GraphDiff changes, boolean cascade) {
+
+        boolean childContext = this != originatingContext && changes != null;
+
+        if (childContext) {
+
+            // PropertyChangeProcessingStrategy oldStrategy =
+            // getPropertyChangeProcessingStrategy();
+            // 
setPropertyChangeProcessingStrategy(PropertyChangeProcessingStrategy.RECORD);
+            try {
+                changes.apply(new CayenneContextChildDiffLoader(this));
+            } finally {
+                // setPropertyChangeProcessingStrategy(oldStrategy);
+            }
+
+            fireDataChannelChanged(originatingContext, changes);
+        }
+
+        return (cascade) ? doCommitChanges(true) : new CompoundDiff();
+    }
+
+    /**
+     * Returns <code>true</code> if there are any modified, deleted or new
+     * objects registered with this CayenneContext, <code>false</code>
+     * otherwise.
+     */
+    public boolean hasChanges() {
+        return graphManager.hasChanges();
+    }
+
+    /**
+     * This method simply returns an iterator over a list of selected objects.
+     * There's no performance benefit of using it vs. regular "select".
+     * 
+     * @since 4.0
+     */
+    public <T> ResultIterator<T> iterator(org.apache.cayenne.query.Select<T> 
query) {
+        List<T> objects = select(query);
+        return new CollectionResultIterator<T>(objects);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextChildDiffLoader.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextChildDiffLoader.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextChildDiffLoader.java
new file mode 100644
index 0000000..151b72e
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextChildDiffLoader.java
@@ -0,0 +1,142 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne;
+
+import org.apache.cayenne.graph.ChildDiffLoader;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+
+/**
+ * Used for loading child's CayenneContext changes to parent context.
+ * 
+ * @since 3.0
+ */
+class CayenneContextChildDiffLoader extends ChildDiffLoader {
+
+    public CayenneContextChildDiffLoader(CayenneContext context) {
+        super(context);
+    }
+
+    @Override
+    public void nodePropertyChanged(
+            Object nodeId,
+            String property,
+            Object oldValue,
+            Object newValue) {
+
+        super.nodePropertyChanged(nodeId, property, oldValue, newValue);
+
+        Persistent object = (Persistent) 
context.getGraphManager().getNode(nodeId);
+        context.propertyChanged(object, property, oldValue, newValue);
+    }
+
+    @Override
+    public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) {
+
+        final Persistent source = findObject(nodeId);
+        final Persistent target = findObject(targetNodeId);
+
+        // if a target was later deleted, the diff for arcCreated is still 
preserved and
+        // can result in NULL target here.
+        if (target == null) {
+            return;
+        }
+
+        ClassDescriptor descriptor = 
context.getEntityResolver().getClassDescriptor(
+                ((ObjectId) nodeId).getEntityName());
+        ArcProperty property = (ArcProperty) 
descriptor.getProperty(arcId.toString());
+
+        property.visit(new PropertyVisitor() {
+
+            public boolean visitAttribute(AttributeProperty property) {
+                return false;
+            }
+
+            public boolean visitToMany(ToManyProperty property) {
+                property.addTargetDirectly(source, target);
+                return false;
+            }
+
+            public boolean visitToOne(ToOneProperty property) {
+                property.setTarget(source, target, false);
+                return false;
+            }
+        });
+        context.propertyChanged(source, (String) arcId, null, target);
+    }
+
+    @Override
+    public void arcDeleted(Object nodeId, final Object targetNodeId, Object 
arcId) {
+        final Persistent source = findObject(nodeId);
+
+        // needed as sometime temporary objects are evoked from the context 
before
+        // changing their relationships
+        if (source == null) {
+            return;
+        }
+
+        ClassDescriptor descriptor = 
context.getEntityResolver().getClassDescriptor(
+                ((ObjectId) nodeId).getEntityName());
+        PropertyDescriptor property = descriptor.getProperty(arcId.toString());
+
+        final Persistent[] target = new Persistent[1];
+        target[0] = findObject(targetNodeId);
+        
+        property.visit(new PropertyVisitor() {
+
+            public boolean visitAttribute(AttributeProperty property) {
+                return false;
+            }
+
+            public boolean visitToMany(ToManyProperty property) {
+                if (target[0] == null) {
+
+                    // this is usually the case when a NEW object was deleted 
and then
+                    // its relationships were manipulated; so try to locate 
the object
+                    // in the collection ... the performance of this is rather 
dubious
+                    // of course...
+                    target[0] = findObjectInCollection(targetNodeId, property
+                            .readProperty(source));
+                }
+
+                if (target[0] == null) {
+                    // ignore?
+                }
+                else {
+                    property.removeTargetDirectly(source, target[0]);
+                }
+
+                return false;
+            }
+
+            public boolean visitToOne(ToOneProperty property) {
+                property.setTarget(source, null, false);
+                return false;
+            }
+        });
+
+        context.propertyChanged(source, (String) arcId, target[0], null);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextGraphManager.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextGraphManager.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextGraphManager.java
new file mode 100644
index 0000000..f4865c0
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextGraphManager.java
@@ -0,0 +1,379 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne;
+
+import org.apache.cayenne.event.EventManager;
+import org.apache.cayenne.event.EventSubject;
+import org.apache.cayenne.graph.ArcCreateOperation;
+import org.apache.cayenne.graph.ArcDeleteOperation;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.graph.GraphEvent;
+import org.apache.cayenne.graph.GraphMap;
+import org.apache.cayenne.graph.NodeCreateOperation;
+import org.apache.cayenne.graph.NodeDeleteOperation;
+import org.apache.cayenne.graph.NodeIdChangeOperation;
+import org.apache.cayenne.graph.NodePropertyChangeOperation;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyException;
+import org.apache.cayenne.reflect.ToManyMapProperty;
+import org.apache.cayenne.util.PersistentObjectMap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A GraphMap extension that works together with {@link ObjectContext} to 
track persistent object
+ * changes and send events.
+ * 
+ * @since 1.2
+ */
+final class CayenneContextGraphManager extends GraphMap {
+
+    static final String COMMIT_MARKER = "commit";
+    static final String FLUSH_MARKER = "flush";
+
+    CayenneContext context;
+    Collection<Object> deadIds;
+    boolean changeEventsEnabled;
+    boolean lifecycleEventsEnabled;
+
+    ObjectContextStateLog stateLog;
+    ObjectContextChangeLog changeLog;
+
+    CayenneContextGraphManager(CayenneContext context, boolean 
changeEventsEnabled,
+            boolean lifecycleEventsEnabled) {
+
+        this.context = context;
+        this.changeEventsEnabled = changeEventsEnabled;
+        this.lifecycleEventsEnabled = lifecycleEventsEnabled;
+
+        this.stateLog = new ObjectContextStateLog(this);
+        this.changeLog = new ObjectContextChangeLog();
+    }
+
+    boolean hasChanges() {
+        return changeLog.size() > 0;
+    }
+
+    boolean hasChangesSinceLastFlush() {
+        int size = changeLog.hasMarker(FLUSH_MARKER) ? changeLog
+                .sizeAfterMarker(FLUSH_MARKER) : changeLog.size();
+        return size > 0;
+    }
+
+    GraphDiff getDiffs() {
+        return changeLog.getDiffs();
+    }
+
+    GraphDiff getDiffsSinceLastFlush() {
+        return changeLog.hasMarker(FLUSH_MARKER) ? changeLog
+                .getDiffsAfterMarker(FLUSH_MARKER) : changeLog.getDiffs();
+    }
+
+    Collection<Object> dirtyNodes() {
+        return stateLog.dirtyNodes();
+    }
+
+    Collection<Object> dirtyNodes(int state) {
+        return stateLog.dirtyNodes(state);
+    }
+
+    @Override
+    public synchronized Object unregisterNode(Object nodeId) {
+        Object node = super.unregisterNode(nodeId);
+
+        // remove node from other collections...
+        if (node != null) {
+            stateLog.unregisterNode(nodeId);
+            changeLog.unregisterNode(nodeId);
+            Persistent object = (Persistent)node;
+            object.setObjectContext(null);
+            object.setPersistenceState(PersistenceState.TRANSIENT);
+            return node;
+        }
+
+        return null;
+    }
+
+    // ****** Sync Events API *****
+    /**
+     * Clears commit marker, but keeps all recorded operations.
+     */
+    void graphCommitAborted() {
+        changeLog.removeMarker(COMMIT_MARKER);
+    }
+
+    /**
+     * Sets commit start marker in the change log. If events are enabled, 
posts commit
+     * start event.
+     */
+    void graphCommitStarted() {
+        changeLog.setMarker(COMMIT_MARKER);
+    }
+
+    void graphCommitted(GraphDiff parentSyncDiff) {
+        if (parentSyncDiff != null) {
+            new CayenneContextMergeHandler(context).merge(parentSyncDiff);
+        }
+
+        remapTargets();
+
+        stateLog.graphCommitted();
+        reset();
+
+        if (lifecycleEventsEnabled) {
+            // include all diffs after the commit start marker.
+            // We fire event as if it was posted by parent channel, so that
+            // nested contexts could catch it
+            context.fireDataChannelCommitted(context.getChannel(), 
parentSyncDiff);
+        }
+    }
+
+    /**
+     * Remaps keys in to-many map relationships that contain dirty objects with
+     * potentially modified properties.
+     */
+    private void remapTargets() {
+
+        Iterator<Object> it = stateLog.dirtyIds().iterator();
+
+        EntityResolver resolver = context.getEntityResolver();
+
+        while (it.hasNext()) {
+            ObjectId id = (ObjectId) it.next();
+            ClassDescriptor descriptor = 
resolver.getClassDescriptor(id.getEntityName());
+
+            Collection<ArcProperty> mapArcProperties = 
descriptor.getMapArcProperties();
+            if (!mapArcProperties.isEmpty()) {
+
+                Object object = getNode(id);
+
+                for (ArcProperty arc : mapArcProperties) {
+                    ToManyMapProperty reverseArc = (ToManyMapProperty) arc
+                            .getComplimentaryReverseArc();
+
+                    Object source = arc.readPropertyDirectly(object);
+                    if (source != null && !reverseArc.isFault(source)) {
+                        remapTarget(reverseArc, source, object);
+                    }
+                }
+            }
+        }
+    }
+
+    // clone of DataDomainSyncBucket.remapTarget
+    private final void remapTarget(
+            ToManyMapProperty property,
+            Object source,
+            Object target) throws PropertyException {
+
+        @SuppressWarnings("unchecked")
+        Map<Object, Object> map = (Map<Object, Object>) 
property.readProperty(source);
+
+        Object newKey = property.getMapKey(target);
+        Object currentValue = map.get(newKey);
+
+        if (currentValue == target) {
+            // nothing to do
+            return;
+        }
+        // else - do not check for conflicts here (i.e. another object mapped 
for the same
+        // key), as we have no control of the order in which this method is 
called, so
+        // another object may be remapped later by the caller
+
+        // must do a slow map scan to ensure the object is not mapped under a 
different
+        // key...
+        Iterator<?> it = map.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<?, ?> e = (Map.Entry<?, ?>) it.next();
+            if (e.getValue() == target) {
+                // this remove does not trigger event in PersistentObjectMap
+                it.remove();
+                break;
+            }
+        }
+
+        // TODO: (andrey, 25/11/09 - this is a hack to prevent event triggering
+        // (and concurrent exceptions)
+        // should find a way to get rid of type casting
+        ((PersistentObjectMap) map).putDirectly(newKey, target);
+    }
+
+    void graphFlushed() {
+        changeLog.setMarker(FLUSH_MARKER);
+    }
+
+    void graphReverted() {
+        GraphDiff diff = changeLog.getDiffs();
+
+        diff.undo(new RollbackChangeHandler());
+        stateLog.graphReverted();
+        reset();
+
+        if (lifecycleEventsEnabled) {
+            context.fireDataChannelRolledback(context, diff);
+        }
+    }
+
+    // ****** GraphChangeHandler API ******
+    // =====================================================
+
+    @Override
+    public synchronized void nodeIdChanged(Object nodeId, Object newId) {
+        stateLog.nodeIdChanged(nodeId, newId);
+        processChange(new NodeIdChangeOperation(nodeId, newId));
+    }
+
+    @Override
+    public synchronized void nodeCreated(Object nodeId) {
+        stateLog.nodeCreated(nodeId);
+        processChange(new NodeCreateOperation(nodeId));
+    }
+
+    @Override
+    public synchronized void nodeRemoved(Object nodeId) {
+        stateLog.nodeRemoved(nodeId);
+        processChange(new NodeDeleteOperation(nodeId));
+    }
+
+    @Override
+    public synchronized void nodePropertyChanged(
+            Object nodeId,
+            String property,
+            Object oldValue,
+            Object newValue) {
+
+        stateLog.nodePropertyChanged(nodeId, property, oldValue, newValue);
+        processChange(new NodePropertyChangeOperation(
+                nodeId,
+                property,
+                oldValue,
+                newValue));
+    }
+
+    @Override
+    public synchronized void arcCreated(Object nodeId, Object targetNodeId, 
Object arcId) {
+        stateLog.arcCreated(nodeId, targetNodeId, arcId);
+        processChange(new ArcCreateOperation(nodeId, targetNodeId, arcId));
+    }
+
+    @Override
+    public synchronized void arcDeleted(Object nodeId, Object targetNodeId, 
Object arcId) {
+        stateLog.arcDeleted(nodeId, targetNodeId, arcId);
+        processChange(new ArcDeleteOperation(nodeId, targetNodeId, arcId));
+    }
+
+    // ****** helper methods ******
+    // =====================================================
+
+    private void processChange(GraphDiff diff) {
+        changeLog.addOperation(diff);
+
+        if (changeEventsEnabled) {
+            context.fireDataChannelChanged(context, diff);
+        }
+    }
+
+    /**
+     * Wraps GraphDiff in a GraphEvent and sends it via EventManager with 
specified
+     * subject.
+     */
+    void send(GraphDiff diff, EventSubject subject, Object eventSource) {
+        EventManager manager = (context.getChannel() != null) ? context
+                .getChannel()
+                .getEventManager() : null;
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(context, eventSource, diff);
+            manager.postEvent(e, subject);
+        }
+    }
+
+    void reset() {
+        changeLog.reset();
+
+        if (deadIds != null) {
+            // unregister dead ids...
+            for (final Object deadId : deadIds) {
+                nodes.remove(deadId);
+            }
+
+            deadIds = null;
+        }
+    }
+
+    Collection<Object> deadIds() {
+        if (deadIds == null) {
+            deadIds = new ArrayList<>();
+        }
+
+        return deadIds;
+    }
+
+    /**
+     * This change handler is used to perform rollback actions for Cayenne 
context
+     */
+    class RollbackChangeHandler implements GraphChangeHandler {
+
+        public void arcCreated(Object nodeId, Object targetNodeId, Object 
arcId) {
+            context.mergeHandler.arcCreated(nodeId, targetNodeId, arcId);
+            CayenneContextGraphManager.this.arcCreated(nodeId, targetNodeId, 
arcId);
+        }
+
+        public void arcDeleted(Object nodeId, Object targetNodeId, Object 
arcId) {
+            context.mergeHandler.arcDeleted(nodeId, targetNodeId, arcId);
+            CayenneContextGraphManager.this.arcDeleted(nodeId, targetNodeId, 
arcId);
+        }
+
+        public void nodeCreated(Object nodeId) {
+            CayenneContextGraphManager.this.nodeCreated(nodeId);
+        }
+
+        public void nodeIdChanged(Object nodeId, Object newId) {
+            CayenneContextGraphManager.this.nodeIdChanged(nodeId, newId);
+        }
+
+        /**
+         * Need to write property directly to this context
+         */
+        public void nodePropertyChanged(
+                Object nodeId,
+                String property,
+                Object oldValue,
+                Object newValue) {
+            context.mergeHandler
+                    .nodePropertyChanged(nodeId, property, oldValue, newValue);
+            CayenneContextGraphManager.this.nodePropertyChanged(
+                    nodeId,
+                    property,
+                    oldValue,
+                    newValue);
+        }
+
+        public void nodeRemoved(Object nodeId) {
+            CayenneContextGraphManager.this.nodeRemoved(nodeId);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
new file mode 100644
index 0000000..f490128
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
@@ -0,0 +1,274 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne;
+
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.graph.GraphEvent;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyDescriptor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.util.Util;
+
+/**
+ * An object that merges "backdoor" modifications of the object graph coming 
from the
+ * underlying DataChannel. When doing an update, CayenneContextMergeHandler 
blocks
+ * broadcasting of GraphManager events.
+ * 
+ * @since 1.2
+ */
+class CayenneContextMergeHandler implements GraphChangeHandler, 
DataChannelListener {
+
+    CayenneContext context;
+    boolean active;
+
+    CayenneContextMergeHandler(CayenneContext context) {
+        this.context = context;
+        this.active = true;
+    }
+
+    // ******* DataChannelListener methods *******
+
+    public void graphChanged(final GraphEvent e) {
+        // process flush
+        if (shouldProcessEvent(e) && e.getDiff() != null) {
+            runWithEventsDisabled(new Runnable() {
+
+                public void run() {
+                    e.getDiff().apply(CayenneContextMergeHandler.this);
+
+                }
+            });
+
+            // post event outside of "execute" to make sure it is sent
+            repostAfterMerge(e);
+        }
+    }
+
+    public void graphFlushed(final GraphEvent e) {
+        // TODO (Andrus, 10/17/2005) - there are a few problems with commit 
processing:
+
+        // 1. Event mechanism reliability:
+        // - events may come out of order (commit and then preceeding flush)
+        // - events may be missing all together (commit arrived, while prior 
flush did
+        // not)
+        // Possible solution - an "event_version_id" to be used for optimistic 
locking
+
+        // 2. We don't know if our own dirty objects were committed or not...
+        // For now we will simply merge the changes, and keep the context dirty
+
+        if (shouldProcessEvent(e)) {
+
+            runWithEventsDisabled(new Runnable() {
+
+                public void run() {
+
+                    if (e.getDiff() != null) {
+                        e.getDiff().apply(CayenneContextMergeHandler.this);
+                    }
+                }
+            });
+
+            // post event outside of "execute" to make sure it is sent
+            repostAfterMerge(e);
+        }
+    }
+
+    public void graphRolledback(final GraphEvent e) {
+
+        // TODO: andrus, 3/29/2007: per CAY-771, if a LOCAL peer context 
posted the event,
+        // just ignore it, however if the REMOTE peer reverted the parent 
remote
+        // DataContext, we need to invalidate stale committed objects...
+    }
+
+    // ******* End DataChannelListener methods *******
+
+    void repostAfterMerge(GraphEvent originalEvent) {
+        // though the subject is CHANGE, "merge" events are really lifecycle.
+        if (context.isLifecycleEventsEnabled()) {
+            context.fireDataChannelChanged(originalEvent.getSource(), 
originalEvent.getDiff());
+        }
+    }
+
+    /**
+     * Executes merging of the external diff.
+     */
+    void merge(final GraphDiff diff) {
+        runWithEventsDisabled(new Runnable() {
+
+            public void run() {
+                diff.apply(CayenneContextMergeHandler.this);
+            }
+        });
+    }
+
+    // ******* GraphChangeHandler methods *********
+
+    public void nodeIdChanged(Object nodeId, Object newId) {
+        // do not unregister the node just yet... only put replaced id in 
deadIds to
+        // remove it later. Otherwise stored operations will not work
+        Object node = context.internalGraphManager().getNode(nodeId);
+
+        if (node != null) {
+            context.internalGraphManager().deadIds().add(nodeId);
+            context.internalGraphManager().registerNode(newId, node);
+
+            if (node instanceof Persistent) {
+                // inject new id
+                ((Persistent) node).setObjectId((ObjectId) newId);
+            }
+        }
+    }
+
+    public void nodeCreated(Object nodeId) {
+        // ignore
+    }
+
+    public void nodeRemoved(Object nodeId) {
+        context.getGraphManager().unregisterNode(nodeId);
+    }
+
+    public void nodePropertyChanged(
+            Object nodeId,
+            String property,
+            Object oldValue,
+            Object newValue) {
+
+        Object object = context.internalGraphManager().getNode(nodeId);
+        if (object != null) {
+
+            // do not override local changes....
+            PropertyDescriptor p = propertyForId(nodeId, property);
+            if (Util.nullSafeEquals(p.readPropertyDirectly(object), oldValue)) 
{
+
+                p.writePropertyDirectly(object, oldValue, newValue);
+            }
+        }
+    }
+
+    public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) {
+        // null source or target likely means the object is not faulted yet... 
Faults
+        // shouldn't get disturbed by adding/removing arcs
+
+        Object source = context.internalGraphManager().getNode(nodeId);
+        if (source == null) {
+            // no need to connect non-existent object
+            return;
+        }
+
+        // TODO (Andrus, 10/17/2005) - check for local modifications to avoid
+        // overwriting...
+
+        ArcProperty p = (ArcProperty) propertyForId(nodeId, arcId.toString());
+        if (p.isFault(source)) {
+            return;
+        }
+
+        Object target = context.internalGraphManager().getNode(targetNodeId);
+        if (target == null) {
+            target = context.createFault((ObjectId) targetNodeId);
+        }
+
+        try {
+            if (p instanceof ToManyProperty) {
+                ((ToManyProperty) p).addTargetDirectly(source, target);
+            }
+            else {
+                p.writePropertyDirectly(source, null, target);
+            }
+        }
+        finally {
+        }
+    }
+
+    public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) {
+
+        // null source or target likely means the object is not faulted yet... 
Faults
+        // shouldn't get disturbed by adding/removing arcs
+
+        Object source = context.internalGraphManager().getNode(nodeId);
+        if (source == null) {
+            // no need to disconnect non-existent object
+            return;
+        }
+
+        // (see "TODO" in 'arcCreated')
+        ArcProperty p = (ArcProperty) propertyForId(nodeId, arcId.toString());
+        if (p.isFault(source)) {
+            return;
+        }
+
+        Object target = context.internalGraphManager().getNode(targetNodeId);
+        if (target == null) {
+            target = context.createFault((ObjectId) targetNodeId);
+        }
+
+        try {
+            if (p instanceof ToManyProperty) {
+                ((ToManyProperty) p).removeTargetDirectly(source, target);
+            }
+            else {
+                p.writePropertyDirectly(source, target, null);
+            }
+        }
+        finally {
+        }
+    }
+
+    private PropertyDescriptor propertyForId(Object nodeId, String 
propertyName) {
+        ClassDescriptor descriptor = 
context.getEntityResolver().getClassDescriptor(
+                ((ObjectId) nodeId).getEntityName());
+        return descriptor.getProperty(propertyName);
+    }
+
+    // Returns true if this object is active; an event came from our channel, 
but did not
+    // originate in it.
+    boolean shouldProcessEvent(GraphEvent e) {
+        // only process events that came from our channel, but did not 
originate in it
+        // (i.e. likely posted by EventBridge)
+        return active
+                && e.getSource() == context.getChannel()
+                && e.getPostedBy() != context
+                && e.getPostedBy() != context.getChannel();
+    }
+
+    // executes a closure, disabling ObjectContext events for the duration of 
the
+    // execution.
+
+    private void runWithEventsDisabled(Runnable closure) {
+
+        synchronized (context.internalGraphManager()) {
+            boolean changeEventsEnabled = 
context.internalGraphManager().changeEventsEnabled;
+            context.internalGraphManager().changeEventsEnabled = false;
+
+            boolean lifecycleEventsEnabled = 
context.internalGraphManager().lifecycleEventsEnabled;
+            context.internalGraphManager().lifecycleEventsEnabled = false;
+
+            try {
+                closure.run();
+            }
+            finally {
+                context.internalGraphManager().changeEventsEnabled = 
changeEventsEnabled;
+                context.internalGraphManager().lifecycleEventsEnabled = 
lifecycleEventsEnabled;
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextQueryAction.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextQueryAction.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextQueryAction.java
new file mode 100644
index 0000000..e0b693c
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextQueryAction.java
@@ -0,0 +1,186 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.cayenne.cache.QueryCacheEntryFactory;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.RefreshQuery;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.remote.RemoteIncrementalFaultList;
+import org.apache.cayenne.util.ListResponse;
+import org.apache.cayenne.util.ObjectContextQueryAction;
+
+/**
+ * @since 1.2
+ */
+class CayenneContextQueryAction extends ObjectContextQueryAction {
+
+    CayenneContextQueryAction(CayenneContext actingContext, ObjectContext 
targetContext,
+            Query query) {
+        super(actingContext, targetContext, query);
+    }
+
+    @Override
+    protected boolean interceptPaginatedQuery() {
+        if (metadata.getPageSize() > 0) {
+            response = new ListResponse(new RemoteIncrementalFaultList(
+                    actingContext,
+                    query));
+            return DONE;
+        }
+
+        return !DONE;
+    }
+
+    @Override
+    protected QueryCacheEntryFactory getCacheObjectFactory() {
+        return new QueryCacheEntryFactory() {
+
+            public List createObject() {
+                if (interceptPaginatedQuery() != DONE) {
+                    runQuery();
+                }
+                return response.firstList();
+            }
+        };
+    }
+
+    @Override
+    protected boolean interceptRefreshQuery() {
+        if (query instanceof RefreshQuery) {
+            RefreshQuery refreshQuery = (RefreshQuery) query;
+
+            CayenneContext context = (CayenneContext) actingContext;
+
+            // handle 4 separate scenarios, but do not combine them as it will 
be
+            // unclear how to handle cascading behavior
+
+            // 1. refresh all
+            if (refreshQuery.isRefreshAll()) {
+
+                invalidateLocally(context.internalGraphManager(), context
+                        .internalGraphManager()
+                        .registeredNodes()
+                        .iterator());
+                context.getQueryCache().clear();
+
+                // cascade
+                return !DONE;
+            }
+
+            // 2. invalidate object collection
+            Collection<?> objects = refreshQuery.getObjects();
+            if (objects != null && !objects.isEmpty()) {
+
+                invalidateLocally(context.internalGraphManager(), 
objects.iterator());
+
+                // cascade
+                return !DONE;
+            }
+
+            // 3. refresh query - have to do it eagerly to refresh the objects 
involved
+            if (refreshQuery.getQuery() != null) {
+                Query cachedQuery = refreshQuery.getQuery();
+
+                String cacheKey = cachedQuery
+                        .getMetaData(context.getEntityResolver())
+                        .getCacheKey();
+                context.getQueryCache().remove(cacheKey);
+
+                this.response = context.performGenericQuery(cachedQuery);
+
+                // do not cascade to avoid running query twice
+                return DONE;
+            }
+
+            // 4. refresh groups...
+            if (refreshQuery.getGroupKeys() != null
+                    && refreshQuery.getGroupKeys().length > 0) {
+
+                String[] groups = refreshQuery.getGroupKeys();
+                for (String group : groups) {
+                    context.getQueryCache().removeGroup(group);
+                }
+
+                // cascade group invalidation
+                return !DONE;
+            }
+        }
+
+        return !DONE;
+    }
+
+    private void invalidateLocally(CayenneContextGraphManager graphManager, 
Iterator<?> it) {
+        if (!it.hasNext()) {
+            return;
+        }
+
+        EntityResolver resolver = actingContext.getEntityResolver();
+
+        while (it.hasNext()) {
+            final Persistent object = (Persistent) it.next();
+
+            // we don't care about NEW objects,
+            // but we still do care about HOLLOW, since snapshot might still be
+            // present
+            if (object.getPersistenceState() == PersistenceState.NEW) {
+                continue;
+            }
+
+            ObjectId id = object.getObjectId();
+
+            // per CAY-1082 ROP objects (unlike CayenneDataObject) require all
+            // relationship faults invalidation.
+            ClassDescriptor descriptor = 
resolver.getClassDescriptor(id.getEntityName());
+            PropertyVisitor arcInvalidator = new PropertyVisitor() {
+
+                public boolean visitAttribute(AttributeProperty property) {
+                    return true;
+                }
+
+                public boolean visitToMany(ToManyProperty property) {
+                    property.invalidate(object);
+                    return true;
+                }
+
+                public boolean visitToOne(ToOneProperty property) {
+                    property.invalidate(object);
+                    return true;
+                }
+            };
+
+            descriptor.visitProperties(arcInvalidator);
+            object.setPersistenceState(PersistenceState.HOLLOW);
+            
+            // remove cached changes
+            graphManager.changeLog.unregisterNode(id);
+            graphManager.stateLog.unregisterNode(id);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/BootstrapMessage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/BootstrapMessage.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/BootstrapMessage.java
new file mode 100644
index 0000000..26f2f1b
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/BootstrapMessage.java
@@ -0,0 +1,36 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+/**
+ * A message sent to a remote service to request Cayenne mapping info.
+ * 
+ * @since 1.2
+ */
+public class BootstrapMessage implements ClientMessage {
+
+    /**
+     * Returns a description of the type of message. In this case always 
"Bootstrap".
+     */
+    @Override
+    public String toString() {
+        return "Bootstrap";
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/ClientMessage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/ClientMessage.java 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/ClientMessage.java
new file mode 100644
index 0000000..e0740b3
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/ClientMessage.java
@@ -0,0 +1,31 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+import java.io.Serializable;
+
+/**
+ * A tag interface representing a message sent by a remote client to Cayenne 
service.
+ * 
+ * @since 1.2
+ */
+public interface ClientMessage extends Serializable {
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalQuery.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalQuery.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalQuery.java
new file mode 100644
index 0000000..51c7946
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalQuery.java
@@ -0,0 +1,73 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.QueryMetadataProxy;
+import org.apache.cayenne.query.QueryRouter;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SQLActionVisitor;
+
+/**
+ * A client wrapper for the incremental query that overrides the metadata to 
ensure that
+ * query result is cached on the server, so that subranges could be retrieved 
at a later
+ * time.
+ * 
+ * @since 1.2
+ */
+class IncrementalQuery implements Query {
+
+    private Query query;
+    private String cacheKey;
+
+    IncrementalQuery(Query query, String cacheKey) {
+        this.query = query;
+        this.cacheKey = cacheKey;
+    }
+
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        final QueryMetadata metadata = query.getMetaData(resolver);
+
+        // the way paginated queries work on the server is that they are never 
cached
+        // (IncrementalFaultList interception happens before cache 
interception). So
+        // overriding caching settings in the metadata will only affect
+        // ClientServerChannel behavior
+        return new QueryMetadataProxy(metadata) {
+            public Query getOriginatingQuery() {
+                return null;
+            }
+
+            public String getCacheKey() {
+                return cacheKey;
+            }
+        };
+    }
+
+    public void route(QueryRouter router, EntityResolver resolver, Query 
substitutedQuery) {
+        query.route(router, resolver, substitutedQuery);
+    }
+
+    public SQLAction createSQLAction(SQLActionVisitor visitor) {
+        return query.createSQLAction(visitor);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
new file mode 100644
index 0000000..9b6f563
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
@@ -0,0 +1,300 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ResultBatchIterator;
+import org.apache.cayenne.ResultIterator;
+import org.apache.cayenne.ResultIteratorCallback;
+import org.apache.cayenne.access.IncrementalFaultList;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.QueryMetadataProxy;
+import org.apache.cayenne.query.QueryRouter;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SQLActionVisitor;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.SortOrder;
+import org.apache.cayenne.util.XMLEncoder;
+
+/**
+ * A SelectQuery decorator that overrides the metadata to ensure that query
+ * result is cached on the server, so that subranges could be retrieved at a
+ * later time. Note that a special decorator that is a subclass of SelectQuery
+ * is needed so that {@link IncrementalFaultList} on the server-side could 
apply
+ * SelectQuery-specific optimizations.
+ * 
+ * @since 3.0
+ */
+class IncrementalSelectQuery<T> extends SelectQuery<T> {
+
+       private SelectQuery<T> query;
+       private String cacheKey;
+
+       IncrementalSelectQuery(SelectQuery<T> delegate, String cacheKey) {
+               this.query = delegate;
+               this.cacheKey = cacheKey;
+       }
+
+       @Override
+       public QueryMetadata getMetaData(EntityResolver resolver) {
+               final QueryMetadata metadata = query.getMetaData(resolver);
+
+               // the way paginated queries work on the server is that they 
are never
+               // cached
+               // (IncrementalFaultList interception happens before cache
+               // interception). So
+               // overriding caching settings in the metadata will only affect
+               // ClientServerChannel behavior
+               return new QueryMetadataProxy(metadata) {
+                       public Query getOriginatingQuery() {
+                               return null;
+                       }
+
+                       public String getCacheKey() {
+                               return cacheKey;
+                       }
+               };
+       }
+
+       @Override
+       public void addOrdering(Ordering ordering) {
+               query.addOrdering(ordering);
+       }
+
+       @Override
+       public void addOrdering(String sortPathSpec, SortOrder order) {
+               query.addOrdering(sortPathSpec, order);
+       }
+
+       @Override
+       public void addOrderings(Collection<? extends Ordering> orderings) {
+               query.addOrderings(orderings);
+       }
+
+       @Override
+       public PrefetchTreeNode addPrefetch(String prefetchPath) {
+               return query.addPrefetch(prefetchPath);
+       }
+
+       @Override
+       public void andQualifier(Expression e) {
+               query.andQualifier(e);
+       }
+
+       @Override
+       public void clearOrderings() {
+               query.clearOrderings();
+       }
+
+       @Override
+       public void clearPrefetches() {
+               query.clearPrefetches();
+       }
+
+       @Override
+       public SelectQuery<T> createQuery(Map<String, ?> parameters) {
+               return query.createQuery(parameters);
+       }
+
+       @Override
+       public SQLAction createSQLAction(SQLActionVisitor visitor) {
+               return query.createSQLAction(visitor);
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               return query.equals(obj);
+       }
+
+       /**
+        * @since 4.0
+        */
+       @Override
+       public String getCacheGroup() {
+               return super.getCacheGroup();
+       }
+
+       @Override
+       public int getFetchLimit() {
+               return query.getFetchLimit();
+       }
+
+       @Override
+       public List<Ordering> getOrderings() {
+               return query.getOrderings();
+       }
+
+       @Override
+       public int getPageSize() {
+               return query.getPageSize();
+       }
+
+       @Override
+       public PrefetchTreeNode getPrefetchTree() {
+               return query.getPrefetchTree();
+       }
+
+       @Override
+       public Expression getQualifier() {
+               return query.getQualifier();
+       }
+
+       @Override
+       public Object getRoot() {
+               return query.getRoot();
+       }
+
+       @Override
+       public int hashCode() {
+               return query.hashCode();
+       }
+
+       @Override
+       public void initWithProperties(Map<String, ?> properties) {
+               query.initWithProperties(properties);
+       }
+
+       @Override
+       public boolean isDistinct() {
+               return query.isDistinct();
+       }
+
+       @Override
+       public boolean isFetchingDataRows() {
+               return query.isFetchingDataRows();
+       }
+
+       @Override
+       public void orQualifier(Expression e) {
+               query.orQualifier(e);
+       }
+
+       @Override
+       public SelectQuery<T> queryWithParameters(Map<String, ?> parameters, 
boolean pruneMissing) {
+               return query.queryWithParameters(parameters, pruneMissing);
+       }
+
+       @Override
+       public SelectQuery<T> queryWithParameters(Map<String, ?> parameters) {
+               return query.queryWithParameters(parameters);
+       }
+
+       @Override
+       public void removeOrdering(Ordering ordering) {
+               query.removeOrdering(ordering);
+       }
+
+       @Override
+       public void removePrefetch(String prefetchPath) {
+               query.removePrefetch(prefetchPath);
+       }
+
+       @Override
+       public void route(QueryRouter router, EntityResolver resolver, Query 
substitutedQuery) {
+               query.route(router, resolver, substitutedQuery);
+       }
+
+       /**
+        * @since 4.0
+        */
+       @Override
+       public void setCacheGroup(String cacheGroup) {
+               query.setCacheGroup(cacheGroup);
+       }
+
+       @Override
+       public void setDistinct(boolean distinct) {
+               query.setDistinct(distinct);
+       }
+
+       @SuppressWarnings("deprecation")
+       @Override
+       public void setFetchingDataRows(boolean flag) {
+               query.setFetchingDataRows(flag);
+       }
+
+       @Override
+       public void setFetchLimit(int fetchLimit) {
+               query.setFetchLimit(fetchLimit);
+       }
+
+       @Override
+       public void setPageSize(int pageSize) {
+               query.setPageSize(pageSize);
+       }
+
+       @Override
+       public void setPrefetchTree(PrefetchTreeNode prefetchTree) {
+               query.setPrefetchTree(prefetchTree);
+       }
+
+       @Override
+       public void setQualifier(Expression qualifier) {
+               query.setQualifier(qualifier);
+       }
+
+       @Override
+       public void setRoot(Object value) {
+               query.setRoot(value);
+       }
+
+       @Override
+       public String toString() {
+               return query.toString();
+       }
+
+       @Override
+       public List<T> select(ObjectContext context) {
+               return query.select(context);
+       }
+
+       @Override
+       public T selectOne(ObjectContext context) {
+               return query.selectOne(context);
+       }
+
+       @Override
+       public T selectFirst(ObjectContext context) {
+               return query.selectFirst(context);
+       }
+
+       @Override
+       public void iterate(ObjectContext context, ResultIteratorCallback<T> 
callback) {
+               query.iterate(context, callback);
+       }
+
+       @Override
+       public ResultIterator<T> iterator(ObjectContext context) {
+               return query.iterator(context);
+       }
+
+       @Override
+       public ResultBatchIterator<T> batchIterator(ObjectContext context, int 
size) {
+               return query.batchIterator(context, size);
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/QueryMessage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/QueryMessage.java 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/QueryMessage.java
new file mode 100644
index 0000000..3c8b4d5
--- /dev/null
+++ 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/QueryMessage.java
@@ -0,0 +1,55 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+import org.apache.cayenne.query.Query;
+
+/**
+ * A message passed to a DataChannel to request a query execution with result 
returned as
+ * QueryResponse.
+ * 
+ * @since 1.2
+ */
+public class QueryMessage implements ClientMessage {
+
+    protected Query query;
+
+    // for hessian serialization
+    @SuppressWarnings("unused")
+    private QueryMessage() {
+
+    }
+
+    public QueryMessage(Query query) {
+        this.query = query;
+    }
+
+    public Query getQuery() {
+        return query;
+    }
+
+    /**
+     * Returns a description of the type of message. In this case always 
"Query".
+     */
+    @Override
+    public String toString() {
+        return "Query";
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RangeQuery.java
----------------------------------------------------------------------
diff --git 
a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RangeQuery.java 
b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RangeQuery.java
new file mode 100644
index 0000000..f2b5e78
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RangeQuery.java
@@ -0,0 +1,156 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.remote;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.Procedure;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryCacheStrategy;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.QueryMetadataProxy;
+import org.apache.cayenne.query.QueryRouter;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SQLActionVisitor;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * A Query that fetches a range of objects from a previously fetched 
server-side paginated
+ * list. This query is client-only and can't be executed on the server.
+ * 
+ * @since 1.2
+ */
+class RangeQuery implements Query {
+
+    private String cacheKey;
+    private int fetchOffset;
+    private int fetchLimit;
+    private Query originatingQuery;
+
+    // exists for hessian serialization.
+    @SuppressWarnings("unused")
+    private RangeQuery() {
+
+    }
+
+    /**
+     * Creates a query that returns a single page from an existing cached 
server-side
+     * result list.
+     */
+    RangeQuery(String cacheKey, int fetchStartIndex, int fetchLimit,
+            Query originatingQuery) {
+        this.cacheKey = cacheKey;
+        this.fetchOffset = fetchStartIndex;
+        this.fetchLimit = fetchLimit;
+        this.originatingQuery = originatingQuery;
+    }
+
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        final QueryMetadata originatingMetadata = 
originatingQuery.getMetaData(resolver);
+
+        return new QueryMetadataProxy(originatingMetadata) {
+
+            public Query getOriginatingQuery() {
+                return originatingQuery;
+            }
+
+            public List<Object> getResultSetMapping() {
+                return null;
+            }
+
+            public boolean isSingleResultSetMapping() {
+                return false;
+            }
+
+            public String getCacheKey() {
+                return cacheKey;
+            }
+
+            public String getCacheGroup() {
+                return null;
+            }
+
+            public int getFetchOffset() {
+                return fetchOffset;
+            }
+
+            public int getFetchLimit() {
+                return fetchLimit;
+            }
+
+            public int getPageSize() {
+                return 0;
+            }
+
+            /**
+             * @since 3.0
+             */
+            public QueryCacheStrategy getCacheStrategy() {
+                return QueryCacheStrategy.getDefaultStrategy();
+            }
+
+            public DataMap getDataMap() {
+                throw new UnsupportedOperationException();
+            }
+
+            public DbEntity getDbEntity() {
+                throw new UnsupportedOperationException();
+            }
+
+            public ObjEntity getObjEntity() {
+                throw new UnsupportedOperationException();
+            }
+
+            public ClassDescriptor getClassDescriptor() {
+                throw new UnsupportedOperationException();
+            }
+
+            public Procedure getProcedure() {
+                throw new UnsupportedOperationException();
+            }
+
+            public Map<String, String> getPathSplitAliases() {
+                throw new UnsupportedOperationException();
+            }
+
+            public boolean isRefreshingObjects() {
+                throw new UnsupportedOperationException();
+            }
+
+            public int getStatementFetchSize() {
+                return 0;
+            }
+        };
+    }
+
+    public SQLAction createSQLAction(SQLActionVisitor visitor) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void route(QueryRouter router, EntityResolver resolver, Query 
substitutedQuery) {
+        throw new UnsupportedOperationException();
+    }
+
+}

Reply via email to