cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ntimof...@apache.org
Subject [5/5] cayenne git commit: 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
Date Tue, 12 Dec 2017 13:05:47 GMT
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 <stariy95@gmail.com>
Authored: Tue Dec 12 15:52:43 2017 +0300
Committer: Nikita Timofeev <stariy95@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();
+    }
+
+}


Mime
View raw message