You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/12/12 13:05:46 UTC

[4/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

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteIncrementalFaultList.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteIncrementalFaultList.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteIncrementalFaultList.java
new file mode 100644
index 0000000..7ed1004
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteIncrementalFaultList.java
@@ -0,0 +1,668 @@
+/*****************************************************************
+ *   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.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.util.IDUtil;
+import org.apache.cayenne.util.IncrementalListResponse;
+import org.apache.cayenne.util.Util;
+
+/**
+ * A list that serves as a container of Persistent objects. It is usually returned by an
+ * ObjectContext when a paginated query is performed. Initially only the first "page" of
+ * objects is fully resolved. Pages following the first page are resolved on demand. When
+ * a list element is accessed, the list would ensure that this element as well as all its
+ * siblings on the same page are fully resolved.
+ * <p>
+ * The list can hold DataRows or Persistent objects. Attempts to add any other object
+ * types will result in an exception.
+ * </p>
+ * <p>
+ * Certain operations like <code>toArray</code> would trigger full list fetch.
+ * </p>
+ * <p>
+ * Synchronization Note: this list is not synchronized. All access to it should follow
+ * synchronization rules applicable for ArrayList.
+ * </p>
+ * 
+ * @since 1.2
+ */
+public class RemoteIncrementalFaultList implements List {
+
+    static final Object PLACEHOLDER = new Object();
+
+    protected List elements;
+
+    protected String cacheKey;
+    protected int pageSize;
+    protected int unfetchedObjects;
+    protected Query paginatedQuery;
+
+    protected transient ObjectContext context;
+
+    /**
+     * Stores a hint allowing to distinguish data rows from unfetched ids when the query
+     * fetches data rows.
+     */
+    protected int rowWidth;
+
+    private ListHelper helper;
+
+    public RemoteIncrementalFaultList(ObjectContext context, Query paginatedQuery) {
+
+        QueryMetadata metadata = paginatedQuery.getMetaData(context.getEntityResolver());
+
+        if (metadata.getPageSize() <= 0) {
+            throw new IllegalArgumentException("Page size must be positive: "
+                    + metadata.getPageSize());
+        }
+
+        this.pageSize = metadata.getPageSize();
+        this.helper = (metadata.isFetchingDataRows())
+                ? new DataRowListHelper()
+                : new PersistentListHelper();
+        this.context = context;
+
+        // use provided cache key if possible; this would allow clients to
+        // address the same server-side list from multiple queries.
+        this.cacheKey = metadata.getCacheKey();
+        if (cacheKey == null) {
+            cacheKey = generateCacheKey();
+        }
+
+        Query query = paginatedQuery;
+
+        // always wrap a query in a Incremental*Query, to ensure cache key is
+        // client-generated (e.g. see CAY-1003 - client and server can be in different
+        // timezones, so the key can be messed up)
+
+        // there are some serious pagination optimizations for SelectQuery on the
+        // server-side, so use a special wrapper that is itself a subclass of
+        // SelectQuery
+        if (query instanceof SelectQuery) {
+            query = new IncrementalSelectQuery<Object>((SelectQuery<Object>) paginatedQuery, cacheKey);
+        }
+        else {
+            query = new IncrementalQuery(paginatedQuery, cacheKey);
+        }
+
+        // ensure that originating query is wrapped to include the right cache key....
+        this.paginatedQuery = query;
+
+        // select directly from the channel, bypassing the context. Otherwise our query
+        // wrapper can be intercepted incorrectly
+        QueryResponse response = context.getChannel().onQuery(context, query);
+
+        List firstPage = response.firstList();
+
+        // sanity check
+        if (firstPage.size() > pageSize) {
+            throw new IllegalArgumentException("Returned page size ("
+                    + firstPage.size()
+                    + ") exceeds requested page size ("
+                    + pageSize
+                    + ")");
+        }
+        // result is smaller than a page
+        else if (firstPage.size() < pageSize) {
+            this.elements = new ArrayList(firstPage);
+            unfetchedObjects = 0;
+        }
+        else {
+
+            if (response instanceof IncrementalListResponse) {
+                int fullListSize = ((IncrementalListResponse) response).getFullSize();
+
+                this.unfetchedObjects = fullListSize - firstPage.size();
+                this.elements = new ArrayList(fullListSize);
+                elements.addAll(firstPage);
+
+                // fill the rest with placeholder...
+                for (int i = pageSize; i < fullListSize; i++) {
+                    elements.add(PLACEHOLDER);
+                }
+            }
+            // this happens when full size equals page size
+            else {
+                this.elements = new ArrayList(firstPage);
+                unfetchedObjects = 0;
+            }
+        }
+    }
+
+    private String generateCacheKey() {
+        byte[] bytes = IDUtil.pseudoUniqueByteSequence8();
+        StringBuffer buffer = new StringBuffer(17);
+        buffer.append("I");
+        for (byte aByte : bytes) {
+            IDUtil.appendFormattedByte(buffer, aByte);
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Will resolve all unread objects.
+     */
+    public void resolveAll() {
+        resolveInterval(0, size());
+    }
+
+    /**
+     * @param object
+     * @return <code>true</code> if the object corresponds to an unresolved state and
+     *         does require a fetch before being returned to the user.
+     */
+    private boolean isUnresolved(Object object) {
+        return object == PLACEHOLDER;
+    }
+
+    /**
+     * Resolves a sublist of objects starting at <code>fromIndex</code> up to but not
+     * including <code>toIndex</code>. Internally performs bound checking and trims
+     * indexes accordingly.
+     */
+    protected void resolveInterval(int fromIndex, int toIndex) {
+        if (fromIndex >= toIndex || elements.isEmpty()) {
+            return;
+        }
+
+        if (context == null) {
+            throw new CayenneRuntimeException("No ObjectContext set, can't resolve objects.");
+        }
+
+        // bounds checking
+
+        if (fromIndex < 0) {
+            fromIndex = 0;
+        }
+
+        if (toIndex > elements.size()) {
+            toIndex = elements.size();
+        }
+
+        // find disjoint ranges and resolve them individually...
+
+        int fromPage = pageIndex(fromIndex);
+        int toPage = pageIndex(toIndex - 1);
+
+        int rangeStartIndex = -1;
+        for (int i = fromPage; i <= toPage; i++) {
+
+            int pageStartIndex = i * pageSize;
+            Object firstPageObject = elements.get(pageStartIndex);
+            if (isUnresolved(firstPageObject)) {
+
+                // start range
+                if (rangeStartIndex < 0) {
+                    rangeStartIndex = pageStartIndex;
+                }
+            }
+            else {
+
+                // finish range...
+                if (rangeStartIndex >= 0) {
+                    forceResolveInterval(rangeStartIndex, pageStartIndex);
+                    rangeStartIndex = -1;
+                }
+            }
+        }
+
+        // load last page
+        if (rangeStartIndex >= 0) {
+            forceResolveInterval(rangeStartIndex, toIndex);
+        }
+    }
+
+    void forceResolveInterval(int fromIndex, int toIndex) {
+
+        int pastEnd = toIndex - size();
+        if (pastEnd > 0) {
+            toIndex = size();
+        }
+
+        int fetchLimit = toIndex - fromIndex;
+
+        RangeQuery query = new RangeQuery(cacheKey, fromIndex, fetchLimit, paginatedQuery);
+
+        List sublist = context.performQuery(query);
+
+        // sanity check
+        if (sublist.size() != fetchLimit) {
+            throw new CayenneRuntimeException("Resolved range size %d is not the same as expected: %d"
+                    , sublist.size(), fetchLimit);
+        }
+
+        for (int i = 0; i < fetchLimit; i++) {
+            elements.set(fromIndex + i, sublist.get(i));
+        }
+
+        unfetchedObjects -= sublist.size();
+    }
+
+    /**
+     * Returns zero-based index of the virtual "page" for a given array element index.
+     */
+    int pageIndex(int elementIndex) {
+        if (elementIndex < 0 || elementIndex > size()) {
+            throw new IndexOutOfBoundsException("Index: " + elementIndex);
+        }
+
+        if (pageSize <= 0 || elementIndex < 0) {
+            return -1;
+        }
+
+        return elementIndex / pageSize;
+    }
+
+    /**
+     * Returns ObjectContext associated with this list.
+     */
+    public ObjectContext getContext() {
+        return context;
+    }
+
+    /**
+     * Returns the pageSize.
+     * 
+     * @return int
+     */
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    /**
+     * Returns a list iterator for this list. DataObjects are resolved a page (according
+     * to getPageSize()) at a time as necessary - when retrieved with next() or
+     * previous().
+     */
+    public ListIterator listIterator() {
+        return new ListIteratorHelper(0);
+    }
+
+    /**
+     * Returns a list iterator of the elements in this list (in proper sequence), starting
+     * at the specified position in this list. The specified index indicates the first
+     * element that would be returned by an initial call to the next method. An initial
+     * call to the previous method would return the element with the specified index minus
+     * one. DataObjects are resolved a page at a time (according to getPageSize()) as
+     * necessary - when retrieved with next() or previous().
+     */
+    public ListIterator listIterator(int index) {
+        if (index < 0 || index > size()) {
+            throw new IndexOutOfBoundsException("Index: " + index);
+        }
+
+        return new ListIteratorHelper(index);
+    }
+
+    /**
+     * Return an iterator for this list. DataObjects are resolved a page (according to
+     * getPageSize()) at a time as necessary - when retrieved with next().
+     */
+    public Iterator iterator() {
+        // by virtue of get(index)'s implementation, resolution of ids into
+        // objects will occur on pageSize boundaries as necessary.
+        return new Iterator() {
+
+            int listIndex = 0;
+
+            public boolean hasNext() {
+                return (listIndex < elements.size());
+            }
+
+            public Object next() {
+                if (listIndex >= elements.size())
+                    throw new NoSuchElementException("no more elements");
+
+                return get(listIndex++);
+            }
+
+            public void remove() {
+                throw new UnsupportedOperationException("remove not supported.");
+            }
+        };
+    }
+
+    /**
+     * @see java.util.List#add(int, Object)
+     */
+    public void add(int index, Object element) {
+        helper.validateListObject(element);
+        elements.add(index, element);
+
+    }
+
+    /**
+     * @see java.util.Collection#add(Object)
+     */
+    public boolean add(Object o) {
+        helper.validateListObject(o);
+        return elements.add(o);
+    }
+
+    /**
+     * @see java.util.Collection#addAll(Collection)
+     */
+    public boolean addAll(Collection c) {
+
+        return elements.addAll(c);
+
+    }
+
+    /**
+     * @see java.util.List#addAll(int, Collection)
+     */
+    public boolean addAll(int index, Collection c) {
+
+        return elements.addAll(index, c);
+
+    }
+
+    /**
+     * @see java.util.Collection#clear()
+     */
+    public void clear() {
+        elements.clear();
+    }
+
+    /**
+     * @see java.util.Collection#contains(Object)
+     */
+    public boolean contains(Object o) {
+        return indexOf(o) >= 0;
+    }
+
+    /**
+     * @see java.util.Collection#containsAll(Collection)
+     */
+    public boolean containsAll(Collection c) {
+        Iterator it = c.iterator();
+        while (it.hasNext()) {
+            if (!contains(it.next())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @see java.util.List#get(int)
+     */
+    public Object get(int index) {
+
+        Object o = elements.get(index);
+
+        if (isUnresolved(o)) {
+            // read this page
+            int pageStart = pageIndex(index) * pageSize;
+            resolveInterval(pageStart, pageStart + pageSize);
+
+            return elements.get(index);
+        }
+        else {
+            return o;
+        }
+
+    }
+
+    /**
+     * @see java.util.List#indexOf(Object)
+     */
+    public int indexOf(Object o) {
+        return helper.indexOfObject(o);
+    }
+
+    /**
+     * @see java.util.Collection#isEmpty()
+     */
+    public boolean isEmpty() {
+
+        return elements.isEmpty();
+
+    }
+
+    /**
+     * @see java.util.List#lastIndexOf(Object)
+     */
+    public int lastIndexOf(Object o) {
+        return helper.lastIndexOfObject(o);
+    }
+
+    /**
+     * @see java.util.List#remove(int)
+     */
+    public Object remove(int index) {
+
+        return elements.remove(index);
+
+    }
+
+    /**
+     * @see java.util.Collection#remove(Object)
+     */
+    public boolean remove(Object o) {
+
+        return elements.remove(o);
+
+    }
+
+    /**
+     * @see java.util.Collection#removeAll(Collection)
+     */
+    public boolean removeAll(Collection c) {
+
+        return elements.removeAll(c);
+
+    }
+
+    /**
+     * @see java.util.Collection#retainAll(Collection)
+     */
+    public boolean retainAll(Collection c) {
+
+        return elements.retainAll(c);
+
+    }
+
+    /**
+     * @see java.util.List#set(int, Object)
+     */
+    public Object set(int index, Object element) {
+        helper.validateListObject(element);
+
+        return elements.set(index, element);
+
+    }
+
+    /**
+     * @see java.util.Collection#size()
+     */
+    public int size() {
+        return elements.size();
+    }
+
+    public List subList(int fromIndex, int toIndex) {
+        resolveInterval(fromIndex, toIndex);
+        return elements.subList(fromIndex, toIndex);
+    }
+
+    public Object[] toArray() {
+        resolveAll();
+
+        return elements.toArray();
+    }
+
+    /**
+     * @see java.util.Collection#toArray(Object[])
+     */
+    public Object[] toArray(Object[] a) {
+        resolveAll();
+
+        return elements.toArray(a);
+    }
+
+    /**
+     * Returns a total number of objects that are not resolved yet.
+     */
+    public int getUnfetchedObjects() {
+        return unfetchedObjects;
+    }
+
+    abstract class ListHelper {
+
+        int indexOfObject(Object object) {
+            if (incorrectObjectType(object)) {
+                return -1;
+            }
+
+            for (int i = 0; i < elements.size(); i++) {
+
+                if (Util.nullSafeEquals(object, get(i))) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        int lastIndexOfObject(Object object) {
+            if (incorrectObjectType(object)) {
+                return -1;
+            }
+
+            for (int i = elements.size() - 1; i >= 0; i--) {
+                if (Util.nullSafeEquals(object, get(i))) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        abstract boolean incorrectObjectType(Object object);
+
+        void validateListObject(Object object) throws IllegalArgumentException {
+            if (incorrectObjectType(object)) {
+                throw new IllegalArgumentException("Can't store this object: " + object);
+            }
+        }
+    }
+
+    class PersistentListHelper extends ListHelper {
+
+        @Override
+        boolean incorrectObjectType(Object object) {
+            if (!(object instanceof Persistent)) {
+                return true;
+            }
+
+            Persistent persistent = (Persistent) object;
+            if (persistent.getObjectContext() != context) {
+                return true;
+            }
+
+            return false;
+        }
+
+    }
+
+    class DataRowListHelper extends ListHelper {
+
+        @Override
+        boolean incorrectObjectType(Object object) {
+            if (!(object instanceof Map)) {
+                return true;
+            }
+
+            Map map = (Map) object;
+            return map.size() != rowWidth;
+        }
+    }
+
+    class ListIteratorHelper implements ListIterator {
+
+        // by virtue of get(index)'s implementation, resolution of ids into
+        // objects will occur on pageSize boundaries as necessary.
+
+        int listIndex;
+
+        public ListIteratorHelper(int startIndex) {
+            this.listIndex = startIndex;
+        }
+
+        public void add(Object o) {
+            throw new UnsupportedOperationException("add operation not supported");
+        }
+
+        public boolean hasNext() {
+            return (listIndex < elements.size());
+        }
+
+        public boolean hasPrevious() {
+            return (listIndex > 0);
+        }
+
+        public Object next() {
+            if (listIndex >= elements.size())
+                throw new NoSuchElementException("at the end of the list");
+
+            return get(listIndex++);
+        }
+
+        public int nextIndex() {
+            return listIndex;
+        }
+
+        public Object previous() {
+            if (listIndex < 1)
+                throw new NoSuchElementException("at the beginning of the list");
+
+            return get(--listIndex);
+        }
+
+        public int previousIndex() {
+            return (listIndex - 1);
+        }
+
+        public void remove() {
+            throw new UnsupportedOperationException("remove operation not supported");
+        }
+
+        public void set(Object o) {
+            throw new UnsupportedOperationException("set operation not supported");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteService.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
new file mode 100644
index 0000000..f357846
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
@@ -0,0 +1,54 @@
+/*****************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.rmi.Remote;
+import java.rmi.RemoteException;
+
+/**
+ * Interface of a Cayenne remote service.
+ *
+ * @since 1.2
+ * @see org.apache.cayenne.rop.ROPServlet
+ */
+public interface RemoteService extends Remote {
+
+    /**
+     * Establishes a dedicated session with Cayenne DataChannel, returning session id.
+     */
+    RemoteSession establishSession() throws RemoteException;
+
+    /**
+     * Creates a new session with the specified or joins an existing one. This method is
+     * used to bootstrap collaborating clients of a single "group chat".
+     */
+    RemoteSession establishSharedSession(String name) throws RemoteException;
+
+    /**
+     * Processes message on a remote server, returning the result of such processing.
+     */
+    Object processMessage(ClientMessage message) throws RemoteException, Throwable;
+
+    /**
+     * Close remote service resources.
+     * @sine 4.0
+     */
+    void close() throws RemoteException;
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteSession.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteSession.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteSession.java
new file mode 100644
index 0000000..461e79d
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/RemoteSession.java
@@ -0,0 +1,145 @@
+/*****************************************************************
+ *   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;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.event.EventBridge;
+import org.apache.cayenne.event.EventBridgeFactory;
+import org.apache.cayenne.event.EventSubject;
+import org.apache.cayenne.util.HashCodeBuilder;
+import org.apache.cayenne.util.ToStringBuilder;
+
+/**
+ * A descriptor used by default service implementation to pass session parameters to the
+ * client. It provides the client with details on how to invoke the service and how to
+ * listen for the server events.
+ * 
+ * @since 1.2
+ */
+public class RemoteSession implements Serializable {
+
+    static final Collection<EventSubject> SUBJECTS = Arrays.asList(
+            DataChannel.GRAPH_CHANGED_SUBJECT,
+            DataChannel.GRAPH_FLUSHED_SUBJECT,
+            DataChannel.GRAPH_ROLLEDBACK_SUBJECT);
+
+    protected String name;
+    protected String sessionId;
+
+    protected String eventBridgeFactory;
+    protected Map<String, String> eventBridgeParameters;
+
+    // private constructor used by hessian deserialization mechanism
+    @SuppressWarnings("unused")
+    private RemoteSession() {
+
+    }
+
+    /**
+     * Creates a HessianServiceDescriptor without server events support.
+     */
+    public RemoteSession(String sessionId) {
+        this(sessionId, null, null);
+    }
+
+    /**
+     * Creates a HessianServiceDescriptor. If <code>eventBridgeFactory</code> argument
+     * is not null, session will support server events.
+     */
+    public RemoteSession(String sessionId, String eventBridgeFactory,
+            Map<String, String> eventBridgeParameters) {
+
+        if (sessionId == null) {
+            throw new IllegalArgumentException("Null sessionId");
+        }
+
+        this.sessionId = sessionId;
+        this.eventBridgeFactory = eventBridgeFactory;
+        this.eventBridgeParameters = eventBridgeParameters;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder(71, 5).append(sessionId).toHashCode();
+    }
+
+    /**
+     * Returns server session id. This is often the same as HttpSession id.
+     */
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    /**
+     * Returns session group name. Group name is used for shared sessions.
+     */
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public boolean isServerEventsEnabled() {
+        return eventBridgeFactory != null;
+    }
+
+    @Override
+    public String toString() {
+        ToStringBuilder builder = new ToStringBuilder(this)
+                .append("sessionId", sessionId);
+
+        if (eventBridgeFactory != null) {
+            builder.append("eventBridgeFactory", eventBridgeFactory);
+        }
+
+        if (name != null) {
+            builder.append("name", name);
+        }
+
+        return builder.toString();
+    }
+
+    public static Collection<EventSubject> getSubjects() {
+        return SUBJECTS;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public String getEventBridgeFactory() {
+        return eventBridgeFactory;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public Map<String, String> getEventBridgeParameters() {
+        return eventBridgeParameters != null ? eventBridgeParameters : Collections.<String, String> emptyMap();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/SyncMessage.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/SyncMessage.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/SyncMessage.java
new file mode 100644
index 0000000..d8d5f37
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/SyncMessage.java
@@ -0,0 +1,91 @@
+/*****************************************************************
+ *   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.DataChannel;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.graph.GraphDiff;
+
+/**
+ * A message used for synchronization of the child with parent. It defines a few types of
+ * synchronization: "flush" (when the child sends its changes without a commit), "commit"
+ * (cascading flush with ultimate commit to the database), and "rollback" - cascading
+ * reverting of all uncommitted changes.
+ * 
+ * @since 1.2
+ */
+public class SyncMessage implements ClientMessage {
+
+    protected transient ObjectContext source;
+    protected int type;
+    protected GraphDiff senderChanges;
+
+    // private constructor for Hessian deserialization
+    @SuppressWarnings("unused")
+    private SyncMessage() {
+
+    }
+
+    public SyncMessage(ObjectContext source, int syncType, GraphDiff senderChanges) {
+        // validate type
+        if (syncType != DataChannel.FLUSH_NOCASCADE_SYNC
+                && syncType != DataChannel.FLUSH_CASCADE_SYNC
+                && syncType != DataChannel.ROLLBACK_CASCADE_SYNC) {
+            throw new IllegalArgumentException("'type' is invalid: " + syncType);
+        }
+
+        this.source = source;
+        this.type = syncType;
+        this.senderChanges = senderChanges;
+    }
+
+    /**
+     * Returns a source of SyncMessage.
+     */
+    public ObjectContext getSource() {
+        return source;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public GraphDiff getSenderChanges() {
+        return senderChanges;
+    }
+
+    /**
+     * Returns a description of the type of message.
+     * Possibilities are "flush-sync", "flush-cascade-sync", "rollback-cascade-sync" or "unknown-sync".
+     */
+    @Override
+    public String toString() {
+        switch (type) {
+            case DataChannel.FLUSH_NOCASCADE_SYNC:
+                return "flush-sync";
+            case DataChannel.FLUSH_CASCADE_SYNC:
+                return "flush-cascade-sync";
+            case DataChannel.ROLLBACK_CASCADE_SYNC:
+                return "rollback-cascade-sync";
+            default:
+                return "unknown-sync";
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/CayenneSerializerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/CayenneSerializerFactory.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/CayenneSerializerFactory.java
new file mode 100644
index 0000000..2357b2e
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/CayenneSerializerFactory.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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.hessian;
+
+import com.caucho.hessian.io.AbstractSerializerFactory;
+import com.caucho.hessian.io.Deserializer;
+import com.caucho.hessian.io.HessianProtocolException;
+
+// This class is an ugly workaround for Hessian 4 bug with not loading custom deserializers.
+// TODO: once it is fixed in Hessian, remove this class
+class CayenneSerializerFactory extends com.caucho.hessian.io.SerializerFactory {
+    @Override
+    public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
+        for (int i = 0; _factories != null && i < _factories.size(); i++) {
+            AbstractSerializerFactory factory;
+            factory = (AbstractSerializerFactory) _factories.get(i);
+
+            Deserializer deserializer = factory.getDeserializer(cl);
+            if (deserializer != null) {
+                return deserializer;
+            }
+        }
+        
+        return super.getDeserializer(cl);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
new file mode 100644
index 0000000..550ef09
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
@@ -0,0 +1,114 @@
+/*****************************************************************
+ *   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.hessian;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.util.Util;
+
+import com.caucho.hessian.io.AbstractSerializerFactory;
+import com.caucho.hessian.io.SerializerFactory;
+
+/**
+ * A utility class that configures Hessian serialization properties using reflection.
+ * 
+ * @since 1.2
+ */
+public class HessianConfig {
+
+    /**
+     * Creates a Hessian SerializerFactory configured with zero or more
+     * AbstractSerializerFactory extensions. Extensions are specified as class names. This
+     * method can inject EntityResolver if an extension factory class defines
+     * <em>setEntityResolver(EntityResolver)</em> method.
+     * 
+     * @param factoryNames an array of factory class names. Each class must be a concrete
+     *            subclass of <em>com.caucho.hessian.io.AbstractSerializerFactory</em>
+     *            and have a default constructor.
+     * @param resolver if not null, EntityResolver will be injected into all factories
+     *            that implement <em>setEntityResolver(EntityResolver)</em> method.
+     */
+    public static SerializerFactory createFactory(
+            String[] factoryNames,
+            EntityResolver resolver) {
+
+        SerializerFactory factory = new CayenneSerializerFactory();
+
+        if (factoryNames != null && factoryNames.length > 0) {
+
+            for (String factoryName : factoryNames) {
+
+                try {
+                    factory.addFactory(loadFactory(factoryName, resolver));
+                }
+                catch (Exception e) {
+                    throw new CayenneRuntimeException("Error configuring factory class "
+                            + factoryName, e);
+                }
+            }
+        }
+
+        return factory;
+    }
+
+    static AbstractSerializerFactory loadFactory(
+            String factoryName,
+            EntityResolver resolver) throws Exception {
+
+        ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        Class factoryClass = Class.forName(factoryName, true, loader);
+
+        if (!AbstractSerializerFactory.class.isAssignableFrom(factoryClass)) {
+            throw new IllegalArgumentException(factoryClass
+                    + " is not a AbstractSerializerFactory");
+        }
+
+        Constructor c = factoryClass.getDeclaredConstructor();
+        if (!Util.isAccessible(c)) {
+            c.setAccessible(true);
+        }
+
+        AbstractSerializerFactory object = (AbstractSerializerFactory) c.newInstance();
+
+        if (resolver != null) {
+            try {
+
+                Method setter = factoryClass.getDeclaredMethod(
+                        "setEntityResolver",
+                        EntityResolver.class);
+
+                if (!Util.isAccessible(setter)) {
+                    setter.setAccessible(true);
+                }
+
+                setter.invoke(object, resolver);
+            }
+            catch (Exception e) {
+                // ignore injection exception
+            }
+        }
+
+        return object;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/HessianService.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/HessianService.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/HessianService.java
new file mode 100644
index 0000000..5409e43
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/HessianService.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   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.hessian.service;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.ObjectContextFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.remote.service.HttpRemoteService;
+
+import com.caucho.services.server.ServiceContext;
+
+/**
+ * An implementation of RemoteService for work within Caucho Hessian environment.
+ * 
+ * @since 3.1 the service API is reworked to initialize via Cayenne DI.
+ */
+public class HessianService extends HttpRemoteService {
+
+    public static final String[] SERVER_SERIALIZER_FACTORIES = new String[] {
+        ServerSerializerFactory.class.getName()
+    };
+
+    /**
+     * @since 3.1
+     */
+    public HessianService(@Inject ObjectContextFactory contextFactory,
+            @Inject(Constants.SERVER_ROP_EVENT_BRIDGE_PROPERTIES_MAP) Map<String, String> eventBridgeProperties) {
+        super(contextFactory, eventBridgeProperties);
+    }
+
+    @Override
+    protected HttpSession getSession(boolean create) {
+        HttpServletRequest request = (HttpServletRequest) ServiceContext
+                .getContextRequest();
+        if (request == null) {
+            throw new IllegalStateException(
+                    "Attempt to access HttpSession outside the request scope.");
+        }
+
+        return request.getSession(create);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerDataRowSerializer.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerDataRowSerializer.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerDataRowSerializer.java
new file mode 100644
index 0000000..bcd9f52
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerDataRowSerializer.java
@@ -0,0 +1,56 @@
+/*****************************************************************
+ *   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.hessian.service;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.cayenne.DataRow;
+
+import com.caucho.hessian.io.AbstractHessianOutput;
+import com.caucho.hessian.io.AbstractSerializer;
+
+/**
+ * A server-side DataRow Hessian serializer.
+ */
+class ServerDataRowSerializer extends AbstractSerializer {
+
+    @Override
+    public void writeObject(Object object, AbstractHessianOutput out) throws IOException {
+        if (out.addRef(object)) {
+            return;
+        }
+
+        DataRow row = (DataRow) object;
+
+        out.writeMapBegin(DataRow.class.getName());
+        
+        out.writeInt(row.size());
+        out.writeLong(row.getVersion());
+        out.writeLong(row.getReplacesVersion());
+
+        for (final Map.Entry<String, Object> entry : row.entrySet()) {
+            out.writeObject(entry.getKey());
+            out.writeObject(entry.getValue());
+        }
+
+        out.writeMapEnd();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerPersistentObjectListSerializer.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerPersistentObjectListSerializer.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerPersistentObjectListSerializer.java
new file mode 100644
index 0000000..cf31ef0
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerPersistentObjectListSerializer.java
@@ -0,0 +1,50 @@
+/*****************************************************************
+ *   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.hessian.service;
+
+import java.io.IOException;
+
+import org.apache.cayenne.util.PersistentObjectList;
+
+import com.caucho.hessian.io.AbstractHessianOutput;
+import com.caucho.hessian.io.JavaSerializer;
+
+/**
+ * Serializer for PersistentObjectLists.
+ * 
+ * @since 1.2
+ */
+class ServerPersistentObjectListSerializer extends JavaSerializer {
+
+    ServerPersistentObjectListSerializer() {
+        super(PersistentObjectList.class);
+    }
+
+    @Override
+    public void writeObject(Object object, AbstractHessianOutput out) throws IOException {
+        PersistentObjectList list = (PersistentObjectList) object;
+        if (list.isFault()) {
+            out.writeNull();
+        }
+        else {
+            super.writeObject(object, out);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerSerializerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerSerializerFactory.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerSerializerFactory.java
new file mode 100644
index 0000000..3310cee
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/service/ServerSerializerFactory.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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.hessian.service;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.util.PersistentObjectList;
+import org.apache.cayenne.util.PersistentObjectMap;
+
+import com.caucho.hessian.io.AbstractSerializerFactory;
+import com.caucho.hessian.io.Deserializer;
+import com.caucho.hessian.io.HessianProtocolException;
+import com.caucho.hessian.io.JavaSerializer;
+import com.caucho.hessian.io.Serializer;
+
+/**
+ * An object that manages all custom (de)serializers used on the server.
+ * 
+ * @since 1.2
+ */
+public class ServerSerializerFactory extends AbstractSerializerFactory {
+    private ServerPersistentObjectListSerializer persistentObjectListSerializer;
+    private ServerDataRowSerializer dataRowSerilaizer;
+    private Serializer javaSerializer;
+
+    ServerSerializerFactory() {
+        this.persistentObjectListSerializer = new ServerPersistentObjectListSerializer();
+        this.dataRowSerilaizer = new ServerDataRowSerializer();
+    }
+
+    @Override
+    public Serializer getSerializer(Class cl) throws HessianProtocolException {
+
+        if (PersistentObjectList.class.isAssignableFrom(cl)) {
+            return persistentObjectListSerializer;
+        }
+        else if (DataRow.class.isAssignableFrom(cl)) {
+            return dataRowSerilaizer;
+        }
+        //turns out Hessian uses its own (incorrect) serialization mechanism for maps
+        else if (PersistentObjectMap.class.isAssignableFrom(cl)) {
+            if (javaSerializer == null) {
+                javaSerializer = new JavaSerializer(cl);
+            }
+            return javaSerializer;
+        }
+
+        return null;
+    }
+
+    @Override
+    public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
new file mode 100644
index 0000000..68fd89d
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
@@ -0,0 +1,199 @@
+/*****************************************************************
+ *   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.service;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.access.ClientServerChannel;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.ObjectContextFactory;
+import org.apache.cayenne.remote.ClientMessage;
+import org.apache.cayenne.remote.RemoteService;
+import org.apache.cayenne.remote.RemoteSession;
+import org.apache.cayenne.util.Util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.rmi.RemoteException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A generic implementation of an RemoteService. Can be subclassed to work with
+ * different remoting mechanisms, such as Hessian or JAXRPC.
+ * 
+ * @since 1.2
+ */
+public abstract class BaseRemoteService implements RemoteService {
+
+	// keep logger non-static so that it could be garbage collected with this
+	// instance.
+	protected final Logger logger;
+
+	protected ObjectContextFactory contextFactory;
+	protected String eventBridgeFactoryName;
+	protected Map<String, String> eventBridgeParameters;
+
+	/**
+	 * @since 3.1
+	 */
+	public BaseRemoteService(ObjectContextFactory contextFactory, Map<String, String> eventBridgeProperties) {
+
+		logger = LoggerFactory.getLogger(getClass());
+
+		// start Cayenne service
+		logger.debug("ROP service is starting");
+
+		this.contextFactory = contextFactory;
+		initEventBridgeParameters(eventBridgeProperties);
+
+		logger.debug(getClass().getName() + " started");
+	}
+
+	public String getEventBridgeFactoryName() {
+		return eventBridgeFactoryName;
+	}
+
+	public Map<String, String> getEventBridgeParameters() {
+		return eventBridgeParameters != null ? Collections.unmodifiableMap(eventBridgeParameters)
+				: Collections.EMPTY_MAP;
+	}
+
+	/**
+	 * Creates a new ServerSession with a dedicated DataChannel.
+	 */
+	protected abstract ServerSession createServerSession();
+
+	/**
+	 * Creates a new ServerSession based on a shared DataChannel.
+	 * 
+	 * @param name
+	 *            shared session name used to lookup a shared DataChannel.
+	 */
+	protected abstract ServerSession createServerSession(String name);
+
+	/**
+	 * Returns a ServerSession object that represents Cayenne-related state
+	 * associated with the current session. If ServerSession hasn't been
+	 * previously saved, returns null.
+	 */
+	protected abstract ServerSession getServerSession();
+
+	@Override
+	public RemoteSession establishSession() {
+		logger.debug("Session requested by client");
+
+		RemoteSession session = createServerSession().getSession();
+
+		logger.debug("Established client session: " + session);
+		return session;
+	}
+
+	@Override
+	public RemoteSession establishSharedSession(String name) {
+		logger.debug("Shared session requested by client. Group name: " + name);
+
+		if (name == null) {
+			throw new CayenneRuntimeException("Invalid null shared session name");
+		}
+
+		return createServerSession(name).getSession();
+	}
+
+	@Override
+	public Object processMessage(ClientMessage message) throws Throwable {
+
+		if (message == null) {
+			throw new IllegalArgumentException("Null client message.");
+		}
+
+		ServerSession handler = getServerSession();
+
+		if (handler == null) {
+			throw new MissingSessionException("No session associated with request.");
+		}
+
+		logger.debug("processMessage, sessionId: " + handler.getSession().getSessionId());
+
+		// intercept and log exceptions
+		try {
+			return DispatchHelper.dispatch(handler.getChannel(), message);
+		} catch (Throwable th) {
+
+			StringBuilder wrapperMessage = new StringBuilder();
+			wrapperMessage.append("Exception processing message ").append(message.getClass().getName())
+					.append(" of type ").append(message);
+
+			String wrapperMessageString = wrapperMessage.toString();
+			logger.info(wrapperMessageString, th);
+
+			// This exception will probably be propagated to the client.
+			// Recast the exception to a serializable form.
+			Exception cause = new Exception(Util.unwindException(th).getLocalizedMessage());
+
+			throw new CayenneRuntimeException(wrapperMessageString, cause);
+		}
+	}
+
+	@Override
+	public void close() throws RemoteException {
+	}
+
+	protected RemoteSession createRemoteSession(String sessionId, String name, boolean enableEvents) {
+		RemoteSession session = (enableEvents) ? new RemoteSession(sessionId, eventBridgeFactoryName,
+				eventBridgeParameters) : new RemoteSession(sessionId);
+
+		session.setName(name);
+		return session;
+	}
+
+	/**
+	 * Creates a server-side channel that will handle all client requests. For
+	 * shared sessions the same channel instance is reused for the entire group
+	 * of clients. For dedicated sessions, one channel per client is created.
+	 * <p>
+	 * This implementation returns {@link ClientServerChannel} instance wrapping
+	 * a DataContext. Subclasses may override the method to customize channel
+	 * creation. For instance they may wrap channel in the custom interceptors
+	 * to handle transactions or security.
+	 */
+	protected DataChannel createChannel() {
+		return new ClientServerChannel((DataContext) contextFactory.createContext());
+	}
+
+	/**
+	 * Initializes EventBridge parameters for remote clients peer-to-peer
+	 * communications.
+	 */
+	protected void initEventBridgeParameters(Map<String, String> properties) {
+		String eventBridgeFactoryName = properties.get(Constants.SERVER_ROP_EVENT_BRIDGE_FACTORY_PROPERTY);
+
+		if (eventBridgeFactoryName != null) {
+
+			Map<String, String> eventBridgeParameters = new HashMap<>(properties);
+			eventBridgeParameters.remove(Constants.SERVER_ROP_EVENT_BRIDGE_FACTORY_PROPERTY);
+
+			this.eventBridgeFactoryName = eventBridgeFactoryName;
+			this.eventBridgeParameters = eventBridgeParameters;
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/DispatchHelper.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/DispatchHelper.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/DispatchHelper.java
new file mode 100644
index 0000000..83b23e2
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/DispatchHelper.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   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.service;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.remote.BootstrapMessage;
+import org.apache.cayenne.remote.ClientMessage;
+import org.apache.cayenne.remote.QueryMessage;
+import org.apache.cayenne.remote.SyncMessage;
+
+/**
+ * A helper class to match message types with DataChannel methods.
+ * 
+ * @since 1.2
+ */
+class DispatchHelper {
+
+    static Object dispatch(DataChannel channel, ClientMessage message) {
+        // do most common messages first...
+        if (message instanceof QueryMessage) {
+            return channel.onQuery(null, ((QueryMessage) message).getQuery());
+        } else if (message instanceof SyncMessage) {
+            SyncMessage sync = (SyncMessage) message;
+            return channel.onSync(null, sync.getSenderChanges(), sync.getType());
+        } else if (message instanceof BootstrapMessage) {
+            return channel.getEntityResolver().getClientEntityResolver();
+        } else {
+            throw new CayenneRuntimeException("Message dispatch error. Unsupported message: %s", message);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/HttpRemoteService.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/HttpRemoteService.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/HttpRemoteService.java
new file mode 100644
index 0000000..80bd9bd
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/HttpRemoteService.java
@@ -0,0 +1,134 @@
+/*****************************************************************
+ *   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.service;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpSession;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.configuration.ObjectContextFactory;
+import org.apache.cayenne.remote.RemoteSession;
+
+/**
+ * A {@link org.apache.cayenne.remote.RemoteService} implementation that stores server
+ * context information in HTTP sessions.
+ * 
+ * @since 1.2
+ */
+public abstract class HttpRemoteService extends BaseRemoteService {
+
+    static final String SESSION_ATTRIBUTE = HttpRemoteService.class.getName()
+            + ".ServerSession";
+
+    private Map<String, WeakReference<DataChannel>> sharedChannels;
+
+    /**
+     * @since 3.1
+     */
+    public HttpRemoteService(ObjectContextFactory contextFactory,
+            Map<String, String> eventBridgeProperties) {
+        super(contextFactory, eventBridgeProperties);
+        this.sharedChannels = new HashMap<>();
+    }
+
+    /**
+     * Returns an HttpSession associated with the current request in progress.
+     */
+    protected abstract HttpSession getSession(boolean create);
+
+    /**
+     * Returns a ServerSession object that represents Cayenne-related state associated
+     * with the current session. If ServerSession hasn't been previously saved, returns
+     * null.
+     */
+    @Override
+    protected ServerSession getServerSession() {
+        HttpSession httpSession = getSession(true);
+        return (ServerSession) httpSession.getAttribute(SESSION_ATTRIBUTE);
+    }
+
+    /**
+     * Creates a new ServerSession with a dedicated DataChannel. Returned ServerSession is
+     * stored in HttpSession for future reuse.
+     */
+    @Override
+    protected ServerSession createServerSession() {
+
+        HttpSession httpSession = getSession(true);
+
+        DataChannel channel = createChannel();
+        RemoteSession remoteSession = createRemoteSession(
+                httpSession.getId(),
+                null,
+                false);
+        ServerSession serverSession = new ServerSession(remoteSession, channel);
+
+        httpSession.setAttribute(SESSION_ATTRIBUTE, serverSession);
+        return serverSession;
+    }
+
+    /**
+     * Creates a new ServerSession based on a shared DataChannel. Returned ServerSession
+     * is stored in HttpSession for future reuse.
+     * 
+     * @param name shared session name used to lookup a shared DataChannel.
+     */
+    @Override
+    protected ServerSession createServerSession(String name) {
+        if (name == null) {
+            throw new IllegalArgumentException("Name is null for shared session.");
+        }
+
+        HttpSession httpSession = getSession(true);
+        DataChannel channel;
+
+        synchronized (sharedChannels) {
+            channel = getSharedChannel(name);
+            if (channel == null) {
+                channel = createChannel();
+                saveSharedChannel(name, channel);
+                logger.debug("Starting a new shared channel: " + name);
+            }
+            else {
+                logger.debug("Joining existing shared channel: " + name);
+            }
+        }
+
+        RemoteSession remoteSession = createRemoteSession(httpSession.getId(), name, true);
+
+        ServerSession serverSession = new ServerSession(remoteSession, channel);
+        httpSession.setAttribute(SESSION_ATTRIBUTE, serverSession);
+        return serverSession;
+    }
+
+    protected DataChannel getSharedChannel(String name) {
+        WeakReference<DataChannel> ref = sharedChannels.get(name);
+        return (ref != null) ? ref.get() : null;
+    }
+
+    protected void saveSharedChannel(String name, DataChannel channel) {
+        // wrap value in a WeakReference so that channels can be deallocated when all
+        // sessions that reference this channel time out...
+        sharedChannels.put(name, new WeakReference<DataChannel>(channel));
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/MissingSessionException.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/MissingSessionException.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/MissingSessionException.java
new file mode 100644
index 0000000..e46e50b
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/MissingSessionException.java
@@ -0,0 +1,38 @@
+/*****************************************************************
+ *   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.service;
+
+import org.apache.cayenne.CayenneRuntimeException;
+
+/**
+ * An exception that are thrown by the ROP server if the client are missing a session.
+ * 
+ * @since 3.0
+ */
+public class MissingSessionException extends CayenneRuntimeException {
+
+    public MissingSessionException() {
+        super();
+    }
+
+    public MissingSessionException(String messageFormat, Object... messageArgs) {
+        super(messageFormat, messageArgs);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/ServerSession.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/ServerSession.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/ServerSession.java
new file mode 100644
index 0000000..2ae8904
--- /dev/null
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/service/ServerSession.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   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.service;
+
+import java.io.Serializable;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.remote.RemoteSession;
+
+/**
+ * An object that stores server side objects for the client session.
+ * 
+ * @since 1.2
+ */
+public class ServerSession implements Serializable {
+
+    protected RemoteSession session;
+    protected DataChannel channel;
+
+    public ServerSession(RemoteSession session, DataChannel channel) {
+        this.session = session;
+        this.channel = channel;
+    }
+
+    public DataChannel getChannel() {
+        return channel;
+    }
+
+    public RemoteSession getSession() {
+        return session;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
new file mode 100644
index 0000000..1adfb52
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
@@ -0,0 +1,67 @@
+/*****************************************************************
+ *   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.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @since 4.0
+ */
+public class CayenneContextGraphManagerTest {
+
+    private CayenneContextGraphManager graphManager;
+
+    @Before
+    public void before() {
+        CayenneContext mockContext = mock(CayenneContext.class);
+        this.graphManager = new CayenneContextGraphManager(mockContext, false, false);
+    }
+
+    @Test
+    public void testRegisterNode() {
+
+        ObjectId id = new ObjectId("E1", "ID", 500);
+        Persistent object = mock(Persistent.class);
+
+        graphManager.registerNode(id, object);
+        assertSame(object, graphManager.getNode(id));
+    }
+
+    @Test
+    public void testUnregisterNode() {
+
+        ObjectId id = new ObjectId("E1", "ID", 500);
+        Persistent object = mock(Persistent.class);
+
+        graphManager.registerNode(id, object);
+        Object unregistered = graphManager.unregisterNode(id);
+        assertSame(object, unregistered);
+
+        verify(object, times(0)).setObjectId(null);
+        verify(object).setObjectContext(null);
+        verify(object).setPersistenceState(PersistenceState.TRANSIENT);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
new file mode 100644
index 0000000..f1ecab2
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.rmi.RemoteException;
+
+public class MockRemoteService implements RemoteService {
+
+    public RemoteSession establishSession() throws RemoteException {
+        return null;
+    }
+
+    public RemoteSession establishSharedSession(String name) throws RemoteException {
+        return null;
+    }
+
+    public Object processMessage(ClientMessage message) throws RemoteException, Throwable {
+        return null;
+    }
+
+    @Override
+    public void close() throws RemoteException {
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/RemoteSessionTest.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/RemoteSessionTest.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/RemoteSessionTest.java
new file mode 100644
index 0000000..869e110
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/RemoteSessionTest.java
@@ -0,0 +1,58 @@
+/*****************************************************************
+ *   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.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class RemoteSessionTest {
+
+    @Test
+    public void testConstructor1() {
+        RemoteSession descriptor = new RemoteSession("abc");
+        assertEquals("abc", descriptor.getSessionId());
+        assertFalse(descriptor.isServerEventsEnabled());
+    }
+
+    @Test
+    public void testConstructor2() {
+        RemoteSession descriptor = new RemoteSession("abc", "factory", null);
+        assertEquals("abc", descriptor.getSessionId());
+        assertTrue(descriptor.isServerEventsEnabled());
+    }
+
+    @Test
+    public void testHashCode() {
+        RemoteSession d1 = new RemoteSession("1");
+        RemoteSession d2 = new RemoteSession("1");
+
+        assertEquals(d1.hashCode(), d1.hashCode());
+        assertEquals(d1.hashCode(), d2.hashCode());
+
+        d2.setName("some name");
+        assertEquals(d1.hashCode(), d2.hashCode());
+
+        RemoteSession d3 = new RemoteSession("2");
+        assertFalse(d1.hashCode() == d3.hashCode());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/HessianConfigTest.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/HessianConfigTest.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/HessianConfigTest.java
new file mode 100644
index 0000000..4cf0d0b
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/HessianConfigTest.java
@@ -0,0 +1,61 @@
+/*****************************************************************
+ *   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.hessian;
+
+import com.caucho.hessian.io.AbstractSerializerFactory;
+import com.caucho.hessian.io.SerializerFactory;
+import org.apache.cayenne.map.EntityResolver;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class HessianConfigTest {
+
+    @Test
+    public void testLoadFactoryNoExtensions() {
+        SerializerFactory factory = HessianConfig.createFactory(null, null);
+        assertNotNull(factory);
+    }
+
+    @Test
+    public void testLoadFactoryNoInjection() throws Exception {
+        AbstractSerializerFactory factory = HessianConfig.loadFactory(
+                MockAbstractSerializerFactory.class.getName(),
+                null);
+
+        assertTrue(factory instanceof MockAbstractSerializerFactory);
+        assertNull(((MockAbstractSerializerFactory) factory).getEntityResolver());
+    }
+
+    @Test
+    public void testLoadFactoryInjection() throws Exception {
+        EntityResolver resolver = new EntityResolver();
+        AbstractSerializerFactory factory = HessianConfig.loadFactory(
+                MockAbstractSerializerFactory.class.getName(),
+                resolver);
+
+        assertTrue(factory instanceof MockAbstractSerializerFactory);
+        assertSame(resolver, ((MockAbstractSerializerFactory) factory)
+                .getEntityResolver());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/MockAbstractSerializerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/MockAbstractSerializerFactory.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/MockAbstractSerializerFactory.java
new file mode 100644
index 0000000..8e861ab
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/MockAbstractSerializerFactory.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.hessian;
+
+import org.apache.cayenne.map.EntityResolver;
+
+import com.caucho.hessian.io.AbstractSerializerFactory;
+import com.caucho.hessian.io.Deserializer;
+import com.caucho.hessian.io.HessianProtocolException;
+import com.caucho.hessian.io.Serializer;
+
+public class MockAbstractSerializerFactory extends AbstractSerializerFactory {
+
+    protected EntityResolver entityResolver;
+
+    @Override
+    public Serializer getSerializer(Class cl) throws HessianProtocolException {
+        return null;
+    }
+
+    @Override
+    public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
+        return null;
+    }
+
+    public EntityResolver getEntityResolver() {
+        return entityResolver;
+    }
+
+    public void setEntityResolver(EntityResolver entityResolver) {
+        this.entityResolver = entityResolver;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/38f37d79/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/service/HessianServiceTest.java
----------------------------------------------------------------------
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/service/HessianServiceTest.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/service/HessianServiceTest.java
new file mode 100644
index 0000000..590651d
--- /dev/null
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/remote/hessian/service/HessianServiceTest.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.hessian.service;
+
+import com.caucho.services.server.ServiceContext;
+import com.mockrunner.mock.web.MockHttpServletRequest;
+import com.mockrunner.mock.web.MockHttpSession;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.ObjectContextFactory;
+import org.apache.cayenne.event.MockEventBridgeFactory;
+import org.junit.Test;
+
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertSame;
+
+public class HessianServiceTest {
+
+	@Test
+	public void testGetSession() throws Exception {
+
+		Map<String, String> map = new HashMap<>();
+		map.put(Constants.SERVER_ROP_EVENT_BRIDGE_FACTORY_PROPERTY, MockEventBridgeFactory.class.getName());
+
+		ObjectContextFactory factory = new ObjectContextFactory() {
+
+			public ObjectContext createContext(DataChannel parent) {
+				return null;
+			}
+
+			public ObjectContext createContext() {
+				return null;
+			}
+		};
+		HessianService service = new HessianService(factory, map);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		HttpSession session = new MockHttpSession();
+		request.setSession(session);
+
+		// for some reason need to call this to get session activated in the
+		// mock request
+		request.getSession();
+
+		try {
+			ServiceContext.begin(request, null, null, null);
+			assertSame(session, service.getSession(false));
+		} finally {
+			ServiceContext.end();
+		}
+	}
+}