Merge branch '92'
diff --git a/.gitignore b/.gitignore
index f12106a..ac5c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,6 @@
 derby.log
 .metadata
 bin/
-
 .idea
 *.iml
+.factorypath
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..de4bc9c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: java
+
+env:
+  - DB_PROFILE=hsql
+  - DB_PROFILE=h2
+  - DB_PROFILE=derby
+
+jdk:
+  - oraclejdk8
+  - oraclejdk7
+
+script:
+  - mvn verify -q -DcayenneTestConnection=$DB_PROFILE
+
+# prevent Travis from uneeded "mvn install" run
+install: /bin/true
\ No newline at end of file
diff --git a/README.md b/README.md
index a656469..f5579ea 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@
 	specific language governing permissions and limitations
 	under the License.   
 -->
+[![Build Status](https://travis-ci.org/apache/cayenne.svg)](https://travis-ci.org/apache/cayenne)
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.cayenne/cayenne-server/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.apache.cayenne/cayenne-server/)
+
 Apache Cayenne
 ==============
 
diff --git a/cayenne-client-jetty/pom.xml b/cayenne-client-jetty/pom.xml
new file mode 100644
index 0000000..ea9b55b
--- /dev/null
+++ b/cayenne-client-jetty/pom.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>cayenne-parent</artifactId>
+        <groupId>org.apache.cayenne</groupId>
+        <version>4.0.M4-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>cayenne-client-jetty</artifactId>
+    <name>Cayenne ROP Client (Jetty)</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <jetty.version>9.3.10.v20160621</jetty.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- Jetty client -->
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-client</artifactId>
+                <version>${jetty.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-client</artifactId>
+                <version>${jetty.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-http-client-transport</artifactId>
+                <version>${jetty.version}</version>
+            </dependency>
+
+            <!-- Jetty server test dependencies -->
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-server</artifactId>
+                <version>${jetty.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-servlet</artifactId>
+                <version>${jetty.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-servlets</artifactId>
+                <version>${jetty.version}</version>
+                <scope>test</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- Compile dependencies -->
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- Jetty client -->
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-http-client-transport</artifactId>
+        </dependency>
+
+        <!--Test dependencies-->
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-server</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlets</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-remote-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>process</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>code-quality</id>
+
+            <activation>
+                <property>
+                    <name>!fast-and-dirty</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-checkstyle-plugin</artifactId>
+                        <!--<configuration>
+                            <suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation>
+                        </configuration>-->
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-pmd-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
\ No newline at end of file
diff --git a/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttp2Module.java b/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttp2Module.java
new file mode 100644
index 0000000..22a8751
--- /dev/null
+++ b/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttp2Module.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ * 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.configuration.rop.client;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.rop.JettyHttp2ClientConnectionProvider;
+import org.apache.cayenne.rop.http.JettyHttpROPConnector;
+
+/**
+ * This is HTTP/2 implementation of ROP Client.
+ * <p>
+ * This module uses {@link JettyHttpROPConnector} initialized by {@link JettyHttp2ClientConnectionProvider}
+ * without ALPN by default.
+ * <p>
+ * In order to use it with ALPN you have to set {@link Constants#ROP_SERVICE_USE_ALPN_PROPERTY} to true
+ * and provide the alpn-boot-XXX.jar into the bootstrap classpath.
+ */
+public class ClientJettyHttp2Module implements Module {
+
+    @Override
+    public void configure(Binder binder) {
+        binder.bind(ClientConnection.class).toProvider(JettyHttp2ClientConnectionProvider.class);
+    }
+
+}
diff --git a/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttpModule.java b/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttpModule.java
new file mode 100644
index 0000000..76b5598
--- /dev/null
+++ b/cayenne-client-jetty/src/main/java/org/apache/cayenne/configuration/rop/client/ClientJettyHttpModule.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
+ * <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.configuration.rop.client;
+
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.rop.JettyHttpClientConnectionProvider;
+import org.apache.cayenne.rop.http.JettyHttpROPConnector;
+
+/**
+ * This is HTTP/1.1 implementation of ROP Client.
+ * <p>
+ * This module uses {@link JettyHttpROPConnector} initialized by {@link JettyHttpClientConnectionProvider}.
+ */
+public class ClientJettyHttpModule implements Module {
+
+    @Override
+    public void configure(Binder binder) {
+        binder.bind(ClientConnection.class).toProvider(JettyHttpClientConnectionProvider.class);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttp2ClientConnectionProvider.java b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttp2ClientConnectionProvider.java
new file mode 100644
index 0000000..b1411e9
--- /dev/null
+++ b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttp2ClientConnectionProvider.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ * 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.rop;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.rop.http.JettyHttpROPConnector;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+/**
+ * This {@link Provider} initializes HTTP/2 {@link ClientConnection} through {@link JettyHttpROPConnector} which uses
+ * {@link org.eclipse.jetty.client.HttpClient} over {@link org.eclipse.jetty.http2.client.HTTP2Client}.
+ * It works without ALPN by default.
+ * <p>
+ * In order to use it with ALPN you have to set {@link Constants#ROP_SERVICE_USE_ALPN_PROPERTY} to true
+ * and provide the alpn-boot-XXX.jar into the bootstrap classpath.
+ */
+public class JettyHttp2ClientConnectionProvider extends JettyHttpClientConnectionProvider {
+
+    @Override
+    protected HttpClient initJettyHttpClient() {
+        try {
+            HttpClientTransportOverHTTP2 http2 = new HttpClientTransportOverHTTP2(new HTTP2Client());
+
+            boolean useALPN = runtimeProperties.getBoolean(Constants.ROP_SERVICE_USE_ALPN_PROPERTY, false);
+            http2.setUseALPN(useALPN);
+
+            HttpClient httpClient = new HttpClient(http2, new SslContextFactory());
+            httpClient.start();
+
+            return httpClient;
+        } catch (Exception e) {
+            throw new CayenneRuntimeException("Exception while starting Jetty HttpClient over HTTP/2.", e);
+        }
+    }
+
+}
diff --git a/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttpClientConnectionProvider.java b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttpClientConnectionProvider.java
new file mode 100644
index 0000000..c430c79
--- /dev/null
+++ b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/JettyHttpClientConnectionProvider.java
@@ -0,0 +1,128 @@
+/*****************************************************************
+ * 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.rop;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ConfigurationException;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.RuntimeProperties;
+import org.apache.cayenne.di.DIRuntimeException;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.rop.http.JettyHttpROPConnector;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import java.net.URI;
+
+/**
+ * This {@link Provider} initializes HTTP/1.1 {@link ClientConnection} through {@link JettyHttpROPConnector} which uses
+ * {@link org.eclipse.jetty.client.HttpClient}.
+ */
+public class JettyHttpClientConnectionProvider implements Provider<ClientConnection> {
+
+    private static Log logger = LogFactory.getLog(JettyHttpROPConnector.class);
+
+    @Inject
+    protected RuntimeProperties runtimeProperties;
+
+    @Inject
+    protected ROPSerializationService serializationService;
+
+    @Override
+    public ClientConnection get() throws DIRuntimeException {
+        String sharedSession = runtimeProperties
+                .get(Constants.ROP_SERVICE_SHARED_SESSION_PROPERTY);
+
+        JettyHttpROPConnector ropConnector = createJettyHttpRopConnector();
+        ProxyRemoteService remoteService = new ProxyRemoteService(serializationService, ropConnector);
+
+        HttpClientConnection clientConnection = new HttpClientConnection(remoteService, sharedSession);
+        ropConnector.setClientConnection(clientConnection);
+
+        return clientConnection;
+    }
+
+    protected JettyHttpROPConnector createJettyHttpRopConnector() {
+        String url = runtimeProperties.get(Constants.ROP_SERVICE_URL_PROPERTY);
+        if (url == null) {
+            throw new ConfigurationException(
+                    "No property defined for '%s', can't initialize connection",
+                    Constants.ROP_SERVICE_URL_PROPERTY);
+        }
+
+        String username = runtimeProperties.get(Constants.ROP_SERVICE_USERNAME_PROPERTY);
+        long readTimeout = runtimeProperties.getLong(
+                Constants.ROP_SERVICE_TIMEOUT_PROPERTY,
+                -1L);
+
+        HttpClient httpClient = initJettyHttpClient();
+
+        addBasicAuthentication(httpClient, url, username);
+
+        JettyHttpROPConnector result = new JettyHttpROPConnector(httpClient, url, username);
+
+        if (readTimeout > 0) {
+            result.setReadTimeout(readTimeout);
+        }
+
+        return result;
+    }
+
+    protected HttpClient initJettyHttpClient() {
+        try {
+            HttpClient httpClient = new HttpClient(new SslContextFactory());
+            httpClient.start();
+
+            return httpClient;
+        } catch (Exception e) {
+            throw new CayenneRuntimeException("Exception while starting Jetty HttpClient.", e);
+        }
+    }
+
+    protected void addBasicAuthentication(HttpClient httpClient, String url, String username) {
+        String password = runtimeProperties.get(Constants.ROP_SERVICE_PASSWORD_PROPERTY);
+        String realm = runtimeProperties.get(Constants.ROP_SERVICE_REALM_PROPERTY);
+
+        if (username != null && password != null) {
+            if (realm == null && logger.isWarnEnabled()) {
+                logger.warn("In order to use JettyClient with BASIC Authentication " +
+                        "you should provide Constants.ROP_SERVICE_REALM_PROPERTY.");
+                return;
+            }
+
+            if (logger.isInfoEnabled()) {
+                logger.info(
+                        "Adding authentication" +
+                                "\nUser: " + username +
+                                "\nRealm: " + realm);
+            }
+
+            AuthenticationStore auth = httpClient.getAuthenticationStore();
+            auth.addAuthentication(new BasicAuthentication(URI.create(url), realm, username, password));
+        }
+    }
+
+}
diff --git a/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/http/JettyHttpROPConnector.java b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/http/JettyHttpROPConnector.java
new file mode 100644
index 0000000..f2c0005
--- /dev/null
+++ b/cayenne-client-jetty/src/main/java/org/apache/cayenne/rop/http/JettyHttpROPConnector.java
@@ -0,0 +1,189 @@
+/*****************************************************************
+ * 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.rop.http;
+
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.remote.RemoteSession;
+import org.apache.cayenne.rop.*;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.client.util.InputStreamResponseListener;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This implementation of ROPConnector uses Jetty HTTP Client.
+ * Depends on {@link ClientConnection} provider it uses HTTP/1.1 or HTTP/2 protocol.
+ * <p>
+ * {@link JettyHttpClientConnectionProvider} for HTTP/1.1 protocol.
+ * {@link JettyHttp2ClientConnectionProvider} for HTTP/2 protocol.
+ */
+public class JettyHttpROPConnector implements ROPConnector {
+
+    private static Log logger = LogFactory.getLog(JettyHttpROPConnector.class);
+
+    public static final String SESSION_COOKIE_NAME = "JSESSIONID";
+
+    protected HttpClient httpClient;
+    protected HttpClientConnection clientConnection;
+
+    protected String url;
+    protected String username;
+
+    protected Long readTimeout = 5l;
+
+    public JettyHttpROPConnector(HttpClient httpClient, String url, String username) {
+        if (httpClient == null)
+            throw new IllegalArgumentException("org.eclipse.jetty.client.HttpClient should be provided " +
+                    "for this ROPConnector implementation.");
+
+        this.httpClient = httpClient;
+        this.url = url;
+        this.username = username;
+    }
+
+    public void setClientConnection(HttpClientConnection clientConnection) {
+        this.clientConnection = clientConnection;
+    }
+
+    public void setReadTimeout(Long readTimeout) {
+        this.readTimeout = readTimeout;
+    }
+
+    @Override
+    public InputStream establishSession() throws IOException {
+        if (logger.isInfoEnabled()) {
+            logger.info(ROPUtil.getLogConnect(url, username, true));
+        }
+
+        try {
+            ContentResponse response = httpClient.newRequest(url)
+                    .method(HttpMethod.POST)
+                    .param(ROPConstants.OPERATION_PARAMETER, ROPConstants.ESTABLISH_SESSION_OPERATION)
+                    .timeout(readTimeout, TimeUnit.SECONDS)
+                    .send();
+
+            return new ByteArrayInputStream(response.getContent());
+        } catch (Exception e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+
+            throw new IOException("Exception while establishing session", e);
+        }
+    }
+
+    @Override
+    public InputStream establishSharedSession(String sharedSessionName) throws IOException {
+        if (logger.isInfoEnabled()) {
+            logger.info(ROPUtil.getLogConnect(url, username, true, sharedSessionName));
+        }
+
+        try {
+            ContentResponse response = httpClient.newRequest(url)
+                    .method(HttpMethod.POST)
+                    .param(ROPConstants.OPERATION_PARAMETER, ROPConstants.ESTABLISH_SHARED_SESSION_OPERATION)
+                    .param(ROPConstants.SESSION_NAME_PARAMETER, sharedSessionName)
+                    .timeout(readTimeout, TimeUnit.SECONDS)
+                    .send();
+
+            return new ByteArrayInputStream(response.getContent());
+        } catch (Exception e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+
+            throw new IOException("Exception while establishing shared session: " + sharedSessionName, e);
+        }
+    }
+
+    @Override
+    public InputStream sendMessage(byte[] message) throws IOException {
+        try {
+            Request request = httpClient.newRequest(url)
+                    .method(HttpMethod.POST)
+                    .header(HttpHeader.CONTENT_TYPE, "application/octet-stream")
+                    .header(HttpHeader.ACCEPT_ENCODING, "gzip")
+                    .content(new BytesContentProvider(message));
+
+            addSessionCookie(request);
+
+            InputStreamResponseListener listener = new InputStreamResponseListener();
+            request.send(listener);
+
+            /**
+             * Waits for the given timeout for the response to be available, then returns it.
+             * The wait ends as soon as all the HTTP headers have been received, without waiting for the content.
+             */
+            Response response = listener.get(readTimeout, TimeUnit.SECONDS);
+
+            if (response.getStatus() >= 300) {
+                throw new IOException(
+                        "Did not receive successful HTTP response: status code = " + response.getStatus() +
+                                ", status message = [" + response.getReason() + "]");
+            }
+
+            return listener.getInputStream();
+        } catch (Exception e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+
+            throw new IOException("Exception while sending message", e);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (httpClient != null) {
+            if (logger.isInfoEnabled()) {
+                logger.info(ROPUtil.getLogDisconnect(url, username, true));
+            }
+
+            try {
+                httpClient.stop();
+            } catch (Exception e) {
+                throw new IOException("Exception while stopping Jetty HttpClient", e);
+            }
+        }
+    }
+
+    protected void addSessionCookie(Request request) {
+        if (clientConnection != null) {
+            RemoteSession session = clientConnection.getSession();
+
+            if (session != null && session.getSessionId() != null) {
+                request.header(HttpHeader.COOKIE, SESSION_COOKIE_NAME
+                        + "="
+                        + session.getSessionId());
+            }
+        }
+    }
+}
diff --git a/cayenne-client-jetty/src/test/java/org/apache/cayenne/rop/http/JettyHttpROPConnectorIT.java b/cayenne-client-jetty/src/test/java/org/apache/cayenne/rop/http/JettyHttpROPConnectorIT.java
new file mode 100644
index 0000000..163e6b1
--- /dev/null
+++ b/cayenne-client-jetty/src/test/java/org/apache/cayenne/rop/http/JettyHttpROPConnectorIT.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
+ * <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.rop.http;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.rop.ROPConstants;
+import org.apache.cayenne.util.Http2TestServer;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class JettyHttpROPConnectorIT {
+
+    private static final String MESSAGE = "test message";
+    private static final String SEND_MESSAGE_SESSION = "send message session";
+
+    private static JettyHttpROPConnector ropConnector;
+    private static Http2TestServer server;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        // Start the test server
+        class TestServlet extends HttpServlet {
+            @Override
+            protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+                String sharedSessionName = req.getParameter(ROPConstants.SESSION_NAME_PARAMETER);
+
+                if (sharedSessionName == null) {
+                    resp.getOutputStream().write(MESSAGE.getBytes());
+                } else if (sharedSessionName.equals(SEND_MESSAGE_SESSION)) {
+                    resp.getOutputStream().write(toByteArray(req.getInputStream()));
+                } else {
+                    resp.getOutputStream().write((MESSAGE + " " + sharedSessionName).getBytes());
+                }
+            }
+        }
+
+        server = Http2TestServer.servlet(new TestServlet()).start();
+
+        ropConnector = new JettyHttpROPConnector(initJettyHttp2Client(), server.getBasePath(), null);
+    }
+
+    protected static HttpClient initJettyHttp2Client() {
+        try {
+            HttpClientTransportOverHTTP2 http2 = new HttpClientTransportOverHTTP2(new HTTP2Client());
+            http2.setUseALPN(false);
+
+            HttpClient httpClient = new HttpClient(http2, new SslContextFactory());
+            httpClient.start();
+
+            return httpClient;
+        } catch (Exception e) {
+            throw new CayenneRuntimeException("Exception while starting Jetty HttpClient over HTTP/2.", e);
+        }
+    }
+
+    @AfterClass
+    public static void tearDownClass() throws Exception {
+        server.stop();
+        ropConnector.close();
+    }
+
+    @Test
+    public void testEstablishSession() throws Exception {
+        String message = read(ropConnector.establishSession());
+        assertEquals(MESSAGE, message);
+    }
+
+    @Test
+    public void testEstablishSharedSession() throws Exception {
+        String sharedSessionName = "test session";
+        String message = read(ropConnector.establishSharedSession(sharedSessionName));
+        assertEquals(MESSAGE + " " + sharedSessionName, message);
+    }
+
+    @Test
+    public void sendMessage() throws Exception {
+        ropConnector.establishSharedSession(SEND_MESSAGE_SESSION);
+
+        byte[] message = toByteArray(ropConnector.sendMessage(MESSAGE.getBytes()));
+        assertArrayEquals(MESSAGE.getBytes(), message);
+    }
+
+    private static String read(InputStream input) throws IOException {
+        try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) {
+            return buffer.lines().collect(Collectors.joining("\n"));
+        }
+    }
+
+    private static byte[] toByteArray(InputStream inputStream) throws IOException {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+
+            int reads = inputStream.read();
+            while (reads != -1) {
+                baos.write(reads);
+                reads = inputStream.read();
+            }
+
+            return baos.toByteArray();
+        }
+    }
+
+}
diff --git a/cayenne-client-jetty/src/test/java/org/apache/cayenne/util/Http2TestServer.java b/cayenne-client-jetty/src/test/java/org/apache/cayenne/util/Http2TestServer.java
new file mode 100644
index 0000000..5e6bb35
--- /dev/null
+++ b/cayenne-client-jetty/src/test/java/org/apache/cayenne/util/Http2TestServer.java
@@ -0,0 +1,127 @@
+/*****************************************************************
+ * 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.util;
+
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.function.BiConsumer;
+
+public class Http2TestServer {
+
+    private final String path;
+    private final Server server;
+    private final ServerConnector connector;
+
+    public static TestServerBuilder servlet(HttpServlet servlet) {
+        return new TestServerBuilder(servlet, "/", 0);
+    }
+
+    public static TestServerBuilder handler(BiConsumer<HttpServletRequest, HttpServletResponse> handler) {
+
+        HttpServlet servlet = new HttpServlet() {
+            private static final long serialVersionUID = -7741340028518626628L;
+
+            @Override
+            protected void service(HttpServletRequest req, HttpServletResponse resp) {
+                handler.accept(req, resp);
+            }
+        };
+
+        return servlet(servlet);
+    }
+
+
+    public Http2TestServer(HttpServlet servlet, String path, int port) {
+        this.path = path;
+
+        QueuedThreadPool serverExecutor = new QueuedThreadPool();
+        serverExecutor.setName("server");
+
+        server = new Server(serverExecutor);
+        connector = new ServerConnector(server, 1, 1, new HTTP2ServerConnectionFactory(new HttpConfiguration()));
+        connector.setPort(port);
+        server.addConnector(connector);
+
+        ServletContextHandler context = new ServletContextHandler(server, "/", true, false);
+        context.addServlet(new ServletHolder(servlet), path);
+    }
+
+    void start() {
+        try {
+            server.start();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void stop() {
+        try {
+            server.stop();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public int getLocalPort() {
+        return connector.getLocalPort();
+    }
+
+
+    public String getBasePath() {
+        return "http://localhost:" + getLocalPort() + path;
+    }
+
+
+    public static class TestServerBuilder {
+        private final HttpServlet servlet;
+        private final String path;
+        private final int port;
+
+        private TestServerBuilder(HttpServlet servlet, String path, int port) {
+            this.servlet = servlet;
+            this.path = path;
+            this.port = port;
+        }
+
+        public TestServerBuilder path(String path) {
+            return new TestServerBuilder(this.servlet, path, this.port);
+        }
+
+        public TestServerBuilder port(int port) {
+            return new TestServerBuilder(this.servlet, this.path, port);
+        }
+
+
+        public Http2TestServer start() {
+            Http2TestServer http2Server = new Http2TestServer(servlet, path, port);
+            http2Server.start();
+            return http2Server;
+        }
+    }
+}
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
index 06b842c..4ab182e 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
@@ -19,6 +19,7 @@
 package org.apache.cayenne.rop;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.di.BeforeScopeEnd;
 import org.apache.cayenne.event.EventBridge;
 import org.apache.cayenne.event.EventBridgeFactory;
 import org.apache.cayenne.remote.BaseConnection;
@@ -26,6 +27,8 @@
 import org.apache.cayenne.remote.RemoteService;
 import org.apache.cayenne.remote.RemoteSession;
 
+import java.rmi.RemoteException;
+
 public class HttpClientConnection extends BaseConnection {
 
 	private RemoteService remoteService;
@@ -71,6 +74,11 @@
         return createServerEventBridge(session);
 	}
 
+    @BeforeScopeEnd
+    public void shutdown() throws RemoteException {
+            remoteService.close();
+    }
+
 	protected synchronized void connect() {
 		if (session != null) {
 			return;
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
index 57d4650..943dca0 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
@@ -24,6 +24,7 @@
 import org.apache.cayenne.remote.RemoteSession;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.rmi.RemoteException;
 
 public class ProxyRemoteService implements RemoteService {
@@ -39,24 +40,37 @@
 
     @Override
     public RemoteSession establishSession() throws RemoteException {
-        try {
-            return serializationService.deserialize(ropConnector.establishSession(), RemoteSession.class);
+        try (InputStream is = ropConnector.establishSession()) {
+            return serializationService.deserialize(is, RemoteSession.class);
         } catch (IOException e) {
-            throw new RemoteException(e.getMessage());
+            throw new RemoteException(e.getMessage(), e);
         }
     }
 
     @Override
     public RemoteSession establishSharedSession(String name) throws RemoteException {
-        try {
-            return serializationService.deserialize(ropConnector.establishSharedSession(name), RemoteSession.class);
+        try (InputStream is = ropConnector.establishSharedSession(name)) {
+            return serializationService.deserialize(is, RemoteSession.class);
         } catch (IOException e) {
-            throw new RemoteException(e.getMessage());
+            throw new RemoteException(e.getMessage(), e);
         }
     }
 
     @Override
     public Object processMessage(ClientMessage message) throws RemoteException, Throwable {
-        return serializationService.deserialize(ropConnector.sendMessage(serializationService.serialize(message)), Object.class);
+        try (InputStream is = ropConnector.sendMessage(serializationService.serialize(message))) {
+            return serializationService.deserialize(is, Object.class);
+        } catch (IOException e) {
+            throw new RemoteException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void close() throws RemoteException {
+        try {
+            ropConnector.close();
+        } catch (IOException e) {
+            throw new RemoteException("Exception while closing ROP resources", e);
+        }
     }
 }
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
index 5855cf8..38a0579 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
@@ -37,11 +37,16 @@
 	 * 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".
 	 */
-    InputStream establishSharedSession(String name) throws IOException;
+    InputStream establishSharedSession(String sharedSessionName) throws IOException;
 
 	/**
 	 * Processes message on a remote server, returning the result of such processing.
 	 */
     InputStream sendMessage(byte[] message) throws IOException;
+
+	/**
+	 * Close all resources related to ROP Connector.
+	 */
+	void close() throws IOException;
     
 }
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java
new file mode 100644
index 0000000..cb5cca9
--- /dev/null
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java
@@ -0,0 +1,150 @@
+/*****************************************************************
+ * 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.rop;
+
+import java.util.Map;
+
+public class ROPUtil {
+
+    public static String getLogConnect(String url, String username, boolean password) {
+        return getLogConnect(url, username, password, null);
+    }
+
+    public static String getLogConnect(String url, String username, boolean password, String sharedSessionName) {
+        StringBuilder log = new StringBuilder("Connecting to [");
+        if (username != null) {
+            log.append(username);
+
+            if (password) {
+                log.append(":*******");
+            }
+
+            log.append("@");
+        }
+
+        log.append(url);
+        log.append("]");
+
+        if (sharedSessionName != null) {
+            log.append(" - shared session '").append(sharedSessionName).append("'");
+        } else {
+            log.append(" - dedicated session.");
+        }
+
+        return log.toString();
+    }
+
+    public static String getLogDisconnect(String url, String username, boolean password) {
+        StringBuilder log = new StringBuilder("Disconnecting from [");
+        if (username != null) {
+            log.append(username);
+
+            if (password) {
+                log.append(":*******");
+            }
+
+            log.append("@");
+        }
+
+        log.append(url);
+        log.append("]");
+
+        return log.toString();
+    }
+
+    public static String getParamsAsString(Map<String, String> params) {
+        StringBuilder urlParams = new StringBuilder();
+
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            if (urlParams.length() > 0) {
+                urlParams.append('&');
+            }
+
+            urlParams.append(entry.getKey());
+            urlParams.append('=');
+            urlParams.append(entry.getValue());
+        }
+
+        return urlParams.toString();
+    }
+
+    public static String getBasicAuth(String username, String password) {
+        if (username != null && password != null) {
+            return "Basic " + base64(username + ":" + password);
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates the Base64 value.
+     */
+    public static String base64(String value) {
+        StringBuffer cb = new StringBuffer();
+
+        int i = 0;
+        for (i = 0; i + 2 < value.length(); i += 3) {
+            long chunk = (int) value.charAt(i);
+            chunk = (chunk << 8) + (int) value.charAt(i + 1);
+            chunk = (chunk << 8) + (int) value.charAt(i + 2);
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append(encode(chunk >> 6));
+            cb.append(encode(chunk));
+        }
+
+        if (i + 1 < value.length()) {
+            long chunk = (int) value.charAt(i);
+            chunk = (chunk << 8) + (int) value.charAt(i + 1);
+            chunk <<= 8;
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append(encode(chunk >> 6));
+            cb.append('=');
+        } else if (i < value.length()) {
+            long chunk = (int) value.charAt(i);
+            chunk <<= 16;
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append('=');
+            cb.append('=');
+        }
+
+        return cb.toString();
+    }
+
+    public static char encode(long d) {
+        d &= 0x3f;
+        if (d < 26)
+            return (char) (d + 'A');
+        else if (d < 52)
+            return (char) (d + 'a' - 26);
+        else if (d < 62)
+            return (char) (d + '0' - 52);
+        else if (d == 62)
+            return '+';
+        else
+            return '/';
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
index 15a54ed..bdae52b 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
@@ -22,6 +22,7 @@
 import org.apache.cayenne.rop.HttpClientConnection;
 import org.apache.cayenne.rop.ROPConnector;
 import org.apache.cayenne.rop.ROPConstants;
+import org.apache.cayenne.rop.ROPUtil;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -66,7 +67,7 @@
     @Override
     public InputStream establishSession() throws IOException {
         if (logger.isInfoEnabled()) {
-            logConnect(null);
+            logger.info(ROPUtil.getLogConnect(url, username, password != null));
         }
 		
 		Map<String, String> requestParams = new HashMap<>();
@@ -76,14 +77,14 @@
     }
 
     @Override
-    public InputStream establishSharedSession(String name) throws IOException {
+    public InputStream establishSharedSession(String sharedSessionName) throws IOException {
         if (logger.isInfoEnabled()) {
-            logConnect(name);
+            logger.info(ROPUtil.getLogConnect(url, username, password != null, sharedSessionName));
         }
 
 		Map<String, String> requestParams = new HashMap<>();
 		requestParams.put(ROPConstants.OPERATION_PARAMETER, ROPConstants.ESTABLISH_SHARED_SESSION_OPERATION);
-		requestParams.put(ROPConstants.SESSION_NAME_PARAMETER, name);
+		requestParams.put(ROPConstants.SESSION_NAME_PARAMETER, sharedSessionName);
 
 		return doRequest(requestParams);
     }
@@ -92,40 +93,35 @@
     public InputStream sendMessage(byte[] message) throws IOException {
         return doRequest(message);
     }
-	
-	protected InputStream doRequest(Map<String, String> params) throws IOException {
-		URLConnection connection = new URL(url).openConnection();
 
-		StringBuilder urlParams = new StringBuilder();
+    @Override
+    public void close() throws IOException {
+        if (logger.isInfoEnabled()) {
+            logger.info(ROPUtil.getLogDisconnect(url, username, password != null));
+        }
+    }
 
-		for (Map.Entry<String, String> entry : params.entrySet()) {
-			if (urlParams.length() > 0) {
-				urlParams.append('&');
-			}
+    protected InputStream doRequest(Map<String, String> params) throws IOException {
+        URLConnection connection = new URL(url).openConnection();
 
-			urlParams.append(entry.getKey());
-			urlParams.append('=');
-			urlParams.append(entry.getValue());
-		}
+        if (readTimeout != null) {
+            connection.setReadTimeout(readTimeout.intValue());
+        }
 
-		if (readTimeout != null) {
-			connection.setReadTimeout(readTimeout.intValue());
-		}
+        addAuthHeader(connection);
 
-		addAuthHeader(connection);
+        connection.setDoOutput(true);
 
-		connection.setDoOutput(true);
-		
-		connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-		connection.setRequestProperty("charset", "utf-8");
+        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        connection.setRequestProperty("charset", "utf-8");
 
-		try (OutputStream output = connection.getOutputStream()) {
-			output.write(urlParams.toString().getBytes(StandardCharsets.UTF_8));
+        try (OutputStream output = connection.getOutputStream()) {
+            output.write(ROPUtil.getParamsAsString(params).getBytes(StandardCharsets.UTF_8));
             output.flush();
-		}
+        }
 
-		return connection.getInputStream();
-	} 
+        return connection.getInputStream();
+    }
 
     protected InputStream doRequest(byte[] data) throws IOException {
         URLConnection connection = new URL(url).openConnection();
@@ -151,7 +147,7 @@
     }
 
     protected void addAuthHeader(URLConnection connection) {
-        String basicAuth = getBasicAuth(username, password);
+        String basicAuth = ROPUtil.getBasicAuth(username, password);
 
         if (basicAuth != null) {
             connection.addRequestProperty("Authorization", basicAuth);
@@ -167,91 +163,4 @@
         }
     }
 
-    public String getBasicAuth(String user, String password) {
-        if (user != null && password != null) {
-            return "Basic " + base64(user + ":" + password);
-        }
-
-        return null;
-    }
-
-    /**
-     * Creates the Base64 value.
-     */
-    private String base64(String value) {
-        StringBuffer cb = new StringBuffer();
-
-        int i = 0;
-        for (i = 0; i + 2 < value.length(); i += 3) {
-            long chunk = (int) value.charAt(i);
-            chunk = (chunk << 8) + (int) value.charAt(i + 1);
-            chunk = (chunk << 8) + (int) value.charAt(i + 2);
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append(encode(chunk >> 6));
-            cb.append(encode(chunk));
-        }
-
-        if (i + 1 < value.length()) {
-            long chunk = (int) value.charAt(i);
-            chunk = (chunk << 8) + (int) value.charAt(i + 1);
-            chunk <<= 8;
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append(encode(chunk >> 6));
-            cb.append('=');
-        }
-        else if (i < value.length()) {
-            long chunk = (int) value.charAt(i);
-            chunk <<= 16;
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append('=');
-            cb.append('=');
-        }
-
-        return cb.toString();
-    }
-
-    public static char encode(long d) {
-        d &= 0x3f;
-        if (d < 26)
-            return (char) (d + 'A');
-        else if (d < 52)
-            return (char) (d + 'a' - 26);
-        else if (d < 62)
-            return (char) (d + '0' - 52);
-        else if (d == 62)
-            return '+';
-        else
-            return '/';
-    }
-
-    private void logConnect(String sharedSessionName) {
-        StringBuilder log = new StringBuilder("Connecting to [");
-        if (username != null) {
-            log.append(username);
-
-            if (password != null) {
-                log.append(":*******");
-            }
-
-            log.append("@");
-        }
-
-        log.append(url);
-        log.append("]");
-
-        if (sharedSessionName != null) {
-            log.append(" - shared session '").append(sharedSessionName).append("'");
-        }
-        else {
-            log.append(" - dedicated session.");
-        }
-
-        logger.info(log.toString());
-    }
 }
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/ClientObjectSelectIT.java b/cayenne-client/src/test/java/org/apache/cayenne/query/ClientObjectSelectIT.java
new file mode 100644
index 0000000..e328dd6
--- /dev/null
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/ClientObjectSelectIT.java
@@ -0,0 +1,149 @@
+/*****************************************************************
+ *   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.query;
+
+import org.apache.cayenne.CayenneContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.remote.RemoteIncrementalFaultList;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.mt.ClientMtTable1;
+import org.apache.cayenne.unit.di.DataChannelInterceptor;
+import org.apache.cayenne.unit.di.UnitTestClosure;
+import org.apache.cayenne.unit.di.client.ClientCase;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static junit.framework.TestCase.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@UseServerRuntime(CayenneProjects.MULTI_TIER_PROJECT)
+public class ClientObjectSelectIT extends ClientCase {
+
+    @Inject
+    private CayenneContext context;
+
+    @Inject
+    DataChannelInterceptor serverCaseDataChannelInterceptor;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    private TableHelper mtTable;
+
+    @Before
+    public void setUp() throws Exception {
+        mtTable = new TableHelper(dbHelper, "MT_TABLE1");
+        mtTable.setColumns("TABLE1_ID", "GLOBAL_ATTRIBUTE1", "SERVER_ATTRIBUTE1");
+
+        for (int i = 1; i <= 20; i++) {
+            mtTable.insert(i, "globalAttr" + i, "serverAttr" + i);
+        }
+    }
+
+    @Test
+    public void testSelect() throws Exception{
+        List<ClientMtTable1> list = ObjectSelect.query(ClientMtTable1.class).
+                select(context);
+
+        assertNotNull(list);
+        assertEquals(20, list.size());
+    }
+
+    @Test
+    public void testCacheSelect() throws Exception{
+        final ObjectSelect objectSelect = ObjectSelect.query(ClientMtTable1.class).
+                cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
+
+        final List<ClientMtTable1> list1 = objectSelect.select(context);
+        assertNotNull(list1);
+        assertFalse(list1.isEmpty());
+
+        serverCaseDataChannelInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
+            @Override
+            public void execute() {
+                List<ClientMtTable1> list2 = objectSelect.select(context);
+                assertNotNull(list2);
+                assertFalse(list2.isEmpty());
+                assertEquals(list1, list2);
+            }
+        });
+    }
+
+    @Test
+    public void testLimitSelect() throws Exception{
+        List<ClientMtTable1> list = ObjectSelect.query(ClientMtTable1.class)
+                .offset(5)
+                .limit(10)
+                .select(context);
+
+        assertNotNull(list);
+        assertEquals(10, list.size());
+    }
+
+    @Test
+    public void testCacheLimitSelect() throws Exception {
+        final ObjectSelect objectSelect = ObjectSelect.query(ClientMtTable1.class)
+                .cacheStrategy(QueryCacheStrategy.SHARED_CACHE)
+                .offset(5)
+                .limit(10);
+
+        final List<ClientMtTable1> list1 = objectSelect.select(context);
+        assertEquals(10, list1.size());
+
+        serverCaseDataChannelInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
+            @Override
+            public void execute() {
+                List<ClientMtTable1> list2 = objectSelect.select(context);
+                assertNotNull(list2);
+                assertEquals(10, list2.size());
+                assertEquals(list1, list2);
+            }
+        });
+    }
+
+    @Test
+    public void testPageSelect() throws Exception{
+        final ObjectSelect objectSelect = ObjectSelect.query(ClientMtTable1.class)
+                .pageSize(5);
+
+        final List<ClientMtTable1> list = objectSelect.select(context);
+        assertNotNull(list);
+        assertEquals(RemoteIncrementalFaultList.class, list.getClass());
+
+        int count = serverCaseDataChannelInterceptor.runWithQueryCounter(new UnitTestClosure() {
+            @Override
+            public void execute() {
+                assertNotNull(list.get(0));
+                assertNotNull(list.get(4));
+                assertNotNull(list.get(5));
+                assertNotNull(list.get(6));
+            }
+        });
+
+        assertEquals(1, count);
+    }
+
+}
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
index e86e564..4b8a471 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.crypto.transformer.BindingsTransformer;
@@ -65,16 +65,16 @@
             }
 
             @Override
-            public ParameterBinding[] getBindings() {
+            public DbAttributeBinding[] getBindings() {
                 return delegateTranslator.getBindings();
             }
 
             @Override
-            public ParameterBinding[] updateBindings(BatchQueryRow row) {
+            public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
 
                 ensureEncryptorCompiled();
 
-                ParameterBinding[] bindings = delegateTranslator.updateBindings(row);
+                DbAttributeBinding[] bindings = delegateTranslator.updateBindings(row);
 
                 if (encryptor != null) {
                     encryptor.transform(bindings);
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
index c6f9577..1ffded8 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
@@ -18,12 +18,12 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 
 /**
  * @since 4.0
  */
 public interface BindingsTransformer {
 
-    void transform(ParameterBinding[] bindings);
+    void transform(DbAttributeBinding[] bindings);
 }
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
index db31eea..e8aeb4b 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor;
 import org.apache.cayenne.crypto.transformer.value.ValueEncryptor;
 
@@ -38,12 +38,12 @@
     }
 
     @Override
-    public void transform(ParameterBinding[] bindings) {
+    public void transform(DbAttributeBinding[] bindings) {
 
         int len = positions.length;
 
         for (int i = 0; i < len; i++) {
-            ParameterBinding b = bindings[positions[i]];
+            DbAttributeBinding b = bindings[positions[i]];
             Object transformed = transformers[i].encrypt(encryptor, b.getValue());
             b.setValue(transformed);
         }
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
index 178a2f8..09a56ef 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
@@ -18,12 +18,8 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.crypto.map.ColumnMapper;
 import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
@@ -32,6 +28,10 @@
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
 /**
  * @since 4.0
  */
@@ -90,7 +90,7 @@
     }
 
     @Override
-    public BindingsTransformer encryptor(ParameterBinding[] bindings) {
+    public BindingsTransformer encryptor(DbAttributeBinding[] bindings) {
         int len = bindings.length;
         List<Integer> cryptoColumns = null;
 
@@ -115,7 +115,7 @@
 
             for (int i = 0; i < dlen; i++) {
                 int pos = cryptoColumns.get(i);
-                ParameterBinding b = bindings[pos];
+                DbAttributeBinding b = bindings[pos];
                 positions[i] = pos;
                 transformers[i] = transformerFactory.encryptor(b.getAttribute());
             }
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
index 9bc28a8..a8893d1 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.crypto.transformer;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 
 /**
  * A factory that creates encryption transformers used for processing batch
@@ -29,7 +29,7 @@
  */
 public interface TransformerFactory {
 
-    BindingsTransformer encryptor(ParameterBinding[] bindings);
+    BindingsTransformer encryptor(DbAttributeBinding[] bindings);
 
     MapTransformer decryptor(ColumnDescriptor[] columns, Object sampleRow);
 }
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
index dead7f0..85c5aca 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
@@ -13,11 +13,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    @Deprecated
-    public static final String CRYPTO_STRING_PROPERTY = "cryptoString";
-    @Deprecated
-    public static final String PLAIN_STRING_PROPERTY = "plainString";
-
     public static final String ID_PK_COLUMN = "ID";
 
     public static final Property<String> CRYPTO_STRING = new Property<String>("cryptoString");
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
index 026d713..2a914d4 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
@@ -13,11 +13,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    @Deprecated
-    public static final String CRYPTO_BYTES_PROPERTY = "cryptoBytes";
-    @Deprecated
-    public static final String PLAIN_BYTES_PROPERTY = "plainBytes";
-
     public static final String ID_PK_COLUMN = "ID";
 
     public static final Property<byte[]> CRYPTO_BYTES = new Property<byte[]>("cryptoBytes");
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
index adf3d67..937b1e6 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
@@ -13,9 +13,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    @Deprecated
-    public static final String CRYPTO_STRING_PROPERTY = "cryptoString";
-
     public static final String ID_PK_COLUMN = "ID";
 
     public static final Property<String> CRYPTO_STRING = new Property<String>("cryptoString");
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
index d8390de..2bc5213 100644
--- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
@@ -35,7 +35,8 @@
 import org.apache.cayenne.query.Query;
 
 /**
- * A {@link DataChannelFilter} that organizes commit changes
+ * A {@link DataChannelFilter} that captures commit changes, delegating their
+ * processing to an underlying collection of listeners.
  * 
  * @since 4.0
  */
@@ -54,8 +55,7 @@
 
 	@Override
 	public void init(DataChannel channel) {
-		// TODO Auto-generated method stub
-
+		// do nothing...
 	}
 
 	@Override
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
index 7df6e27..71d866e 100644
--- a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
@@ -57,7 +57,7 @@
 
 		Processor processor = new Processor();
 
-		AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor);
+		AuditableFilter filter = new AuditableFilter(processor);
 		domain.addFilter(filter);
 
 		// prerequisite for BaseAuditableProcessor use
@@ -97,7 +97,7 @@
 
 		Processor processor = new Processor();
 
-		AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor);
+		AuditableFilter filter = new AuditableFilter(processor);
 		domain.addFilter(filter);
 
 		// prerequisite for BaseAuditableProcessor use
@@ -130,7 +130,7 @@
 
 		Processor processor = new Processor();
 
-		AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor);
+		AuditableFilter filter = new AuditableFilter(processor);
 		domain.addFilter(filter);
 
 		// prerequisite for BaseAuditableProcessor use
@@ -166,7 +166,7 @@
 
 		Processor processor = new Processor();
 
-		AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor);
+		AuditableFilter filter = new AuditableFilter(processor);
 		domain.addFilter(filter);
 
 		// prerequisite for BaseAuditableProcessor use
@@ -197,7 +197,7 @@
 		DataDomain domain = runtime.getDataDomain();
 		Processor processor = new Processor();
 
-		AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor);
+		AuditableFilter filter = new AuditableFilter(processor);
 		domain.addFilter(filter);
 
 		// prerequisite for BaseAuditableProcessor use
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
new file mode 100644
index 0000000..7bf0de8
--- /dev/null
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
@@ -0,0 +1,237 @@
+package org.apache.cayenne.lifecycle.postcommit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.annotation.PrePersist;
+import org.apache.cayenne.annotation.PreUpdate;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.lifecycle.changemap.AttributeChange;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.apache.cayenne.lifecycle.changemap.ObjectChange;
+import org.apache.cayenne.lifecycle.changemap.ObjectChangeType;
+import org.apache.cayenne.lifecycle.db.Auditable1;
+import org.apache.cayenne.lifecycle.db.AuditableChild1;
+import org.apache.cayenne.lifecycle.unit.AuditableServerCase;
+import org.apache.cayenne.query.SelectById;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Testing capturing changes introduced by the pre-commit listeners.
+ */
+public class PostCommitFilter_ListenerInducedChangesIT extends AuditableServerCase {
+
+	protected ObjectContext context;
+	protected PostCommitListener mockListener;
+
+	@Override
+	protected ServerRuntimeBuilder configureCayenne() {
+		this.mockListener = mock(PostCommitListener.class);
+		return super.configureCayenne().addModule(PostCommitModuleBuilder.builder().listener(mockListener).build());
+	}
+
+	@Before
+	public void before() {
+		context = runtime.newContext();
+	}
+
+	@Test
+	public void testPostCommit_Insert() throws SQLException {
+
+		final InsertListener listener = new InsertListener();
+		runtime.getDataDomain().addListener(listener);
+
+		final Auditable1 a1 = context.newObject(Auditable1.class);
+		a1.setCharProperty1("yy");
+
+		doAnswer(new Answer<Object>() {
+			@Override
+			public Object answer(InvocationOnMock invocation) throws Throwable {
+
+				assertNotNull(listener.c);
+
+				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+
+				assertEquals(2, sortedChanges.size());
+
+				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+				assertEquals(ObjectChangeType.INSERT, sortedChanges.get(0).getType());
+
+				assertEquals(listener.c.getObjectId(), sortedChanges.get(1).getPostCommitId());
+				assertEquals(ObjectChangeType.INSERT, sortedChanges.get(1).getType());
+
+				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+						.get(AuditableChild1.CHAR_PROPERTY1.getName());
+				assertNotNull(listenerInducedChange);
+				assertEquals("c1", listenerInducedChange.getNewValue());
+
+				return null;
+			}
+		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+
+		context.commitChanges();
+
+		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+	}
+
+	@Test
+	public void testPostCommit_Delete() throws SQLException {
+
+		auditable1.insert(1, "yy");
+		auditableChild1.insert(31, 1, "yyc");
+
+		final DeleteListener listener = new DeleteListener();
+		runtime.getDataDomain().addListener(listener);
+
+		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).prefetch(Auditable1.CHILDREN1.joint())
+				.selectFirst(context);
+		a1.setCharProperty1("zz");
+
+		doAnswer(new Answer<Object>() {
+			@Override
+			public Object answer(InvocationOnMock invocation) throws Throwable {
+
+				assertNotNull(listener.toDelete);
+				assertEquals(1, listener.toDelete.size());
+
+				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+
+				assertEquals(2, sortedChanges.size());
+
+				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
+				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+
+				assertEquals(ObjectChangeType.DELETE, sortedChanges.get(1).getType());
+				assertEquals(listener.toDelete.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
+
+				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+						.get(AuditableChild1.CHAR_PROPERTY1.getName());
+				assertNotNull(listenerInducedChange);
+				assertEquals("yyc", listenerInducedChange.getOldValue());
+
+				return null;
+			}
+		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+
+		context.commitChanges();
+
+		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+	}
+
+	@Test
+	public void testPostCommit_Update() throws SQLException {
+
+		auditable1.insert(1, "yy");
+		auditableChild1.insert(31, 1, "yyc");
+
+		final UpdateListener listener = new UpdateListener();
+		runtime.getDataDomain().addListener(listener);
+
+		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).prefetch(Auditable1.CHILDREN1.joint())
+				.selectFirst(context);
+		a1.setCharProperty1("zz");
+
+		doAnswer(new Answer<Object>() {
+			@Override
+			public Object answer(InvocationOnMock invocation) throws Throwable {
+
+				assertNotNull(listener.toUpdate);
+				assertEquals(1, listener.toUpdate.size());
+
+				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+
+				assertEquals(2, sortedChanges.size());
+
+				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
+				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+
+				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(1).getType());
+				assertEquals(listener.toUpdate.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
+
+				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+						.get(AuditableChild1.CHAR_PROPERTY1.getName());
+				assertNotNull(listenerInducedChange);
+				assertEquals("yyc", listenerInducedChange.getOldValue());
+				assertEquals("yyc_", listenerInducedChange.getNewValue());
+
+				return null;
+			}
+		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+
+		context.commitChanges();
+
+		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
+	}
+
+	private List<ObjectChange> sortedChanges(InvocationOnMock invocation) {
+		assertSame(context, invocation.getArguments()[0]);
+
+		ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+
+		List<ObjectChange> sortedChanges = new ArrayList<>(changes.getUniqueChanges());
+		Collections.sort(sortedChanges, new Comparator<ObjectChange>() {
+			public int compare(ObjectChange o1, ObjectChange o2) {
+				return o1.getPostCommitId().getEntityName().compareTo(o2.getPostCommitId().getEntityName());
+			}
+		});
+
+		return sortedChanges;
+	}
+
+	static class InsertListener {
+
+		private AuditableChild1 c;
+
+		@PrePersist(Auditable1.class)
+		public void prePersist(Auditable1 a) {
+
+			c = a.getObjectContext().newObject(AuditableChild1.class);
+			c.setCharProperty1("c1");
+			c.setParent(a);
+		}
+	}
+
+	static class DeleteListener {
+
+		private List<AuditableChild1> toDelete;
+
+		@PreUpdate(Auditable1.class)
+		public void prePersist(Auditable1 a) {
+
+			toDelete = new ArrayList<>(a.getChildren1());
+			for (AuditableChild1 c : toDelete) {
+				c.getObjectContext().deleteObject(c);
+			}
+		}
+	}
+
+	static class UpdateListener {
+
+		private List<AuditableChild1> toUpdate;
+
+		@PreUpdate(Auditable1.class)
+		public void prePersist(Auditable1 a) {
+
+			toUpdate = new ArrayList<>(a.getChildren1());
+			for (AuditableChild1 c : toUpdate) {
+				c.setCharProperty1(c.getCharProperty1() + "_");
+			}
+		}
+	}
+
+}
diff --git a/cayenne-protostuff/pom.xml b/cayenne-protostuff/pom.xml
new file mode 100644
index 0000000..d868f43
--- /dev/null
+++ b/cayenne-protostuff/pom.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.cayenne</groupId>
+        <artifactId>cayenne-parent</artifactId>
+        <version>4.0.M4-SNAPSHOT</version>
+    </parent>
+    <artifactId>cayenne-protostuff</artifactId>
+    <packaging>jar</packaging>
+    <name>Cayenne Protostuff Extension</name>
+
+    <properties>
+        <protostuff.version>1.4.3</protostuff.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>io.protostuff</groupId>
+                <artifactId>protostuff-core</artifactId>
+                <version>${protostuff.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.protostuff</groupId>
+                <artifactId>protostuff-runtime</artifactId>
+                <version>${protostuff.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.protostuff</groupId>
+                <artifactId>protostuff-collectionschema</artifactId>
+                <version>${protostuff.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.protostuff</groupId>
+            <artifactId>protostuff-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.protostuff</groupId>
+            <artifactId>protostuff-runtime</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.protostuff</groupId>
+            <artifactId>protostuff-collectionschema</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-client</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-java8</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>hsqldb</groupId>
+            <artifactId>hsqldb</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.cayenne.plugins</groupId>
+                <artifactId>maven-cayenne-plugin</artifactId>
+                <version>${project.version}</version>
+                <configuration>
+                    <map>${project.basedir}/src/test/resources/protostuff.map.xml</map>
+                    <destDir>${project.basedir}/src/test/java</destDir>
+                    <client>true</client>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>default-cli</id>
+                        <goals>
+                            <goal>cgen</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-remote-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>process</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>code-quality</id>
+
+            <activation>
+                <property>
+                    <name>!fast-and-dirty</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-checkstyle-plugin</artifactId>
+                        <!--<configuration>
+                            <suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation>
+                        </configuration>-->
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-pmd-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
\ No newline at end of file
diff --git a/cayenne-protostuff/src/main/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactory.java b/cayenne-protostuff/src/main/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactory.java
new file mode 100644
index 0000000..f6d5a0c
--- /dev/null
+++ b/cayenne-protostuff/src/main/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactory.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
+ * <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;
+
+import io.protostuff.CollectionSchema;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class ObjectContextChangeLogSubListMessageFactory implements CollectionSchema.MessageFactory {
+
+    @Override
+    public <V> Collection<V> newMessage() {
+        return new ArrayList<>();
+    }
+
+    @Override
+    public Class<?> typeClass() {
+        return ObjectContextChangeLog.SubList.class;
+    }
+}
diff --git a/cayenne-protostuff/src/main/java/org/apache/cayenne/configuration/rop/client/ProtostuffModule.java b/cayenne-protostuff/src/main/java/org/apache/cayenne/configuration/rop/client/ProtostuffModule.java
new file mode 100644
index 0000000..ccc5510
--- /dev/null
+++ b/cayenne-protostuff/src/main/java/org/apache/cayenne/configuration/rop/client/ProtostuffModule.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ * 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.configuration.rop.client;
+
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.rop.protostuff.ProtostuffROPSerializationService;
+
+/**
+ * A DI module that uses Protostuff Object Graph Serialization as Cayenne {@link ROPSerializationService}.
+ * <a href="http://www.protostuff.io/">
+ *
+ * To use this module you should add the following system properties:
+ *      -Dprotostuff.runtime.collection_schema_on_repeated_fields=true
+ *      -Dprotostuff.runtime.morph_collection_interfaces=true
+ *      -Dprotostuff.runtime.morph_map_interfaces=true
+ *      -Dprotostuff.runtime.pojo_schema_on_collection_fields=true
+ *      -Dprotostuff.runtime.pojo_schema_on_map_fields=true
+ *
+ * @since 4.0
+ */
+public class ProtostuffModule implements Module {
+
+    public ProtostuffModule() {
+    }
+
+    @Override
+    public void configure(Binder binder) {
+        binder.bind(ROPSerializationService.class).to(ProtostuffROPSerializationService.class).inSingletonScope();
+    }
+}
diff --git a/cayenne-protostuff/src/main/java/org/apache/cayenne/query/PrefetchTreeNodeSchema.java b/cayenne-protostuff/src/main/java/org/apache/cayenne/query/PrefetchTreeNodeSchema.java
new file mode 100644
index 0000000..2d4a9a2
--- /dev/null
+++ b/cayenne-protostuff/src/main/java/org/apache/cayenne/query/PrefetchTreeNodeSchema.java
@@ -0,0 +1,157 @@
+/*****************************************************************
+ * 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.query;
+
+import io.protostuff.Input;
+import io.protostuff.Output;
+import io.protostuff.Schema;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * As {@link PrefetchTreeNode} has {@link PrefetchTreeNode#readResolve readResolve} method, which isn't supported
+ * by Protostuff, we have to provide custom schema for this class.
+ *
+ * @see java.io.Serializable
+ */
+public class PrefetchTreeNodeSchema implements Schema<PrefetchTreeNode> {
+
+    private static final HashMap<String, Integer> fieldMap = new HashMap<>();
+
+    static {
+        fieldMap.put("name", 1);
+        fieldMap.put("phantom", 2);
+        fieldMap.put("semantics", 3);
+        fieldMap.put("ejbqlPathEntityId", 4);
+        fieldMap.put("entityName", 5);
+        fieldMap.put("children", 6);
+    }
+
+    @Override
+    public String getFieldName(int number) {
+        switch (number) {
+            case 1:
+                return "name";
+            case 2:
+                return "phantom";
+            case 3:
+                return "semantics";
+            case 4:
+                return "ejbqlPathEntityId";
+            case 5:
+                return "entityName";
+            case 6:
+                return "children";
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public int getFieldNumber(String name) {
+        return fieldMap.getOrDefault(name, 0);
+    }
+
+    @Override
+    public boolean isInitialized(PrefetchTreeNode message) {
+        return true;
+    }
+
+    @Override
+    public PrefetchTreeNode newMessage() {
+        return new PrefetchTreeNode();
+    }
+
+    @Override
+    public String messageName() {
+        return PrefetchTreeNode.class.getSimpleName();
+    }
+
+    @Override
+    public String messageFullName() {
+        return PrefetchTreeNode.class.getName();
+    }
+
+    @Override
+    public Class<PrefetchTreeNode> typeClass() {
+        return PrefetchTreeNode.class;
+    }
+
+    @Override
+    public void mergeFrom(Input input, PrefetchTreeNode message) throws IOException {
+        for (int number = input.readFieldNumber(this);; number = input.readFieldNumber(this)) {
+            switch (number) {
+                case 0:
+                    message.readResolve();
+                    return;
+                case 1:
+                    message.name = input.readString();
+                    break;
+                case 2:
+                    message.setPhantom(input.readBool());
+                    break;
+                case 3:
+                    message.setSemantics(input.readInt32());
+                    break;
+                case 4:
+                    message.setEjbqlPathEntityId(input.readString());
+                    break;
+                case 5:
+                    message.setEntityName(input.readString());
+                    break;
+                case 6:
+                    if (message.children == null) {
+                        message.children = new ArrayList<>(4);
+                    }
+                    message.children.add(input.mergeObject(null, this));
+                    break;
+                default:
+                    input.handleUnknownField(number, this);
+            }
+        }
+    }
+
+    @Override
+    public void writeTo(Output output, PrefetchTreeNode message) throws IOException {
+        if (message.getName() != null) {
+            output.writeString(1, message.getName(), false);
+        }
+
+        output.writeBool(2, message.isPhantom(), false);
+        output.writeInt32(3, message.getSemantics(), false);
+
+        if (message.getEjbqlPathEntityId() != null) {
+            output.writeString(4, message.getEjbqlPathEntityId(), false);
+        }
+
+        if (message.getEntityName() != null) {
+            output.writeString(5, message.getEntityName(), false);
+        }
+
+        if (message.hasChildren()) {
+            for (PrefetchTreeNode node : message.getChildren()) {
+                output.writeObject(6, node, this, true);
+            }
+        }
+    }
+
+}
diff --git a/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationService.java b/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationService.java
new file mode 100644
index 0000000..af4aeb2
--- /dev/null
+++ b/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationService.java
@@ -0,0 +1,92 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+import io.protostuff.GraphIOUtil;
+import io.protostuff.LinkedBuffer;
+import io.protostuff.Schema;
+import io.protostuff.runtime.DefaultIdStrategy;
+import io.protostuff.runtime.RuntimeEnv;
+import io.protostuff.runtime.RuntimeSchema;
+import org.apache.cayenne.ObjectContextChangeLogSubListMessageFactory;
+import org.apache.cayenne.access.ToManyList;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.PrefetchTreeNodeSchema;
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.util.PersistentObjectList;
+import org.apache.cayenne.util.PersistentObjectMap;
+import org.apache.cayenne.util.PersistentObjectSet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * This {@link ROPSerializationService} implementation uses Protostuff {@link GraphIOUtil} to (de)serialize
+ * Cayenne object graph.
+ *
+ * @since 4.0
+ */
+public class ProtostuffROPSerializationService implements ROPSerializationService {
+
+    protected Schema<Wrapper> wrapperSchema;
+    protected DefaultIdStrategy strategy;
+
+    public ProtostuffROPSerializationService() {
+        this.strategy = (DefaultIdStrategy) RuntimeEnv.ID_STRATEGY;
+        register();
+    }
+
+    protected void register() {
+        this.wrapperSchema = RuntimeSchema.getSchema(Wrapper.class);
+
+        this.strategy.registerCollection(new ObjectContextChangeLogSubListMessageFactory());
+
+        RuntimeSchema.register(PrefetchTreeNode.class, new PrefetchTreeNodeSchema());
+        RuntimeSchema.register(PersistentObjectList.class);
+        RuntimeSchema.register(PersistentObjectMap.class);
+        RuntimeSchema.register(PersistentObjectSet.class);
+        RuntimeSchema.register(ToManyList.class);
+    }
+
+    @Override
+    public byte[] serialize(Object object) throws IOException {
+        return GraphIOUtil.toByteArray(new Wrapper(object), wrapperSchema, LinkedBuffer.allocate());
+    }
+
+    @Override
+    public void serialize(Object object, OutputStream outputStream) throws IOException {
+        GraphIOUtil.writeTo(outputStream, new Wrapper(object), wrapperSchema, LinkedBuffer.allocate());
+    }
+
+    @Override
+    public <T> T deserialize(InputStream inputStream, Class<T> objectClass) throws IOException {
+        Wrapper result = wrapperSchema.newMessage();
+        GraphIOUtil.mergeFrom(inputStream, result, wrapperSchema);
+        return objectClass.cast(result.data);
+    }
+
+    @Override
+    public <T> T deserialize(byte[] serializedObject, Class<T> objectClass) throws IOException {
+        Wrapper result = wrapperSchema.newMessage();
+        GraphIOUtil.mergeFrom(serializedObject, result, wrapperSchema);
+        return objectClass.cast(result.data);
+    }
+
+}
diff --git a/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/Wrapper.java b/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/Wrapper.java
new file mode 100644
index 0000000..3205b6d
--- /dev/null
+++ b/cayenne-protostuff/src/main/java/org/apache/cayenne/rop/protostuff/Wrapper.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
+ * <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.rop.protostuff;
+
+import java.io.Serializable;
+
+/**
+ * As Protostuff has limitation that nested messages should not contain references to the root message, so we provide
+ * a simple wrapper for the root message.
+ *
+ * <a href="http://www.protostuff.io/documentation/object-graphs/">
+ *
+ * @since 4.0
+ */
+public class Wrapper implements Serializable {
+
+    public Object data;
+
+    public Wrapper(Object data) {
+        this.data = data;
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java
new file mode 100644
index 0000000..7fe3dd2
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.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
+ * <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;
+
+import org.apache.cayenne.graph.CompoundDiff;
+import org.apache.cayenne.graph.NodeCreateOperation;
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.rop.protostuff.ProtostuffProperties;
+import org.apache.cayenne.rop.protostuff.ProtostuffROPSerializationService;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ObjectContextChangeLogSubListMessageFactoryTest extends ProtostuffProperties {
+
+    private ROPSerializationService serializationService;
+
+    @Before
+    public void setUp() throws Exception {
+        serializationService = new ProtostuffROPSerializationService();
+    }
+
+    @Test
+    public void testGetDiffsSerializable() throws Exception {
+        ObjectContextChangeLog recorder = new ObjectContextChangeLog();
+        recorder.addOperation(new NodeCreateOperation(new ObjectId("test")));
+        CompoundDiff diff = (CompoundDiff) recorder.getDiffs();
+
+        byte[] data = serializationService.serialize(diff);
+        CompoundDiff diff0 = serializationService.deserialize(data, CompoundDiff.class);
+
+        assertNotNull(diff0);
+        assertEquals(1, diff0.getDiffs().size());
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeSchemaTest.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeSchemaTest.java
new file mode 100644
index 0000000..4e5d2ca
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeSchemaTest.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
+ * <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.query;
+
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.rop.protostuff.ProtostuffProperties;
+import org.apache.cayenne.rop.protostuff.ProtostuffROPSerializationService;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class PrefetchTreeNodeSchemaTest extends ProtostuffProperties {
+
+    private ROPSerializationService serializationService;
+
+    @Before
+    public void setUp() throws Exception {
+        serializationService = new ProtostuffROPSerializationService();
+    }
+
+    @Test
+    public void testPrefetchTreeNodeSchema() throws IOException {
+        PrefetchTreeNode parent = new PrefetchTreeNode(null, "parent");
+        PrefetchTreeNode child = new PrefetchTreeNode(parent, "child");
+        parent.addChild(child);
+
+        byte[] data = serializationService.serialize(parent);
+        PrefetchTreeNode parent0 = serializationService.deserialize(data, PrefetchTreeNode.class);
+
+        assertNotNull(parent0);
+        assertTrue(parent0.hasChildren());
+
+        PrefetchTreeNode child0 = parent0.getChild("child");
+        assertNotNull(child0);
+        assertNotNull(child0.parent);
+        assertEquals(child0.parent, parent0);
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnection.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnection.java
new file mode 100644
index 0000000..0545308
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnection.java
@@ -0,0 +1,78 @@
+/*****************************************************************
+ * 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.service;
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.event.EventBridge;
+import org.apache.cayenne.remote.BaseConnection;
+import org.apache.cayenne.remote.ClientMessage;
+import org.apache.cayenne.rop.ROPSerializationService;
+
+import java.io.IOException;
+
+/**
+ * A ClientConnection that connects to a DataChannel. Used as an emulator of a remote
+ * service. Emulation includes serialization/deserialization of objects via {@link ROPSerializationService}.
+ *
+ * {@link LocalConnection} should be replaced by this one after moving all ROP functionality to the separate module.
+ * It'll provide more flexibility around which serialization should be used.
+ */
+public class ProtostuffLocalConnection extends BaseConnection {
+
+    protected DataChannel channel;
+
+    @Inject
+    private ROPSerializationService serializationService;
+
+    public ProtostuffLocalConnection(DataChannel channel) {
+        this.channel = channel;
+    }
+
+    @Override
+    public EventBridge getServerEventBridge() throws CayenneRuntimeException {
+        return null;
+    }
+
+    @Override
+    protected void beforeSendMessage(ClientMessage message) throws CayenneRuntimeException {
+        // noop
+    }
+
+    @Override
+    protected Object doSendMessage(ClientMessage message) throws CayenneRuntimeException {
+        try {
+            ClientMessage processedMessage = (ClientMessage) cloneViaSerializationService(message);
+
+            Object result = DispatchHelper.dispatch(channel, processedMessage);
+
+            return cloneViaSerializationService(result);
+        }
+        catch (Exception ex) {
+            throw new CayenneRuntimeException("Error deserializing result", ex);
+        }
+    }
+
+    public Object cloneViaSerializationService(Object object) throws IOException {
+        byte[] data = serializationService.serialize(object);
+        return serializationService.deserialize(data, object.getClass());
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnectionProvider.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnectionProvider.java
new file mode 100644
index 0000000..9cae2d1
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/remote/service/ProtostuffLocalConnectionProvider.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
+ * <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.service;
+
+import org.apache.cayenne.ConfigurationException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.configuration.rop.client.ClientLocalRuntime;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.remote.ClientConnection;
+
+public class ProtostuffLocalConnectionProvider implements Provider<ClientConnection> {
+
+    @Inject(ClientLocalRuntime.CLIENT_SERVER_CHANNEL_KEY)
+    protected Provider<DataChannel> clientServerChannelProvider;
+
+    @Override
+    public ClientConnection get() throws ConfigurationException {
+        DataChannel clientServerChannel = clientServerChannelProvider.get();
+        return new ProtostuffLocalConnection(clientServerChannel);
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffPersistentObjectCollectionsTest.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffPersistentObjectCollectionsTest.java
new file mode 100644
index 0000000..ab93dd9
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffPersistentObjectCollectionsTest.java
@@ -0,0 +1,211 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.reflect.MapAccessor;
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.util.PersistentObjectList;
+import org.apache.cayenne.util.PersistentObjectMap;
+import org.apache.cayenne.util.PersistentObjectSet;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class ProtostuffPersistentObjectCollectionsTest extends ProtostuffProperties {
+
+    private ROPSerializationService serializationService;
+
+    private TestObject object1;
+    private TestObject object2;
+
+    @Before
+    public void setUp() throws Exception {
+        serializationService = new ProtostuffROPSerializationService();
+
+        object1 = new TestObject();
+        object2 = new TestObject();
+        object1.name = "object1";
+        object2.name = "object2";
+        object1.object = object2;
+    }
+
+    @Test
+    public void testPersistentObjectList() throws IOException {
+        PersistentObjectList list = new PersistentObjectList(object1, "test");
+        list.add(object2);
+
+        byte[] bytes = serializationService.serialize(list);
+        PersistentObjectList list0 = serializationService.deserialize(bytes, PersistentObjectList.class);
+
+        assertNotNull(list0);
+        assertEquals(list.getRelationshipName(), list0.getRelationshipName());
+        assertEquals(list.getRelationshipOwner(), list0.getRelationshipOwner());
+
+        Object object0 = list0.get(0);
+        assertEquals(object2, object0);
+    }
+
+    @Test
+    public void testPersistentObjectListWithWrapper() throws IOException {
+        PersistentObjectList list = new PersistentObjectList(object1, "test");
+        list.add(object2);
+
+        byte[] bytes = serializationService.serialize(new ListWrapper(list));
+        ListWrapper lw = serializationService.deserialize(bytes, ListWrapper.class);
+
+        assertNotNull(lw.object);
+        assertTrue(lw.object instanceof PersistentObjectList);
+
+        PersistentObjectList list0 = (PersistentObjectList) lw.object;
+        assertEquals(list.getRelationshipName(), list0.getRelationshipName());
+        assertEquals(list.getRelationshipOwner(), list0.getRelationshipOwner());
+
+        Object object0 = list0.get(0);
+        assertEquals(object2, object0);
+    }
+
+    @Test
+    public void testPersistentObjectSet() throws IOException {
+        PersistentObjectSet set = new PersistentObjectSet(object1, "test");
+        set.add(object2);
+
+        byte[] bytes = serializationService.serialize(set);
+        PersistentObjectSet set0 = serializationService.deserialize(bytes, PersistentObjectSet.class);
+
+        assertNotNull(set0);
+        assertEquals(set.getRelationshipName(), set0.getRelationshipName());
+        assertEquals(set.getRelationshipOwner(), set0.getRelationshipOwner());
+
+        Object object0 = set0.toArray()[0];
+        assertEquals(object2, object0);
+    }
+
+    @Test
+    public void testPersistentObjectSetWithWrapper() throws IOException {
+        PersistentObjectSet set = new PersistentObjectSet(object1, "test");
+        set.add(object2);
+
+        byte[] bytes = serializationService.serialize(new SetWrapper(set));
+        SetWrapper sw = serializationService.deserialize(bytes, SetWrapper.class);
+
+        assertNotNull(sw.object);
+        assertTrue(sw.object instanceof PersistentObjectSet);
+
+        PersistentObjectSet set0 = (PersistentObjectSet) sw.object;
+        assertNotNull(set0);
+        assertEquals(set.getRelationshipName(), set0.getRelationshipName());
+        assertEquals(set.getRelationshipOwner(), set0.getRelationshipOwner());
+
+        Object object0 = set0.toArray()[0];
+        assertEquals(object2, object0);
+    }
+
+    @Test
+    public void testPersistentObjectMap() throws IOException {
+        PersistentObjectMap map = new PersistentObjectMap(object1, "test", new MapAccessor("test"));
+        map.put(object2.name, object2);
+
+        byte[] bytes = serializationService.serialize(map);
+        PersistentObjectMap map0 = serializationService.deserialize(bytes, PersistentObjectMap.class);
+
+        assertNotNull(map0);
+        assertEquals(map0.getRelationshipName(), map0.getRelationshipName());
+        assertEquals(map0.getRelationshipOwner(), map0.getRelationshipOwner());
+
+        Object object0 = map0.get(object2.name);
+        assertEquals(object2, object0);
+    }
+
+    @Test
+    public void testPersistentObjectMapWithWrapper() throws IOException {
+        PersistentObjectMap map = new PersistentObjectMap(object1, "test", new MapAccessor("test"));
+        map.put(object2.name, object2);
+
+        byte[] bytes = serializationService.serialize(new MapWrapper(map));
+        MapWrapper mw = serializationService.deserialize(bytes, MapWrapper.class);
+
+        assertNotNull(mw.object);
+        assertTrue(mw.object instanceof PersistentObjectMap);
+
+        PersistentObjectMap map0 = (PersistentObjectMap) mw.object;
+        assertEquals(map0.getRelationshipName(), map0.getRelationshipName());
+        assertEquals(map0.getRelationshipOwner(), map0.getRelationshipOwner());
+
+        Object object0 = map0.get(object2.name);
+        assertEquals(object2, object0);
+    }
+
+    private static class TestObject extends PersistentObject {
+        public String name;
+        public TestObject object;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TestObject)) return false;
+
+            TestObject that = (TestObject) o;
+
+            if (name != null ? !name.equals(that.name) : that.name != null) return false;
+            return object != null ? object.equals(that.object) : that.object == null;
+
+        }
+
+        @Override
+        public int hashCode() {
+            int result = name != null ? name.hashCode() : 0;
+            result = 31 * result + (object != null ? object.hashCode() : 0);
+            return result;
+        }
+    }
+
+    private static class ListWrapper {
+        List<?> object;
+
+        public ListWrapper(List<?> object) {
+            this.object = object;
+        }
+    }
+
+    private static class SetWrapper {
+        Set<?> object;
+
+        public SetWrapper(Set<?> object) {
+            this.object = object;
+        }
+    }
+
+    private static class MapWrapper {
+        Map<?, ?> object;
+
+        public MapWrapper(Map<?, ?> object) {
+            this.object = object;
+        }
+    }
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffProperties.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffProperties.java
new file mode 100644
index 0000000..21d6857
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffProperties.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+public class ProtostuffProperties {
+
+    static {
+        System.setProperty("protostuff.runtime.collection_schema_on_repeated_fields", "true");
+        System.setProperty("protostuff.runtime.morph_collection_interfaces", "true");
+        System.setProperty("protostuff.runtime.morph_map_interfaces", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_collection_fields", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_map_fields", "true");
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationServiceIT.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationServiceIT.java
new file mode 100644
index 0000000..578b4fa
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationServiceIT.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable1;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable2;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.Assert.assertEquals;
+
+public class ProtostuffROPSerializationServiceIT extends RuntimeBase {
+
+    private ClientMtTable1 table1;
+    private ClientMtTable2 table2;
+
+    @Before
+    public void setUp() throws Exception {
+        context = clientRuntime.newContext();
+
+        table1 = context.newObject(ClientMtTable1.class);
+        table1.setGlobalAttribute("table1");
+        table1.setDateAttribute(LocalDate.now());
+
+        table2 = context.newObject(ClientMtTable2.class);
+        table2.setGlobalAttribute("table2");
+
+        table1.addToTable2Array(table2);
+        table2.setTable1(table1);
+
+        context.commitChanges();
+    }
+
+    @After
+    public void setDown() throws Exception {
+        context.deleteObjects(table2, table1);
+        context.commitChanges();
+    }
+
+    @Test
+    public void testSerializationWithPrefetch1() throws Exception {
+        ClientMtTable1 table1 = ObjectSelect.query(ClientMtTable1.class)
+                .prefetch(ClientMtTable1.TABLE2ARRAY.joint())
+                .selectOne(context);
+
+        ClientMtTable2 table2 = table1.getTable2Array().get(0);
+
+        assertEquals(this.table1, table1);
+        assertEquals(this.table2, table2);
+    }
+
+    @Test
+    public void testSerializationWithPrefetch2() throws Exception {
+        ClientMtTable2 table2 = ObjectSelect.query(ClientMtTable2.class)
+                .prefetch(ClientMtTable2.TABLE1.joint())
+                .selectOne(context);
+
+        ClientMtTable1 table1 = table2.getTable1();
+
+        assertEquals(this.table1, table1);
+        assertEquals(this.table2, table2);
+    }
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationTest.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationTest.java
new file mode 100644
index 0000000..38ab617
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/ProtostuffROPSerializationTest.java
@@ -0,0 +1,123 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable1;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable2;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+
+public class ProtostuffROPSerializationTest extends ProtostuffProperties {
+
+    private static final String GLOBAL_ATTRIBUTE1 = "Test table1";
+    private static final String GLOBAL_ATTRIBUTE2 = "Test table2";
+
+    private ClientMtTable1 table1;
+    private ClientMtTable2 table2;
+
+    private Date oldDate;
+    private LocalDate localDate;
+    private LocalTime localTime;
+    private LocalDateTime localDateTime;
+
+    private ROPSerializationService clientService;
+    private ROPSerializationService serverService;
+
+    @Before
+    public void setUpData() throws Exception {
+        oldDate = new Date();
+        localDate = LocalDate.now();
+        localTime = LocalTime.now();
+        localDateTime = LocalDateTime.now();
+
+        table1 = new ClientMtTable1();
+        table1.setGlobalAttribute(GLOBAL_ATTRIBUTE1);
+        table1.setOldDateAttribute(oldDate);
+        table1.setDateAttribute(localDate);
+        table1.setTimeAttribute(localTime);
+        table1.setTimestampAttribute(localDateTime);
+
+        table2 = new ClientMtTable2();
+        table2.setTable1(table1);
+        table2.setGlobalAttribute(GLOBAL_ATTRIBUTE2);
+
+        clientService = new ProtostuffROPSerializationService();
+        serverService = new ProtostuffROPSerializationService();
+    }
+
+    @Test
+    public void testByteArraySerialization() throws Exception {
+        // test client to server serialization
+        byte[] data = clientService.serialize(table2);
+        ClientMtTable2 serverTable2 = serverService.deserialize(data, ClientMtTable2.class);
+
+        assertCorrectness(serverTable2);
+
+        // test server to client serialization
+        data = serverService.serialize(table2);
+        ClientMtTable2 clientTable2 = clientService.deserialize(data, ClientMtTable2.class);
+
+        assertCorrectness(clientTable2);
+    }
+
+    @Test
+    public void testStreamSerialization() throws Exception {
+        // test client to server serialization
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        clientService.serialize(table2, out);
+        out.flush();
+
+        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        ClientMtTable2 serverTable2 = serverService.deserialize(in, ClientMtTable2.class);
+
+        assertCorrectness(serverTable2);
+
+        // test server to client serialization
+        out = new ByteArrayOutputStream();
+        serverService.serialize(table2, out);
+        out.flush();
+
+        in = new ByteArrayInputStream(out.toByteArray());
+        ClientMtTable2 clientTable2 = clientService.deserialize(in, ClientMtTable2.class);
+
+        assertCorrectness(clientTable2);
+    }
+
+    private void assertCorrectness(ClientMtTable2 table2) {
+        ClientMtTable1 table1 = table2.getTable1();
+        assertEquals(GLOBAL_ATTRIBUTE2, table2.getGlobalAttribute());
+        assertEquals(GLOBAL_ATTRIBUTE1, table1.getGlobalAttribute());
+        assertEquals(oldDate, table1.getOldDateAttribute());
+        assertEquals(localDate, table1.getDateAttribute());
+        assertEquals(localTime, table1.getTimeAttribute());
+        assertEquals(localDateTime, table1.getTimestampAttribute());
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/RuntimeBase.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/RuntimeBase.java
new file mode 100644
index 0000000..f474353
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/RuntimeBase.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ * 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.rop.protostuff;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.rop.client.ClientLocalRuntime;
+import org.apache.cayenne.configuration.rop.client.ClientRuntime;
+import org.apache.cayenne.configuration.rop.client.ProtostuffModule;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.java8.CayenneJava8Module;
+import org.apache.cayenne.remote.ClientConnection;
+import org.apache.cayenne.remote.service.ProtostuffLocalConnectionProvider;
+import org.junit.Before;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class RuntimeBase extends ProtostuffProperties {
+
+    protected ServerRuntime serverRuntime;
+    protected ClientRuntime clientRuntime;
+    protected ObjectContext context;
+
+    @Before
+    public void setUpRuntimes() throws Exception {
+        this.serverRuntime = new ServerRuntime("cayenne-protostuff.xml",
+                new ProtostuffModule(),
+                new CayenneJava8Module());
+
+        Map<String, String> properties = new HashMap<>();
+        properties.put(Constants.ROP_CHANNEL_EVENTS_PROPERTY, Boolean.TRUE.toString());
+
+        Module module = binder -> binder.bind(ClientConnection.class)
+                .toProviderInstance(new ProtostuffLocalConnectionProvider());
+
+        this.clientRuntime = new ClientLocalRuntime(
+                serverRuntime.getInjector(),
+                properties,
+                new ProtostuffModule(),
+                new CayenneJava8Module(),
+                module);
+
+        this.context = clientRuntime.newContext();
+    }
+
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable1.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable1.java
new file mode 100644
index 0000000..7a07d5f
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable1.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.rop.protostuff.persistent;
+
+import org.apache.cayenne.rop.protostuff.persistent.auto._ClientMtTable1;
+
+/**
+ * A persistent class mapped as "MtTable1" Cayenne entity.
+ */
+public class ClientMtTable1 extends _ClientMtTable1 {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable2.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable2.java
new file mode 100644
index 0000000..c17b1c1
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/ClientMtTable2.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.rop.protostuff.persistent;
+
+import org.apache.cayenne.rop.protostuff.persistent.auto._ClientMtTable2;
+
+/**
+ * A persistent class mapped as "MtTable2" Cayenne entity.
+ */
+public class ClientMtTable2 extends _ClientMtTable2 {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable1.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable1.java
new file mode 100644
index 0000000..db169a2
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable1.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.rop.protostuff.persistent;
+
+import org.apache.cayenne.rop.protostuff.persistent.auto._MtTable1;
+
+public class MtTable1 extends _MtTable1 {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable2.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable2.java
new file mode 100644
index 0000000..aa23626
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/MtTable2.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.rop.protostuff.persistent;
+
+import org.apache.cayenne.rop.protostuff.persistent.auto._MtTable2;
+
+public class MtTable2 extends _MtTable2 {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable1.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable1.java
new file mode 100644
index 0000000..30e9a7f
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable1.java
@@ -0,0 +1,191 @@
+package org.apache.cayenne.rop.protostuff.persistent.auto;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable2;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "MtTable1" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _ClientMtTable1 extends PersistentObject {
+
+    public static final Property<LocalDate> DATE_ATTRIBUTE = new Property<LocalDate>("dateAttribute");
+    public static final Property<String> GLOBAL_ATTRIBUTE = new Property<String>("globalAttribute");
+    public static final Property<Date> OLD_DATE_ATTRIBUTE = new Property<Date>("oldDateAttribute");
+    public static final Property<String> SERVER_ATTRIBUTE = new Property<String>("serverAttribute");
+    public static final Property<LocalTime> TIME_ATTRIBUTE = new Property<LocalTime>("timeAttribute");
+    public static final Property<LocalDateTime> TIMESTAMP_ATTRIBUTE = new Property<LocalDateTime>("timestampAttribute");
+    public static final Property<List<ClientMtTable2>> TABLE2ARRAY = new Property<List<ClientMtTable2>>("table2Array");
+
+    protected LocalDate dateAttribute;
+    protected String globalAttribute;
+    protected Date oldDateAttribute;
+    protected String serverAttribute;
+    protected LocalTime timeAttribute;
+    protected LocalDateTime timestampAttribute;
+    protected List<ClientMtTable2> table2Array;
+
+    public LocalDate getDateAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateAttribute", false);
+        }
+
+        return dateAttribute;
+    }
+    public void setDateAttribute(LocalDate dateAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateAttribute", false);
+        }
+
+        Object oldValue = this.dateAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "dateAttribute", oldValue, dateAttribute);
+        }
+        
+        this.dateAttribute = dateAttribute;
+    }
+
+    public String getGlobalAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "globalAttribute", false);
+        }
+
+        return globalAttribute;
+    }
+    public void setGlobalAttribute(String globalAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "globalAttribute", false);
+        }
+
+        Object oldValue = this.globalAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "globalAttribute", oldValue, globalAttribute);
+        }
+        
+        this.globalAttribute = globalAttribute;
+    }
+
+    public Date getOldDateAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "oldDateAttribute", false);
+        }
+
+        return oldDateAttribute;
+    }
+    public void setOldDateAttribute(Date oldDateAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "oldDateAttribute", false);
+        }
+
+        Object oldValue = this.oldDateAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "oldDateAttribute", oldValue, oldDateAttribute);
+        }
+        
+        this.oldDateAttribute = oldDateAttribute;
+    }
+
+    public String getServerAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "serverAttribute", false);
+        }
+
+        return serverAttribute;
+    }
+    public void setServerAttribute(String serverAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "serverAttribute", false);
+        }
+
+        Object oldValue = this.serverAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "serverAttribute", oldValue, serverAttribute);
+        }
+        
+        this.serverAttribute = serverAttribute;
+    }
+
+    public LocalTime getTimeAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "timeAttribute", false);
+        }
+
+        return timeAttribute;
+    }
+    public void setTimeAttribute(LocalTime timeAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "timeAttribute", false);
+        }
+
+        Object oldValue = this.timeAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "timeAttribute", oldValue, timeAttribute);
+        }
+        
+        this.timeAttribute = timeAttribute;
+    }
+
+    public LocalDateTime getTimestampAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "timestampAttribute", false);
+        }
+
+        return timestampAttribute;
+    }
+    public void setTimestampAttribute(LocalDateTime timestampAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "timestampAttribute", false);
+        }
+
+        Object oldValue = this.timestampAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "timestampAttribute", oldValue, timestampAttribute);
+        }
+        
+        this.timestampAttribute = timestampAttribute;
+    }
+
+    public List<ClientMtTable2> getTable2Array() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "table2Array", true);
+        } else if (this.table2Array == null) {
+        	this.table2Array = new PersistentObjectList(this, "table2Array");
+		}
+
+        return table2Array;
+    }
+    public void addToTable2Array(ClientMtTable2 object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "table2Array", true);
+        } else if (this.table2Array == null) {
+        	this.table2Array = new PersistentObjectList(this, "table2Array");
+		}
+
+        this.table2Array.add(object);
+    }
+    public void removeFromTable2Array(ClientMtTable2 object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "table2Array", true);
+        } else if (this.table2Array == null) {
+        	this.table2Array = new PersistentObjectList(this, "table2Array");
+		}
+
+        this.table2Array.remove(object);
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable2.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable2.java
new file mode 100644
index 0000000..b4432a1
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_ClientMtTable2.java
@@ -0,0 +1,69 @@
+package org.apache.cayenne.rop.protostuff.persistent.auto;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.ValueHolder;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.rop.protostuff.persistent.ClientMtTable1;
+import org.apache.cayenne.util.PersistentObjectHolder;
+
+/**
+ * A generated persistent class mapped as "MtTable2" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _ClientMtTable2 extends PersistentObject {
+
+    public static final Property<String> GLOBAL_ATTRIBUTE = new Property<String>("globalAttribute");
+    public static final Property<ClientMtTable1> TABLE1 = new Property<ClientMtTable1>("table1");
+
+    protected String globalAttribute;
+    protected ValueHolder table1;
+
+    public String getGlobalAttribute() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "globalAttribute", false);
+        }
+
+        return globalAttribute;
+    }
+    public void setGlobalAttribute(String globalAttribute) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "globalAttribute", false);
+        }
+
+        Object oldValue = this.globalAttribute;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "globalAttribute", oldValue, globalAttribute);
+        }
+        
+        this.globalAttribute = globalAttribute;
+    }
+
+    public ClientMtTable1 getTable1() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "table1", true);
+        } else if (this.table1 == null) {
+        	this.table1 = new PersistentObjectHolder(this, "table1");
+		}
+
+        return (ClientMtTable1) table1.getValue();
+    }
+    public void setTable1(ClientMtTable1 table1) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "table1", true);
+        } else if (this.table1 == null) {
+        	this.table1 = new PersistentObjectHolder(this, "table1");
+		}
+
+        // note how we notify ObjectContext of change BEFORE the object is actually
+        // changed... this is needed to take a valid current snapshot
+        Object oldValue = this.table1.getValueDirectly();
+        if (objectContext != null) {
+        	objectContext.propertyChanged(this, "table1", oldValue, table1);
+        }
+        
+        this.table1.setValue(table1);
+    }
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable1.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable1.java
new file mode 100644
index 0000000..edc4630
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable1.java
@@ -0,0 +1,87 @@
+package org.apache.cayenne.rop.protostuff.persistent.auto;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.rop.protostuff.persistent.MtTable2;
+
+/**
+ * Class _MtTable1 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _MtTable1 extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String TABLE1_ID_PK_COLUMN = "TABLE1_ID";
+
+    public static final Property<LocalDate> DATE_ATTRIBUTE = new Property<LocalDate>("dateAttribute");
+    public static final Property<String> GLOBAL_ATTRIBUTE = new Property<String>("globalAttribute");
+    public static final Property<Date> OLD_DATE_ATTRIBUTE = new Property<Date>("oldDateAttribute");
+    public static final Property<String> SERVER_ATTRIBUTE = new Property<String>("serverAttribute");
+    public static final Property<LocalTime> TIME_ATTRIBUTE = new Property<LocalTime>("timeAttribute");
+    public static final Property<LocalDateTime> TIMESTAMP_ATTRIBUTE = new Property<LocalDateTime>("timestampAttribute");
+    public static final Property<List<MtTable2>> TABLE2ARRAY = new Property<List<MtTable2>>("table2Array");
+
+    public void setDateAttribute(LocalDate dateAttribute) {
+        writeProperty("dateAttribute", dateAttribute);
+    }
+    public LocalDate getDateAttribute() {
+        return (LocalDate)readProperty("dateAttribute");
+    }
+
+    public void setGlobalAttribute(String globalAttribute) {
+        writeProperty("globalAttribute", globalAttribute);
+    }
+    public String getGlobalAttribute() {
+        return (String)readProperty("globalAttribute");
+    }
+
+    public void setOldDateAttribute(Date oldDateAttribute) {
+        writeProperty("oldDateAttribute", oldDateAttribute);
+    }
+    public Date getOldDateAttribute() {
+        return (Date)readProperty("oldDateAttribute");
+    }
+
+    public void setServerAttribute(String serverAttribute) {
+        writeProperty("serverAttribute", serverAttribute);
+    }
+    public String getServerAttribute() {
+        return (String)readProperty("serverAttribute");
+    }
+
+    public void setTimeAttribute(LocalTime timeAttribute) {
+        writeProperty("timeAttribute", timeAttribute);
+    }
+    public LocalTime getTimeAttribute() {
+        return (LocalTime)readProperty("timeAttribute");
+    }
+
+    public void setTimestampAttribute(LocalDateTime timestampAttribute) {
+        writeProperty("timestampAttribute", timestampAttribute);
+    }
+    public LocalDateTime getTimestampAttribute() {
+        return (LocalDateTime)readProperty("timestampAttribute");
+    }
+
+    public void addToTable2Array(MtTable2 obj) {
+        addToManyTarget("table2Array", obj, true);
+    }
+    public void removeFromTable2Array(MtTable2 obj) {
+        removeToManyTarget("table2Array", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<MtTable2> getTable2Array() {
+        return (List<MtTable2>)readProperty("table2Array");
+    }
+
+
+}
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable2.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable2.java
new file mode 100644
index 0000000..db226a8
--- /dev/null
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/rop/protostuff/persistent/auto/_MtTable2.java
@@ -0,0 +1,38 @@
+package org.apache.cayenne.rop.protostuff.persistent.auto;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.rop.protostuff.persistent.MtTable1;
+
+/**
+ * Class _MtTable2 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _MtTable2 extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String TABLE2_ID_PK_COLUMN = "TABLE2_ID";
+
+    public static final Property<String> GLOBAL_ATTRIBUTE = new Property<String>("globalAttribute");
+    public static final Property<MtTable1> TABLE1 = new Property<MtTable1>("table1");
+
+    public void setGlobalAttribute(String globalAttribute) {
+        writeProperty("globalAttribute", globalAttribute);
+    }
+    public String getGlobalAttribute() {
+        return (String)readProperty("globalAttribute");
+    }
+
+    public void setTable1(MtTable1 table1) {
+        setToOneTarget("table1", table1, true);
+    }
+
+    public MtTable1 getTable1() {
+        return (MtTable1)readProperty("table1");
+    }
+
+
+}
diff --git a/cayenne-protostuff/src/test/resources/cayenne-protostuff.xml b/cayenne-protostuff/src/test/resources/cayenne-protostuff.xml
new file mode 100644
index 0000000..e585624
--- /dev/null
+++ b/cayenne-protostuff/src/test/resources/cayenne-protostuff.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain project-version="8">
+	<map name="protostuff"/>
+
+	<node name="datanode"
+		  factory="org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory"
+		  schema-update-strategy="org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy"
+	>
+		<map-ref name="protostuff"/>
+		<data-source>
+			<driver value="org.hsqldb.jdbcDriver"/>
+			<url value="jdbc:hsqldb:mem:protostuff"/>
+			<connectionPool min="1" max="1"/>
+			<login userName="sa"/>
+		</data-source>
+	</node>
+</domain>
diff --git a/cayenne-protostuff/src/test/resources/protostuff.map.xml b/cayenne-protostuff/src/test/resources/protostuff.map.xml
new file mode 100644
index 0000000..373b84e
--- /dev/null
+++ b/cayenne-protostuff/src/test/resources/protostuff.map.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/8/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/8/modelMap http://cayenne.apache.org/schema/8/modelMap.xsd"
+	 project-version="8">
+	<property name="defaultPackage" value="org.apache.cayenne.rop.protostuff.persistent"/>
+	<property name="clientSupported" value="true"/>
+	<property name="defaultClientPackage" value="org.apache.cayenne.rop.protostuff.persistent"/>
+	<db-entity name="MT_TABLE1">
+		<db-attribute name="DATE_ATTRIBUTE" type="DATE"/>
+		<db-attribute name="GLOBAL_ATTRIBUTE" type="VARCHAR" length="100"/>
+		<db-attribute name="OLD_DATE_ATTRIBUTE" type="TIMESTAMP"/>
+		<db-attribute name="SERVER_ATTRIBUTE" type="VARCHAR" length="100"/>
+		<db-attribute name="TABLE1_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="TIMESTAMP_ATTRIBUTE" type="TIMESTAMP"/>
+		<db-attribute name="TIME_ATTRIBUTE" type="TIME"/>
+	</db-entity>
+	<db-entity name="MT_TABLE2">
+		<db-attribute name="GLOBAL_ATTRIBUTE" type="VARCHAR" length="100"/>
+		<db-attribute name="TABLE1_ID" type="INTEGER"/>
+		<db-attribute name="TABLE2_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<obj-entity name="MtTable1" className="org.apache.cayenne.rop.protostuff.persistent.MtTable1" clientClassName="org.apache.cayenne.rop.protostuff.persistent.ClientMtTable1" dbEntityName="MT_TABLE1">
+		<obj-attribute name="dateAttribute" type="java.time.LocalDate" db-attribute-path="DATE_ATTRIBUTE"/>
+		<obj-attribute name="globalAttribute" type="java.lang.String" db-attribute-path="GLOBAL_ATTRIBUTE"/>
+		<obj-attribute name="oldDateAttribute" type="java.util.Date" db-attribute-path="OLD_DATE_ATTRIBUTE"/>
+		<obj-attribute name="serverAttribute" type="java.lang.String" db-attribute-path="SERVER_ATTRIBUTE"/>
+		<obj-attribute name="timeAttribute" type="java.time.LocalTime" db-attribute-path="TIME_ATTRIBUTE"/>
+		<obj-attribute name="timestampAttribute" type="java.time.LocalDateTime" db-attribute-path="TIMESTAMP_ATTRIBUTE"/>
+	</obj-entity>
+	<obj-entity name="MtTable2" className="org.apache.cayenne.rop.protostuff.persistent.MtTable2" clientClassName="org.apache.cayenne.rop.protostuff.persistent.ClientMtTable2" dbEntityName="MT_TABLE2">
+		<obj-attribute name="globalAttribute" type="java.lang.String" db-attribute-path="GLOBAL_ATTRIBUTE"/>
+	</obj-entity>
+	<db-relationship name="table2Array" source="MT_TABLE1" target="MT_TABLE2" toMany="true">
+		<db-attribute-pair source="TABLE1_ID" target="TABLE1_ID"/>
+	</db-relationship>
+	<db-relationship name="table1" source="MT_TABLE2" target="MT_TABLE1" toMany="false">
+		<db-attribute-pair source="TABLE1_ID" target="TABLE1_ID"/>
+	</db-relationship>
+	<obj-relationship name="table2Array" source="MtTable1" target="MtTable2" deleteRule="Deny" db-relationship-path="table2Array"/>
+	<obj-relationship name="table1" source="MtTable2" target="MtTable1" deleteRule="Nullify" db-relationship-path="table1"/>
+</data-map>
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ClientServerChannelQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ClientServerChannelQueryAction.java
index 2abd30b..0be21c6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ClientServerChannelQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ClientServerChannelQueryAction.java
@@ -19,9 +19,6 @@
 
 package org.apache.cayenne.access;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
@@ -37,6 +34,9 @@
 import org.apache.cayenne.util.ListResponse;
 import org.apache.cayenne.util.ObjectDetachOperation;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * A query handler used by ClientServerChannel.
  * 
@@ -83,20 +83,20 @@
             if (cachedList == null) {
 
                 // attempt to refetch... respawn the action...
-
                 Query originatingQuery = serverMetadata.getOrginatingQuery();
                 if (originatingQuery != null) {
-
                     ClientServerChannelQueryAction subaction = new ClientServerChannelQueryAction(
                             channel,
                             originatingQuery);
                     subaction.execute();
-                    cachedList = channel.getQueryCache().get(serverMetadata);
-                }
 
-                if (cachedList == null) {
-                    throw new CayenneRuntimeException("No cached list for "
-                            + serverMetadata.getCacheKey());
+                    cachedList = channel.getQueryCache().get(serverMetadata);
+                    if (cachedList == null) {
+                        throw new CayenneRuntimeException("No cached list for "
+                                + serverMetadata.getCacheKey());
+                    }
+                } else {
+                    return !DONE;
                 }
             }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
index edd2a7d..948671f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
@@ -75,7 +75,7 @@
             ObjectIdQuery oidQuery = (ObjectIdQuery) query;
 
             if (!oidQuery.isFetchMandatory()) {
-                Object object = actingContext.getGraphManager().getNode(
+                Object object = polymorphicObjectFromCache(
                         oidQuery.getObjectId());
                 if (object != null) {
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index 5ee7531..dc82017 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -42,6 +42,7 @@
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityInheritanceTree;
 import org.apache.cayenne.map.LifecycleEvent;
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.query.EntityResultSegment;
@@ -159,9 +160,9 @@
 
             DataRow row = null;
 
-            if (cache != null && !oidQuery.isFetchMandatory()) {
-                row = cache.getCachedSnapshot(oidQuery.getObjectId());
-            }
+			if (cache != null && !oidQuery.isFetchMandatory()) {
+				row = polymorphicRowFromCache(oid);
+			}
 
             // refresh is forced or not found in cache
             if (row == null) {
@@ -181,6 +182,38 @@
 
         return !DONE;
     }
+    
+	private DataRow polymorphicRowFromCache(ObjectId superOid) {
+		DataRow row = cache.getCachedSnapshot(superOid);
+		if (row != null) {
+			return row;
+		}
+
+		EntityInheritanceTree inheritanceTree = domain.getEntityResolver().getInheritanceTree(superOid.getEntityName());
+		if (!inheritanceTree.getChildren().isEmpty()) {
+			row = polymorphicRowFromCache(inheritanceTree, superOid.getIdSnapshot());
+		}
+
+		return row;
+	}
+    
+	private DataRow polymorphicRowFromCache(EntityInheritanceTree superNode, Map<String, ?> idSnapshot) {
+
+		for (EntityInheritanceTree child : superNode.getChildren()) {
+			ObjectId id = new ObjectId(child.getEntity().getName(), idSnapshot);
+			DataRow row = cache.getCachedSnapshot(id);
+			if (row != null) {
+				return row;
+			}
+			
+			row = polymorphicRowFromCache(child, idSnapshot);
+			if (row != null) {
+				return row;
+			}
+		}
+
+		return null;
+	}
 
     private boolean interceptRelationshipQuery() {
 
@@ -226,7 +259,8 @@
                 return DONE;
             }
 
-            DataRow targetRow = cache.getCachedSnapshot(targetId);
+            // target id resolution (unlike source) should be polymorphic
+            DataRow targetRow = polymorphicRowFromCache(targetId);
 
             if (targetRow != null) {
                 this.response = new GenericResponse(Collections.singletonList(targetRow));
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
index 45b7176..73b4f78 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
@@ -88,7 +88,7 @@
         if (state == PersistenceState.COMMITTED || state == PersistenceState.DELETED
                 || state == PersistenceState.MODIFIED) {
 
-            ObjEntity entity = entityResolver.getObjEntity(entityName);
+            final ObjEntity entity = entityResolver.getObjEntity(entityName);
             final boolean lock = entity.getLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC;
 
             this.snapshot = new HashMap<>();
@@ -109,9 +109,10 @@
 
                 @Override
                 public boolean visitToOne(ToOneProperty property) {
-
+                    boolean isUsedForLocking = entity.getRelationship(property.getName()).isUsedForLocking();
+                    
                     // eagerly resolve optimistically locked relationships
-                    Object target = lock ? property.readProperty(object) : property.readPropertyDirectly(object);
+                    Object target = isUsedForLocking ? property.readProperty(object) : property.readPropertyDirectly(object);
 
                     if (target instanceof Persistent) {
                         target = ((Persistent) target).getObjectId();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
index 18a6174..9a0ffba 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
@@ -48,6 +48,7 @@
 
     private ObjectStore objectStore;
     private GraphDiff resolvedDiff;
+    private int lastSeenDiffId;
 
     ObjectStoreGraphDiff(ObjectStore objectStore) {
         this.objectStore = objectStore;
@@ -147,26 +148,31 @@
      * Converts diffs organized by ObjectId in a collection of diffs sorted by
      * diffId (same as creation order).
      */
-    private void resolveDiff() {
-        if (resolvedDiff == null) {
+	private void resolveDiff() {
 
-            CompoundDiff diff = new CompoundDiff();
-            Map<Object, ObjectDiff> changes = getChangesByObjectId();
+		// refresh the diff on first access or if the underlying ObjectStore has
+		// changed the the last time we cached the changes.
+		if (resolvedDiff == null || lastSeenDiffId < objectStore.currentDiffId) {
 
-            if (!changes.isEmpty()) {
-                List<NodeDiff> allChanges = new ArrayList<NodeDiff>(changes.size() * 2);
+			CompoundDiff diff = new CompoundDiff();
+			Map<Object, ObjectDiff> changes = getChangesByObjectId();
 
-                for (final ObjectDiff objectDiff : changes.values()) {
-                    objectDiff.appendDiffs(allChanges);
-                }
+			if (!changes.isEmpty()) {
+				List<NodeDiff> allChanges = new ArrayList<NodeDiff>(changes.size() * 2);
 
-                Collections.sort(allChanges);
-                diff.addAll(allChanges);
-            }
+				for (final ObjectDiff objectDiff : changes.values()) {
+					objectDiff.appendDiffs(allChanges);
+				}
 
-            this.resolvedDiff = diff;
-        }
-    }
+				Collections.sort(allChanges);
+				diff.addAll(allChanges);
+
+			}
+
+			this.lastSeenDiffId = objectStore.currentDiffId;
+			this.resolvedDiff = diff;
+		}
+	}
 
     private void preprocess(ObjectStore objectStore) {
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index faff51c..a97bc78 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -33,7 +33,7 @@
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.OptimisticLockException;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -53,13 +53,12 @@
 	protected BatchQuery query;
 	protected RowDescriptor keyRowDescriptor;
 
-	private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
+	private static void bind(DbAdapter adapter, PreparedStatement statement, DbAttributeBinding[] bindings)
 			throws SQLException, Exception {
 
-		for (ParameterBinding b : bindings) {
+		for (DbAttributeBinding b : bindings) {
 			if (!b.isExcluded()) {
-				adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
-						.getAttribute().getScale());
+				adapter.bindParameter(statement, b);
 			}
 		}
 	}
@@ -114,7 +113,7 @@
 		try (PreparedStatement statement = con.prepareStatement(sql);) {
 			for (BatchQueryRow row : query.getRows()) {
 
-				ParameterBinding[] bindings = translator.updateBindings(row);
+				DbAttributeBinding[] bindings = translator.updateBindings(row);
 				logger.logQueryParameters("batch bind", bindings);
 				bind(adapter, statement, bindings);
 
@@ -166,7 +165,7 @@
 				Statement.RETURN_GENERATED_KEYS) : connection.prepareStatement(queryStr);) {
 			for (BatchQueryRow row : query.getRows()) {
 
-				ParameterBinding[] bindings = translator.updateBindings(row);
+				DbAttributeBinding[] bindings = translator.updateBindings(row);
 				logger.logQueryParameters("bind", bindings);
 
 				bind(adapter, statement, bindings);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
index 97a1067..876c77e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
@@ -38,6 +38,8 @@
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -149,8 +151,8 @@
 
 		// for now supporting deprecated batch parameters...
 		@SuppressWarnings("unchecked")
-		Iterator<Map<String, ?>> it = (size > 0) ? query.parametersIterator() : IteratorUtils
-				.singletonIterator(Collections.emptyMap());
+		Iterator<Map<String, ?>> it = (size > 0) ? query.parametersIterator()
+				: IteratorUtils.singletonIterator(Collections.emptyMap());
 		for (int i = 0; i < batchSize; i++) {
 			Map<String, ?> nextParameters = it.next();
 
@@ -343,14 +345,23 @@
 	/**
 	 * Binds parameters to the PreparedStatement.
 	 */
-	protected void bind(PreparedStatement preparedStatement, SQLParameterBinding[] bindings) throws SQLException,
-			Exception {
+	protected void bind(PreparedStatement preparedStatement, SQLParameterBinding[] bindings)
+			throws SQLException, Exception {
 		// bind parameters
 		if (bindings.length > 0) {
 			int len = bindings.length;
 			for (int i = 0; i < len; i++) {
-				dataNode.getAdapter().bindParameter(preparedStatement, bindings[i].getValue(), i + 1,
-						bindings[i].getJdbcType(), bindings[i].getScale());
+
+				Object value = bindings[i].getValue();
+				ExtendedType extendedType = value != null
+						? getAdapter().getExtendedTypes().getRegisteredType(value.getClass())
+						: getAdapter().getExtendedTypes().getDefaultType();
+
+				ParameterBinding binding = new ParameterBinding(extendedType);
+				binding.setType(bindings[i].getJdbcType());
+				binding.setStatementPosition(i + 1);
+				binding.setValue(value);
+				dataNode.getAdapter().bindParameter(preparedStatement, binding);
 			}
 		}
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index f5ff8d1..fc84de6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -19,18 +19,12 @@
 
 package org.apache.cayenne.access.jdbc;
 
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.List;
-
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -39,6 +33,12 @@
 import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.SelectQuery;
 
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
 /**
  * A SQLAction that handles SelectQuery execution.
  * 
@@ -46,10 +46,10 @@
  */
 public class SelectAction extends BaseSQLAction {
 
-	private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
+	private static void bind(DbAdapter adapter, PreparedStatement statement, DbAttributeBinding[] bindings)
 			throws SQLException, Exception {
 
-		for (ParameterBinding b : bindings) {
+		for (DbAttributeBinding b : bindings) {
 
 			if (b.isExcluded()) {
 				continue;
@@ -62,8 +62,7 @@
 			if (b.getAttribute() == null) {
 				statement.setObject(b.getStatementPosition(), b.getValue());
 			} else {
-				adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
-						.getAttribute().getScale());
+				adapter.bindParameter(statement, b);
 			}
 		}
 
@@ -91,7 +90,7 @@
 		SelectTranslator translator = dataNode.selectTranslator(query);
 		final String sql = translator.getSql();
 
-		ParameterBinding[] bindings = translator.getBindings();
+		DbAttributeBinding[] bindings = translator.getBindings();
 		PreparedStatement statement = connection.prepareStatement(sql);
 		bind(dataNode.getAdapter(), statement, bindings);
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/DbAttributeBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/DbAttributeBinding.java
new file mode 100644
index 0000000..523c473
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/DbAttributeBinding.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.access.translator;
+
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Describes a PreparedStatement parameter binding mapped to a DbAttribute.
+ * 
+ * @since 4.0
+ */
+public class DbAttributeBinding extends ParameterBinding {
+
+	private final DbAttribute attribute;
+
+	public DbAttributeBinding(DbAttribute attribute, ExtendedType extendedType) {
+		super(extendedType);
+		this.attribute = attribute;
+	}
+
+	public DbAttribute getAttribute() {
+		return attribute;
+	}
+
+	@Override
+	public Integer getType() {
+		return super.getType() != null ? super.getType() : attribute.getType();
+	}
+
+	@Override
+	public int getScale() {
+		return getAttribute().getScale();
+	}
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
index 336dd3b..8ef6b28 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
@@ -19,10 +19,9 @@
 package org.apache.cayenne.access.translator;
 
 import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.map.DbAttribute;
 
 /**
- * Describes a PreparedStatement parameter binding mapped to a DbAttribute.
+ * Describes a PreparedStatement parameter generic binding.
  * 
  * @since 4.0
  */
@@ -30,21 +29,17 @@
 
 	static final int EXCLUDED_POSITION = -1;
 
-	private DbAttribute attribute;
 	private Object value;
 	private int statementPosition;
 	private ExtendedType extendedType;
+	private Integer type;
+	private int scale;
 
-	public ParameterBinding(DbAttribute attribute, ExtendedType extendedType) {
-		this.attribute = attribute;
+	public ParameterBinding(ExtendedType extendedType) {
 		this.statementPosition = EXCLUDED_POSITION;
 		this.extendedType = extendedType;
 	}
 
-	public DbAttribute getAttribute() {
-		return attribute;
-	}
-
 	public Object getValue() {
 		return value;
 	}
@@ -85,4 +80,20 @@
 		this.statementPosition = statementPosition;
 		this.value = value;
 	}
+
+	public Integer getType() {
+		return type;
+	}
+
+	public void setType(Integer type) {
+		this.type = type;
+	}
+
+	public int getScale() {
+		return scale;
+	}
+
+	public void setScale(int scale) {
+		this.scale = scale;
+	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ProcedureParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ProcedureParameterBinding.java
new file mode 100644
index 0000000..df15c96
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ProcedureParameterBinding.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
+ * <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.access.translator;
+
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.map.ProcedureParameter;
+
+/**
+ * Describes a PreparedStatement parameter binding mapped to a DbAttribute.
+ *
+ * @since 4.0
+ */
+public class ProcedureParameterBinding extends ParameterBinding {
+
+	private final ProcedureParameter parameter;
+
+	public ProcedureParameterBinding(ProcedureParameter procedureParameter, ExtendedType extendedType) {
+		super(extendedType);
+		this.parameter = procedureParameter;
+	}
+
+	public ProcedureParameter getParameter() {
+		return parameter;
+	}
+
+	@Override
+	public Integer getType() {
+		return parameter.getType();
+	}
+
+	@Override
+	public int getScale() {
+		return parameter.getPrecision();
+	}
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
index 04a4902..66fe58c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.query.BatchQueryRow;
 
 /**
@@ -37,7 +37,7 @@
     /**
      * Returns the widest possible array of bindings for this query.
      */
-    ParameterBinding[] getBindings();
+    DbAttributeBinding[] getBindings();
 
     /**
      * Updates internal bindings to be used with a given row, returning updated
@@ -47,5 +47,5 @@
      * parameter). Usually the returned array is actually the same object reused
      * for every iteration, only with changed object state.
      */
-    ParameterBinding[] updateBindings(BatchQueryRow row);
+    DbAttributeBinding[] updateBindings(BatchQueryRow row);
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
index 732ec64..70c2a27 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
@@ -20,7 +20,7 @@
 
 import java.sql.Types;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -40,7 +40,7 @@
 
     protected boolean translated;
     protected String sql;
-    protected ParameterBinding[] bindings;
+    protected DbAttributeBinding[] bindings;
 
     public DefaultBatchTranslator(BatchQuery query, DbAdapter adapter, String trimFunction) {
         this.query = query;
@@ -67,22 +67,22 @@
     }
 
     @Override
-    public ParameterBinding[] getBindings() {
+    public DbAttributeBinding[] getBindings() {
         ensureTranslated();
         return bindings;
     }
     
     @Override
-    public ParameterBinding[] updateBindings(BatchQueryRow row) {
+    public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
         ensureTranslated();
         return doUpdateBindings(row);
     }
 
     protected abstract String createSql();
 
-    protected abstract ParameterBinding[] createBindings();
+    protected abstract DbAttributeBinding[] createBindings();
     
-    protected abstract ParameterBinding[] doUpdateBindings(BatchQueryRow row);
+    protected abstract DbAttributeBinding[] doUpdateBindings(BatchQueryRow row);
 
     /**
      * Appends the name of the column to the query buffer. Subclasses use this
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
index 4f63e5f..9660b3f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
@@ -74,26 +74,26 @@
     }
 
     @Override
-    protected ParameterBinding[] createBindings() {
+    protected DbAttributeBinding[] createBindings() {
         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
         List<DbAttribute> attributes = deleteBatch.getDbAttributes();
         int len = attributes.size();
 
-        ParameterBinding[] bindings = new ParameterBinding[len];
+        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
 
             String typeName = TypesMapping.getJavaBySqlType(a.getType());
             ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
-            bindings[i] = new ParameterBinding(a, extendedType);
+            bindings[i] = new DbAttributeBinding(a, extendedType);
         }
 
         return bindings;
     }
 
     @Override
-    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
 
         int len = bindings.length;
 
@@ -101,7 +101,7 @@
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            ParameterBinding b = bindings[i];
+            DbAttributeBinding b = bindings[i];
 
             // skip null attributes... they are translated as "IS NULL"
             if (deleteBatch.isNull(b.getAttribute())) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
index 920b29f..cf52929 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
@@ -84,18 +84,18 @@
     }
 
     @Override
-    protected ParameterBinding[] createBindings() {
+    protected DbAttributeBinding[] createBindings() {
         List<DbAttribute> attributes = query.getDbAttributes();
         int len = attributes.size();
 
-        ParameterBinding[] bindings = new ParameterBinding[len];
+        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
 
             String typeName = TypesMapping.getJavaBySqlType(a.getType());
             ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
-            bindings[i] = new ParameterBinding(a, extendedType);
+            bindings[i] = new DbAttributeBinding(a, extendedType);
 
             // include/exclude state depends on DbAttribute only and can be
             // precompiled here
@@ -112,12 +112,12 @@
     }
 
     @Override
-    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
         int len = bindings.length;
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            ParameterBinding b = bindings[i];
+            DbAttributeBinding b = bindings[i];
 
             // exclusions are permanent
             if (!b.isExcluded()) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
index 2a3b2f3..514ca26 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
@@ -18,7 +18,7 @@
  ****************************************************************/

 package org.apache.cayenne.access.translator.batch;

 

-import org.apache.cayenne.access.translator.ParameterBinding;

+import org.apache.cayenne.access.translator.DbAttributeBinding;

 import org.apache.cayenne.access.types.ExtendedType;

 import org.apache.cayenne.dba.DbAdapter;

 import org.apache.cayenne.dba.QuotingStrategy;

@@ -56,19 +56,19 @@
     }

 

     @Override

-    protected ParameterBinding[] createBindings() {

+    protected DbAttributeBinding[] createBindings() {

 

-        ParameterBinding[] superBindings = super.createBindings();

+        DbAttributeBinding[] superBindings = super.createBindings();

 

         int slen = superBindings.length;

 

-        ParameterBinding[] bindings = new ParameterBinding[slen + 1];

+        DbAttributeBinding[] bindings = new DbAttributeBinding[slen + 1];

 

         DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName);

         String typeName = TypesMapping.getJavaBySqlType(deleteAttribute.getType());

         ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);

 

-        bindings[0] = new ParameterBinding(deleteAttribute, extendedType);

+        bindings[0] = new DbAttributeBinding(deleteAttribute, extendedType);

         bindings[0].include(1, true);

         

         System.arraycopy(superBindings, 0, bindings, 1, slen);

@@ -77,7 +77,7 @@
     }

 

     @Override

-    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {

+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {

         int len = bindings.length;

 

         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;

@@ -85,7 +85,7 @@
         // skip position 0... Otherwise follow super algorithm

         for (int i = 1, j = 2; i < len; i++) {

 

-            ParameterBinding b = bindings[i];

+            DbAttributeBinding b = bindings[i];

 

             // skip null attributes... they are translated as "IS NULL"

             if (deleteBatch.isNull(b.getAttribute())) {

diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
index 0ad6037..cb34b99 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
@@ -81,7 +81,7 @@
     }
 
     @Override
-    protected ParameterBinding[] createBindings() {
+    protected DbAttributeBinding[] createBindings() {
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 
         List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
@@ -90,14 +90,14 @@
         int ul = updatedDbAttributes.size();
         int ql = qualifierAttributes.size();
 
-        ParameterBinding[] bindings = new ParameterBinding[ul + ql];
+        DbAttributeBinding[] bindings = new DbAttributeBinding[ul + ql];
 
         for (int i = 0; i < ul; i++) {
             DbAttribute a = updatedDbAttributes.get(i);
 
             String typeName = TypesMapping.getJavaBySqlType(a.getType());
             ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
-            bindings[i] = new ParameterBinding(a, extendedType);
+            bindings[i] = new DbAttributeBinding(a, extendedType);
         }
 
         for (int i = 0; i < ql; i++) {
@@ -105,14 +105,14 @@
 
             String typeName = TypesMapping.getJavaBySqlType(a.getType());
             ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
-            bindings[ul + i] = new ParameterBinding(a, extendedType);
+            bindings[ul + i] = new DbAttributeBinding(a, extendedType);
         }
 
         return bindings;
     }
 
     @Override
-    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
 
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
index 12d291e..c5c04f9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
@@ -1,31 +1,26 @@
 /*****************************************************************
- *   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.
+ * 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.access.translator.procedure;
 
-import java.sql.CallableStatement;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
+import org.apache.cayenne.access.translator.ProcedureParameterBinding;
+import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.log.NoopJdbcEventLogger;
@@ -34,218 +29,225 @@
 import org.apache.cayenne.map.ProcedureParameter;
 import org.apache.cayenne.query.ProcedureQuery;
 
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
 /**
  * Stored procedure query translator.
  */
 public class ProcedureTranslator {
 
-    /**
-     * Helper class to make OUT and VOID parameters logger-friendly.
-     */
-    static class NotInParam {
+	/**
+	 * Helper class to make OUT and VOID parameters logger-friendly.
+	 */
+	static class NotInParam {
 
-        protected String type;
+		protected String type;
 
-        public NotInParam(String type) {
-            this.type = type;
-        }
+		public NotInParam(String type) {
+			this.type = type;
+		}
 
-        @Override
-        public String toString() {
-            return type;
-        }
-    }
+		@Override
+		public String toString() {
+			return type;
+		}
+	}
 
-    private static NotInParam OUT_PARAM = new NotInParam("[OUT]");
+	private static NotInParam OUT_PARAM = new NotInParam("[OUT]");
 
-    protected ProcedureQuery query;
-    protected Connection connection;
-    protected DbAdapter adapter;
-    protected EntityResolver entityResolver;
-    protected List<ProcedureParameter> callParams;
-    protected List<Object> values;
-    protected JdbcEventLogger logger;
+	protected ProcedureQuery query;
+	protected Connection connection;
+	protected DbAdapter adapter;
+	protected EntityResolver entityResolver;
+	protected List<ProcedureParameter> callParams;
+	protected List<Object> values;
+	protected JdbcEventLogger logger;
 
-    public ProcedureTranslator() {
-        this.logger = NoopJdbcEventLogger.getInstance();
-    }
+	public ProcedureTranslator() {
+		this.logger = NoopJdbcEventLogger.getInstance();
+	}
 
-    public void setQuery(ProcedureQuery query) {
-        this.query = query;
-    }
+	public void setQuery(ProcedureQuery query) {
+		this.query = query;
+	}
 
-    public void setConnection(Connection connection) {
-        this.connection = connection;
-    }
+	public void setConnection(Connection connection) {
+		this.connection = connection;
+	}
 
-    public void setAdapter(DbAdapter adapter) {
-        this.adapter = adapter;
-    }
+	public void setAdapter(DbAdapter adapter) {
+		this.adapter = adapter;
+	}
 
-    /**
-     * @since 3.1
-     */
-    public void setJdbcEventLogger(JdbcEventLogger logger) {
-        this.logger = logger;
-    }
+	/**
+	 * @since 3.1
+	 */
+	public void setJdbcEventLogger(JdbcEventLogger logger) {
+		this.logger = logger;
+	}
 
-    /**
-     * @since 3.1
-     */
-    public JdbcEventLogger getJdbcEventLogger() {
-        return logger;
-    }
+	/**
+	 * @since 3.1
+	 */
+	public JdbcEventLogger getJdbcEventLogger() {
+		return logger;
+	}
 
-    /**
-     * @since 1.2
-     */
-    public void setEntityResolver(EntityResolver entityResolver) {
-        this.entityResolver = entityResolver;
-    }
+	/**
+	 * @since 1.2
+	 */
+	public void setEntityResolver(EntityResolver entityResolver) {
+		this.entityResolver = entityResolver;
+	}
 
-    /**
-     * Creates an SQL String for the stored procedure call.
-     */
-    protected String createSqlString() {
-        Procedure procedure = getProcedure();
+	/**
+	 * Creates an SQL String for the stored procedure call.
+	 */
+	protected String createSqlString() {
+		Procedure procedure = getProcedure();
 
-        StringBuilder buf = new StringBuilder();
+		StringBuilder buf = new StringBuilder();
 
-        int totalParams = callParams.size();
+		int totalParams = callParams.size();
 
-        // check if procedure returns values
-        if (procedure.isReturningValue()) {
-            totalParams--;
-            buf.append("{? = call ");
-        }
-        else {
-            buf.append("{call ");
-        }
+		// check if procedure returns values
+		if (procedure.isReturningValue()) {
+			totalParams--;
+			buf.append("{? = call ");
+		} else {
+			buf.append("{call ");
+		}
 
-        buf.append(procedure.getFullyQualifiedName());
+		buf.append(procedure.getFullyQualifiedName());
 
-        if (totalParams > 0) {
-            // unroll the loop
-            buf.append("(?");
+		if (totalParams > 0) {
+			// unroll the loop
+			buf.append("(?");
 
-            for (int i = 1; i < totalParams; i++) {
-                buf.append(", ?");
-            }
+			for (int i = 1; i < totalParams; i++) {
+				buf.append(", ?");
+			}
 
-            buf.append(")");
-        }
+			buf.append(")");
+		}
 
-        buf.append("}");
-        return buf.toString();
-    }
+		buf.append("}");
+		return buf.toString();
+	}
 
-    /**
-     * Creates and binds a PreparedStatement to execute query SQL via JDBC.
-     */
-    public PreparedStatement createStatement() throws Exception {
-        long t1 = System.currentTimeMillis();
+	/**
+	 * Creates and binds a PreparedStatement to execute query SQL via JDBC.
+	 */
+	public PreparedStatement createStatement() throws Exception {
+		long t1 = System.currentTimeMillis();
 
-        this.callParams = getProcedure().getCallParameters();
-        this.values = new ArrayList<Object>(callParams.size());
+		this.callParams = getProcedure().getCallParameters();
+		this.values = new ArrayList<Object>(callParams.size());
 
-        initValues();
-        String sqlStr = createSqlString();
+		initValues();
+		String sqlStr = createSqlString();
 
-        if (logger.isLoggable()) {
-            // need to convert OUT/VOID parameters to loggable strings
-            long time = System.currentTimeMillis() - t1;
+		if (logger.isLoggable()) {
+			// need to convert OUT/VOID parameters to loggable strings
+			long time = System.currentTimeMillis() - t1;
 
-            List<Object> loggableParameters = new ArrayList<Object>(values.size());
-            for (Object val : values) {
-                if (val instanceof NotInParam) {
-                    val = val.toString();
-                }
-                loggableParameters.add(val);
-            }
+			List<Object> loggableParameters = new ArrayList<Object>(values.size());
+			for (Object val : values) {
+				if (val instanceof NotInParam) {
+					val = val.toString();
+				}
+				loggableParameters.add(val);
+			}
 
-            // FIXME: compute proper attributes via callParams
-            logger.logQuery(sqlStr, null, loggableParameters, time);
-        }
-        CallableStatement stmt = connection.prepareCall(sqlStr);
-        initStatement(stmt);
-        return stmt;
-    }
+			// FIXME: compute proper attributes via callParams
+			logger.logQuery(sqlStr, null, loggableParameters, time);
+		}
+		CallableStatement stmt = connection.prepareCall(sqlStr);
+		initStatement(stmt);
+		return stmt;
+	}
 
-    public Procedure getProcedure() {
-        return query.getMetaData(entityResolver).getProcedure();
-    }
+	public Procedure getProcedure() {
+		return query.getMetaData(entityResolver).getProcedure();
+	}
 
-    public ProcedureQuery getProcedureQuery() {
-        return query;
-    }
+	public ProcedureQuery getProcedureQuery() {
+		return query;
+	}
 
-    /**
-     * Set IN and OUT parameters.
-     */
-    protected void initStatement(CallableStatement stmt) throws Exception {
-        if (values != null && values.size() > 0) {
-            List<ProcedureParameter> params = getProcedure().getCallParameters();
+	/**
+	 * Set IN and OUT parameters.
+	 */
+	protected void initStatement(CallableStatement stmt) throws Exception {
+		if (values != null && values.size() > 0) {
+			List<ProcedureParameter> params = getProcedure().getCallParameters();
 
-            int len = values.size();
-            for (int i = 0; i < len; i++) {
-                ProcedureParameter param = params.get(i);
+			int len = values.size();
+			for (int i = 0; i < len; i++) {
+				ProcedureParameter param = params.get(i);
 
-                // !Stored procedure parameter can be both in and out
-                // at the same time
-                if (param.isOutParam()) {
-                    setOutParam(stmt, param, i + 1);
-                }
+				// !Stored procedure parameter can be both in and out
+				// at the same time
+				if (param.isOutParam()) {
+					setOutParam(stmt, param, i + 1);
+				}
 
-                if (param.isInParameter()) {
-                    setInParam(stmt, param, values.get(i), i + 1);
-                }
-            }
-        }
-    }
+				if (param.isInParameter()) {
+					setInParam(stmt, param, values.get(i), i + 1);
+				}
+			}
+		}
+	}
 
-    protected void initValues() {
-        Map<String, ?> queryValues = getProcedureQuery().getParameters();
+	protected void initValues() {
+		Map<String, ?> queryValues = getProcedureQuery().getParameters();
 
-        // match values with parameters in the correct order.
-        // make an assumption that a missing value is NULL
-        // Any reason why this is bad?
+		// match values with parameters in the correct order.
+		// make an assumption that a missing value is NULL
+		// Any reason why this is bad?
 
-        for (ProcedureParameter param : callParams) {
+		for (ProcedureParameter param : callParams) {
 
-            if (param.getDirection() == ProcedureParameter.OUT_PARAMETER) {
-                values.add(OUT_PARAM);
-            }
-            else {
-                values.add(queryValues.get(param.getName()));
-            }
-        }
-    }
+			if (param.getDirection() == ProcedureParameter.OUT_PARAMETER) {
+				values.add(OUT_PARAM);
+			} else {
+				values.add(queryValues.get(param.getName()));
+			}
+		}
+	}
 
-    /**
-     * Sets a single IN parameter of the CallableStatement.
-     */
-    protected void setInParam(
-            CallableStatement stmt,
-            ProcedureParameter param,
-            Object val,
-            int pos) throws Exception {
+	/**
+	 * Sets a single IN parameter of the CallableStatement.
+	 */
+	protected void setInParam(
+			CallableStatement stmt,
+			ProcedureParameter param,
+			Object val,
+			int pos) throws Exception {
+		ExtendedType extendedType = val != null ? adapter.getExtendedTypes().getRegisteredType(val.getClass())
+				: adapter.getExtendedTypes().getDefaultType();
+		ProcedureParameterBinding binding = new ProcedureParameterBinding(param, extendedType);
+		binding.setValue(val);
+		binding.setStatementPosition(pos);
+		adapter.bindParameter(stmt, binding);
+	}
 
-        int type = param.getType();
-        adapter.bindParameter(stmt, val, pos, type, param.getPrecision());
-    }
+	/**
+	 * Sets a single OUT parameter of the CallableStatement.
+	 */
+	protected void setOutParam(CallableStatement stmt, ProcedureParameter param, int pos)
+			throws Exception {
 
-    /**
-     * Sets a single OUT parameter of the CallableStatement.
-     */
-    protected void setOutParam(CallableStatement stmt, ProcedureParameter param, int pos)
-            throws Exception {
-
-        int precision = param.getPrecision();
-        if (precision >= 0) {
-            stmt.registerOutParameter(pos, param.getType(), precision);
-        }
-        else {
-            stmt.registerOutParameter(pos, param.getType());
-        }
-    }
+		int precision = param.getPrecision();
+		if (precision >= 0) {
+			stmt.registerOutParameter(pos, param.getType(), precision);
+		} else {
+			stmt.registerOutParameter(pos, param.getType());
+		}
+	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
index 59ca220..03949c6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -45,7 +45,7 @@
 	protected String sql;
 	protected DbAdapter adapter;
 	protected EntityResolver entityResolver;
-	protected List<ParameterBinding> bindings;
+	protected List<DbAttributeBinding> bindings;
 
 	/**
 	 * @since 4.0
@@ -55,7 +55,7 @@
 		this.adapter = adapter;
 		this.query = query;
 		this.queryMetadata = query.getMetaData(entityResolver);
-		this.bindings = new ArrayList<ParameterBinding>();
+		this.bindings = new ArrayList<DbAttributeBinding>();
 	}
 
 	/**
@@ -150,10 +150,11 @@
 	 *            DbAttribute being processed.
 	 */
 	public void addToParamList(DbAttribute dbAttr, Object anObject) {
-		String typeName = TypesMapping.getJavaBySqlType(dbAttr.getType());
+		String typeName = TypesMapping.SQL_NULL;
+		if (dbAttr != null) typeName = TypesMapping.getJavaBySqlType(dbAttr.getType());
 		ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
 		
-		ParameterBinding binding = new ParameterBinding(dbAttr, extendedType);
+		DbAttributeBinding binding = new DbAttributeBinding(dbAttr, extendedType);
 		binding.setValue(anObject);
 		binding.setStatementPosition(bindings.size() + 1);
 		bindings.add(binding);
@@ -162,7 +163,7 @@
 	/**
 	 * @since 4.0
 	 */
-	public ParameterBinding[] getBindings() {
-		return bindings.toArray(new ParameterBinding[bindings.size()]);
+	public DbAttributeBinding[] getBindings() {
+		return bindings.toArray(new DbAttributeBinding[bindings.size()]);
 	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
index d652885..6554565 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
@@ -22,7 +22,7 @@
 import java.util.Map;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.query.SelectQuery;
 
@@ -35,7 +35,7 @@
 
 	String getSql() throws Exception;
 
-	ParameterBinding[] getBindings();
+	DbAttributeBinding[] getBindings();
 
 	Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides();
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/EnumType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/EnumType.java
index 55cc89e..1261a26 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/EnumType.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/EnumType.java
@@ -40,6 +40,7 @@
 
     protected Class<T> enumClass;
     protected Object[] values;
+    protected String canonicalName;
 
     public EnumType(Class<T> enumClass) {
         if (enumClass == null) {
@@ -47,6 +48,7 @@
         }
 
         this.enumClass = enumClass;
+        this.canonicalName = enumClass.getCanonicalName();
 
         try {
             Method m = enumClass.getMethod("values");
@@ -61,7 +63,7 @@
 
     @Override
     public String getClassName() {
-        return enumClass.getName();
+        return canonicalName;
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedTypeMap.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedTypeMap.java
index 08c51db..387fe77 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedTypeMap.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedTypeMap.java
@@ -49,6 +49,7 @@
 		classesForPrimitives.put("int", Integer.class.getName());
 	}
 
+	protected Map<String, String> typeAliases;
 	protected final Map<String, ExtendedType> typeMap;
 	protected ExtendedType defaultType;
 
@@ -67,6 +68,7 @@
 	public ExtendedTypeMap() {
 		this.defaultType = new ObjectType();
 		this.typeMap = new ConcurrentHashMap<>();
+		this.typeAliases = new ConcurrentHashMap<>(classesForPrimitives);
 		this.extendedTypeFactories = new CopyOnWriteArrayList<>();
 		this.internalTypeFactories = new CopyOnWriteArrayList<>();
 
@@ -168,13 +170,9 @@
 			return getDefaultType();
 		}
 
-		String nonPrimitive = classesForPrimitives.get(javaClassName);
-		if (nonPrimitive != null) {
-			javaClassName = nonPrimitive;
-		}
+		javaClassName = canonicalizedTypeName(javaClassName);
 
 		ExtendedType type = getExplictlyRegisteredType(javaClassName);
-
 		if (type != null) {
 			return type;
 		}
@@ -276,4 +274,29 @@
 
 		return null;
 	}
+	
+	/**
+	 * For the class name returns a name "canonicalized" for the purpose of
+	 * ExtendedType lookup.
+	 * 
+	 * @since 4.0
+	 */
+	protected String canonicalizedTypeName(String className) {
+
+		String canonicalized = typeAliases.get(className);
+		if (canonicalized == null) {
+
+			int index = className.indexOf('$');
+			if (index >= 0) {
+				canonicalized = className.replace('$', '.');
+			} else {
+				canonicalized = className;
+			}
+
+			typeAliases.put(className, canonicalized);
+
+		}
+
+		return canonicalized;
+	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/Constants.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/Constants.java
index e49f434..6742863 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/Constants.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/Constants.java
@@ -152,6 +152,14 @@
 
 	public static final String ROP_SERVICE_PASSWORD_PROPERTY = "cayenne.rop.service_password";
 
+	public static final String ROP_SERVICE_REALM_PROPERTY = "cayenne.rop.service_realm";
+
+	/**
+	 * A boolean property that defines whether ALPN should be used.
+	 * Possible values are "true" or "false".
+	 */
+	public static final String ROP_SERVICE_USE_ALPN_PROPERTY = "cayenne.rop.service_use_alpn";
+
 	public static final String ROP_SERVICE_SHARED_SESSION_PROPERTY = "cayenne.rop.shared_session_name";
 
 	public static final String ROP_SERVICE_TIMEOUT_PROPERTY = "cayenne.rop.service_timeout";
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataChannelDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataChannelDescriptor.java
index 804f4b6..bc3929b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataChannelDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataChannelDescriptor.java
@@ -18,6 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.configuration;
 
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.resource.Resource;
+import org.apache.cayenne.util.XMLEncoder;
+import org.apache.cayenne.util.XMLSerializable;
+
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -26,11 +31,6 @@
 import java.util.List;
 import java.util.Map;
 
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.resource.Resource;
-import org.apache.cayenne.util.XMLEncoder;
-import org.apache.cayenne.util.XMLSerializable;
-
 /**
  * A descriptor of a DataChannel normally loaded from XML configuration.
  * 
@@ -44,7 +44,7 @@
 	protected Map<String, String> properties;
 	protected Collection<DataMap> dataMaps;
 	protected Collection<DataNodeDescriptor> nodeDescriptors;
-	protected Resource configurationSource;
+	protected transient Resource configurationSource;
 	protected String defaultNodeName;
 
 	public DataChannelDescriptor() {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataNodeDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataNodeDescriptor.java
index 1aeb420..e3c4aef 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataNodeDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/DataNodeDescriptor.java
@@ -18,18 +18,18 @@
  ****************************************************************/
 package org.apache.cayenne.configuration;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory;
 import org.apache.cayenne.conn.DataSourceInfo;
 import org.apache.cayenne.resource.Resource;
 import org.apache.cayenne.util.XMLEncoder;
 import org.apache.cayenne.util.XMLSerializable;
+
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * A descriptor of {@link DataNode} configuration.
@@ -51,7 +51,7 @@
     // (DataSourceDescriptor?)
     protected DataSourceInfo dataSourceDescriptor;
 
-    protected Resource configurationSource;
+    protected transient Resource configurationSource;
 
     /**
      * @since 3.1
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainLoadException.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainLoadException.java
index 34434f0..37b5fc2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainLoadException.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainLoadException.java
@@ -27,33 +27,33 @@
  */
 public class DataDomainLoadException extends ConfigurationException {
 
-    private ConfigurationTree<DataChannelDescriptor> configurationTree;
+	private static final long serialVersionUID = 7969847819485380271L;
+	
+	private ConfigurationTree<DataChannelDescriptor> configurationTree;
 
-    public DataDomainLoadException() {
-    }
+	public DataDomainLoadException() {
+	}
 
-    public DataDomainLoadException(String messageFormat, Object... messageArgs) {
-        super(messageFormat, messageArgs);
-    }
+	public DataDomainLoadException(String messageFormat, Object... messageArgs) {
+		super(messageFormat, messageArgs);
+	}
 
-    public DataDomainLoadException(
-            ConfigurationTree<DataChannelDescriptor> configurationTree,
-            String messageFormat, Object... messageArgs) {
-        super(messageFormat, messageArgs);
-        this.configurationTree = configurationTree;
-    }
+	public DataDomainLoadException(ConfigurationTree<DataChannelDescriptor> configurationTree, String messageFormat,
+			Object... messageArgs) {
+		super(messageFormat, messageArgs);
+		this.configurationTree = configurationTree;
+	}
 
-    public DataDomainLoadException(Throwable cause) {
-        super(cause);
-    }
+	public DataDomainLoadException(Throwable cause) {
+		super(cause);
+	}
 
-    public DataDomainLoadException(String messageFormat, Throwable cause,
-            Object... messageArgs) {
-        super(messageFormat, cause, messageArgs);
-    }
+	public DataDomainLoadException(String messageFormat, Throwable cause, Object... messageArgs) {
+		super(messageFormat, cause, messageArgs);
+	}
 
-    public ConfigurationTree<DataChannelDescriptor> getConfigurationTree() {
-        return configurationTree;
-    }
+	public ConfigurationTree<DataChannelDescriptor> getConfigurationTree() {
+		return configurationTree;
+	}
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
index 4815fe4..b7b34c7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
@@ -18,9 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.configuration.server;
 
-import java.util.Collection;
-import java.util.List;
-
 import org.apache.cayenne.ConfigurationException;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataChannelFilter;
@@ -46,6 +43,9 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import java.util.Collection;
+import java.util.List;
+
 /**
  * A {@link DataChannel} provider that provides a single instance of DataDomain
  * configured per configuration supplied via injected
@@ -92,7 +92,9 @@
 		} catch (ConfigurationException e) {
 			throw e;
 		} catch (Exception e) {
-			throw new DataDomainLoadException("Error loading DataChannel: '%s'", e, e.getMessage());
+			String causeMessage = e.getMessage();
+			String message = causeMessage != null && causeMessage.length() > 0 ? causeMessage : e.getClass().getName();
+			throw new DataDomainLoadException("DataDomain startup failed: %s", e, message);
 		}
 	}
 
@@ -102,18 +104,7 @@
 
 	protected DataDomain createAndInitDataDomain() throws Exception {
 
-		DataChannelDescriptor descriptor;
-
-		if (locations.isEmpty()) {
-			descriptor = new DataChannelDescriptor();
-		} else {
-			descriptor = descriptorFromConfigs();
-		}
-
-		String nameOverride = runtimeProperties.get(Constants.SERVER_DOMAIN_NAME_PROPERTY);
-		if (nameOverride != null) {
-			descriptor.setName(nameOverride);
-		}
+		DataChannelDescriptor descriptor = loadDescriptor();
 
 		DataDomain dataDomain = createDataDomain(descriptor.getName());
 
@@ -165,6 +156,20 @@
 
 	/**
 	 * @since 4.0
+     */
+	protected DataChannelDescriptor loadDescriptor() {
+		DataChannelDescriptor descriptor = locations.isEmpty() ? new DataChannelDescriptor() : loadDescriptorFromConfigs();
+
+		String nameOverride = runtimeProperties.get(Constants.SERVER_DOMAIN_NAME_PROPERTY);
+		if (nameOverride != null) {
+			descriptor.setName(nameOverride);
+		}
+
+		return descriptor;
+	}
+
+	/**
+	 * @since 4.0
 	 */
 	protected DataNode addDataNode(DataDomain dataDomain, DataNodeDescriptor nodeDescriptor) throws Exception {
 		DataNode dataNode = dataNodeFactory.createDataNode(nodeDescriptor);
@@ -178,7 +183,7 @@
 		return dataNode;
 	}
 
-	private DataChannelDescriptor descriptorFromConfigs() {
+	private DataChannelDescriptor loadDescriptorFromConfigs() {
 
 		long t0 = System.currentTimeMillis();
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
index ef3f1ab..36516d9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
@@ -203,10 +203,8 @@
 		binder.bind(RuntimeProperties.class).to(DefaultRuntimeProperties.class);
 
 		// a service to load DataSourceFactories. DelegatingDataSourceFactory
-		// will attempt
-		// to find the actual worker factory dynamically on each call depending
-		// on
-		// DataNodeDescriptor data and the environment
+		// will attempt to find the actual worker factory dynamically on each
+		// call depending on DataNodeDescriptor data and the environment
 		binder.bind(DataSourceFactory.class).to(DelegatingDataSourceFactory.class);
 
 		// a default SchemaUpdateStrategy (used when no explicit strategy is
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java b/cayenne-server/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
index 3a794de..367a6e0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
@@ -55,6 +55,18 @@
 		return dataSourceManager;
 	}
 
+	int poolSize() {
+		return dataSourceManager.getDataSource().poolSize();
+	}
+
+	int availableSize() {
+		return dataSourceManager.getDataSource().availableSize();
+	}
+	
+	int canExpandSize() {
+		return dataSourceManager.getDataSource().canExpandSize();
+	}
+
 	/**
 	 * Calls {@link #shutdown()} to drain the underlying pool, close open
 	 * connections and block the DataSource from creating any new connections.
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/datasource/UnmanagedPoolingDataSource.java b/cayenne-server/src/main/java/org/apache/cayenne/datasource/UnmanagedPoolingDataSource.java
index 0ffd34c..2d0e6e2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/datasource/UnmanagedPoolingDataSource.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/datasource/UnmanagedPoolingDataSource.java
@@ -136,7 +136,7 @@
 		this.poolCap = new Semaphore(maxConnections);
 		this.maxIdleConnections = maxIdleConnections(minConnections, maxConnections);
 
-		// grow pull to min connections
+		// grow pool to min connections
 		try {
 			for (int i = 0; i < minConnections; i++) {
 				PoolAwareConnection c = createUnchecked();
@@ -157,6 +157,10 @@
 		return available.size();
 	}
 
+	int canExpandSize() {
+		return poolCap.availablePermits();
+	}
+
 	@Override
 	public void close() {
 
@@ -264,7 +268,13 @@
 			return null;
 		}
 
-		PoolAwareConnection c = createWrapped();
+		PoolAwareConnection c;
+		try {
+			c = createWrapped();
+		} catch (SQLException e) {
+			poolCap.release();
+			throw e;
+		}
 
 		pool.put(c, 1);
 
@@ -335,8 +345,11 @@
 			return resetState(c);
 		}
 
-		throw new ConnectionUnavailableException(
-				"Can't obtain connection. Request to pool timed out. Total pool size: " + pool.size());
+		int poolSize = poolSize();
+		int canGrow = poolCap.availablePermits();
+
+		throw new ConnectionUnavailableException("Can't obtain connection. Request to pool timed out. Total pool size: "
+				+ poolSize + ", can expand by: " + canGrow);
 	}
 
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
index 0976763..a8876f6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
@@ -19,12 +19,9 @@
 
 package org.apache.cayenne.dba;
 
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.util.Collection;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
@@ -41,6 +38,10 @@
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
 
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
 /**
  * A DbAdapter that automatically detects the kind of database it is running on
  * and instantiates an appropriate DB-specific adapter, delegating all
@@ -199,9 +200,9 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int precision)
+	public void bindParameter(PreparedStatement statement, ParameterBinding parameterBinding)
 			throws SQLException, Exception {
-		getAdapter().bindParameter(statement, object, pos, sqlType, precision);
+		getAdapter().bindParameter(statement, parameterBinding);
 	}
 
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
index b71a741..d72bc31 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
@@ -1,24 +1,25 @@
 /*****************************************************************
- *   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.
+ * 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.dba;
 
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
@@ -48,14 +49,14 @@
 	/**
 	 * Returns a String used to terminate a batch in command-line tools. E.g.
 	 * ";" on Oracle or "go" on Sybase.
-	 * 
+	 *
 	 * @since 1.0.4
 	 */
 	String getBatchTerminator();
 
 	/**
 	 * Returns a SelectTranslator that works with the adapter target database.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver);
@@ -64,7 +65,7 @@
 
 	/**
 	 * Returns an instance of SQLAction that should handle the query.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	SQLAction getAction(Query query, DataNode node);
@@ -87,7 +88,7 @@
 	/**
 	 * Returns true if a target database supports key autogeneration. This
 	 * feature also requires JDBC3-compliant driver.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	boolean supportsGeneratedKeys();
@@ -101,7 +102,7 @@
 
 	/**
 	 * Returns a collection of SQL statements needed to drop a database table.
-	 * 
+	 *
 	 * @since 3.0
 	 */
 	Collection<String> dropTableStatements(DbEntity table);
@@ -115,7 +116,7 @@
 	/**
 	 * Returns a DDL string to create a unique constraint over a set of columns,
 	 * or null if the unique constraints are not supported.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	String createUniqueConstraint(DbEntity source, Collection<DbAttribute> columns);
@@ -146,7 +147,7 @@
 	/**
 	 * Creates and returns a DbAttribute based on supplied parameters (usually
 	 * obtained from database meta data).
-	 * 
+	 *
 	 * @param name
 	 *            database column name
 	 * @param typeName
@@ -165,10 +166,9 @@
 	DbAttribute buildAttribute(String name, String typeName, int type, int size, int scale, boolean allowNulls);
 
 	/**
-	 * Binds an object value to PreparedStatement's numbered parameter.
+	 * Binds an object value to PreparedStatement's parameter.
 	 */
-	void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
-			throws SQLException, Exception;
+	void bindParameter(PreparedStatement statement, ParameterBinding parameterBinding) throws SQLException, Exception;
 
 	/**
 	 * Returns the name of the table type (as returned by
@@ -190,7 +190,7 @@
 	/**
 	 * Append the column type part of a "create table" to the given
 	 * {@link StringBuffer}
-	 * 
+	 *
 	 * @param sqlBuffer
 	 *            the {@link StringBuffer} to append the column type to
 	 * @param column
@@ -208,7 +208,7 @@
 
 	/**
 	 * Returns SQL identifier quoting strategy object
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	QuotingStrategy getQuotingStrategy();
@@ -216,14 +216,14 @@
 	/**
 	 * Allows the users to get access to the adapter decorated by a given
 	 * adapter.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	DbAdapter unwrap();
 
 	/**
 	 * Returns a translator factory for EJBQL to SQL translation.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	EJBQLTranslatorFactory getEjbqlTranslatorFactory();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
index 243d3f7..57d3430 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
@@ -21,6 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
@@ -35,11 +36,7 @@
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.*;
 import org.apache.cayenne.merge.MergerFactory;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
@@ -79,7 +76,7 @@
 	/**
 	 * @since 3.1
 	 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the
-	 *             DataNode.
+	 * DataNode.
 	 */
 	@Inject
 	protected BatchTranslatorFactory batchQueryBuilderFactory;
@@ -91,10 +88,10 @@
 	 * Creates new JdbcAdapter with a set of default parameters.
 	 */
 	public JdbcAdapter(@Inject RuntimeProperties runtimeProperties,
-			@Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
-			@Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes,
-			@Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories,
-			@Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator) {
+	                   @Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
+	                   @Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes,
+	                   @Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories,
+	                   @Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator) {
 
 		// init defaults
 		this.setSupportsBatchUpdates(false);
@@ -113,7 +110,7 @@
 
 	/**
 	 * Returns default separator - a semicolon.
-	 * 
+	 *
 	 * @since 1.0.4
 	 */
 	@Override
@@ -137,7 +134,7 @@
 	 * well. Resource lookup is recursive, so that if DbAdapter is a subclass of
 	 * another adapter, parent adapter package is searched as a failover.
 	 * </p>
-	 * 
+	 *
 	 * @since 3.0
 	 */
 	protected URL findResource(String name) {
@@ -172,7 +169,7 @@
 	 * @since 3.1
 	 */
 	protected void initExtendedTypes(List<ExtendedType> defaultExtendedTypes, List<ExtendedType> userExtendedTypes,
-			List<ExtendedTypeFactory> extendedTypeFactories) {
+	                                 List<ExtendedTypeFactory> extendedTypeFactories) {
 		for (ExtendedType type : defaultExtendedTypes) {
 			extendedTypes.registerType(type);
 		}
@@ -201,7 +198,7 @@
 	 * Creates and returns an {@link EJBQLTranslatorFactory} used to generate
 	 * visitors for EJBQL to SQL translations. This method should be overriden
 	 * by subclasses that need to customize EJBQL generation.
-	 * 
+	 *
 	 * @since 3.0
 	 */
 	protected EJBQLTranslatorFactory createEJBQLTranslatorFactory() {
@@ -220,7 +217,7 @@
 
 	/**
 	 * Sets new primary key generator.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	public void setPkGenerator(PkGenerator pkGenerator) {
@@ -229,7 +226,7 @@
 
 	/**
 	 * Returns true.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	@Override
@@ -257,7 +254,7 @@
 	/**
 	 * Returns true if supplied type can have a length attribute as a part of
 	 * column definition
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	public boolean typeSupportsLength(int type) {
@@ -267,11 +264,11 @@
 	/**
 	 * Returns true if supplied type can have a length attribute as a part of
 	 * column definition
-	 * 
+	 * <p/>
 	 * TODO: this is a static method only to support the deprecated method
 	 * {@link TypesMapping#supportsLength(int)} When the deprecated method is
 	 * removed this body should be moved in to {@link #typeSupportsLength(int)}
-	 * 
+	 *
 	 * @deprecated
 	 */
 	static boolean supportsLength(int type) {
@@ -360,7 +357,7 @@
 
 	/**
 	 * Appends SQL for column creation to CREATE TABLE buffer.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	@Override
@@ -405,7 +402,7 @@
 
 	/**
 	 * Returns a DDL string to create a unique constraint over a set of columns.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	@Override
@@ -525,7 +522,7 @@
 
 	/**
 	 * Uses JdbcActionBuilder to create the right action.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	@Override
@@ -539,14 +536,18 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
+	public void bindParameter(PreparedStatement statement, ParameterBinding binding)
 			throws SQLException, Exception {
 
-		if (object == null) {
-			statement.setNull(pos, sqlType);
+		if (binding.getValue() == null) {
+			statement.setNull(binding.getStatementPosition(), binding.getType());
 		} else {
-			ExtendedType typeProcessor = getExtendedTypes().getRegisteredType(object.getClass());
-			typeProcessor.setJdbcObject(statement, object, pos, sqlType, scale);
+			ExtendedType typeProcessor = getExtendedTypes().getRegisteredType(binding.getValue().getClass());
+			typeProcessor.setJdbcObject(statement
+					, binding.getValue()
+					, binding.getStatementPosition()
+					, binding.getType()
+					, binding.getScale());
 		}
 	}
 
@@ -579,7 +580,7 @@
 	 * normally initialized in constructor by calling
 	 * {@link #createEJBQLTranslatorFactory()}, and can be changed later by
 	 * calling {@link #setEjbqlTranslatorFactory(EJBQLTranslatorFactory)}.
-	 * 
+	 *
 	 * @since 3.0
 	 */
 	public EJBQLTranslatorFactory getEjbqlTranslatorFactory() {
@@ -591,7 +592,7 @@
 	 * normally initialized in constructor by calling
 	 * {@link #createEJBQLTranslatorFactory()}, so users would only override it
 	 * if they need to customize EJBQL translation.
-	 * 
+	 *
 	 * @since 3.0
 	 */
 	public void setEjbqlTranslatorFactory(EJBQLTranslatorFactory ejbqlTranslatorFactory) {
@@ -606,8 +607,8 @@
 	}
 
 	/**
-	 * @since 4.0
 	 * @return
+	 * @since 4.0
 	 */
 	protected QuotingStrategy createQuotingStrategy() {
 		return new DefaultQuotingStrategy("\"", "\"");
@@ -632,7 +633,7 @@
 	/**
 	 * @since 3.1
 	 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the
-	 *             DataNode.
+	 * DataNode.
 	 */
 	@Deprecated
 	public BatchTranslatorFactory getBatchQueryBuilderFactory() {
@@ -642,7 +643,7 @@
 	/**
 	 * @since 3.1
 	 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the
-	 *             DataNode.
+	 * DataNode.
 	 */
 	@Deprecated
 	public void setBatchQueryBuilderFactory(BatchTranslatorFactory batchQueryBuilderFactory) {
@@ -651,7 +652,7 @@
 
 	/**
 	 * Simply returns this, as JdbcAdapter is not a wrapper.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
index 5064fd1..f71f655 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
@@ -21,14 +21,10 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.types.BooleanType;
-import org.apache.cayenne.access.types.ByteArrayType;
-import org.apache.cayenne.access.types.CharType;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeFactory;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.*;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.JdbcAdapter;
@@ -233,17 +229,13 @@
 
     @Override
     public void bindParameter(
-            PreparedStatement statement,
-            Object object,
-            int pos,
-            int sqlType,
-            int precision) throws SQLException, Exception {
+            PreparedStatement statement, ParameterBinding binding) throws SQLException, Exception {
 
-        if (object == null && (sqlType == 0 || sqlType == Types.BOOLEAN)) {
-            statement.setNull(pos, Types.VARCHAR);
+        if (binding.getValue() == null && (binding.getType() == 0 || binding.getType() == Types.BOOLEAN)) {
+            statement.setNull(binding.getStatementPosition(), Types.VARCHAR);
         }
         else {
-            super.bindParameter(statement, object, pos, sqlType, precision);
+            super.bindParameter(statement, binding);
         }
     }
     
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
index dca2dc6..8a3b462 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
@@ -20,16 +20,12 @@
 package org.apache.cayenne.dba.derby;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.types.ByteType;
-import org.apache.cayenne.access.types.CharType;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeFactory;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
-import org.apache.cayenne.access.types.ShortType;
+import org.apache.cayenne.access.types.*;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.JdbcAdapter;
@@ -191,15 +187,13 @@
     @Override
     public void bindParameter(
             PreparedStatement statement,
-            Object object,
-            int pos,
-            int sqlType,
-            int precision) throws SQLException, Exception {
+            ParameterBinding binding) throws SQLException, Exception {
 
-        if (object == null && sqlType == 0) {
-            statement.setNull(pos, Types.VARCHAR);
+        if (binding.getValue() == null && binding.getType() == 0) {
+            statement.setNull(binding.getStatementPosition(), Types.VARCHAR);
         } else {
-            super.bindParameter(statement, object, pos, convertNTypes(sqlType), precision);
+            binding.setType(convertNTypes(binding.getType()));
+            super.bindParameter(statement, binding);
         }
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
index 886cf20..d9aee83 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
@@ -1,26 +1,27 @@
 /*****************************************************************
- *   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.
+ * 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.dba.ingres;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -51,7 +52,7 @@
  * DbAdapter implementation for <a
  * href="http://opensource.ca.com/projects/ingres/">Ingres</a>. Sample
  * connection settings to use with Ingres are shown below:
- * 
+ *
  * <pre>
  *  ingres.jdbc.username = test
  *  ingres.jdbc.password = secret
@@ -64,10 +65,10 @@
 	public static final String TRIM_FUNCTION = "TRIM";
 
 	public IngresAdapter(@Inject RuntimeProperties runtimeProperties,
-			@Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
-			@Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes,
-			@Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories,
-			@Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator) {
+	                     @Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
+	                     @Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes,
+	                     @Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories,
+	                     @Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator) {
 		super(runtimeProperties, defaultExtendedTypes, userExtendedTypes, extendedTypeFactories, resourceLocator);
 		setSupportsUniqueConstraints(true);
 		setSupportsGeneratedKeys(true);
@@ -109,13 +110,13 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
+	public void bindParameter(PreparedStatement statement, ParameterBinding binding)
 			throws SQLException, Exception {
 
-		if (object == null && (sqlType == Types.BOOLEAN || sqlType == Types.BIT)) {
-			statement.setNull(pos, Types.VARCHAR);
+		if (binding.getValue() == null && (binding.getType() == Types.BOOLEAN || binding.getType() == Types.BIT)) {
+			statement.setNull(binding.getStatementPosition(), Types.VARCHAR);
 		} else {
-			super.bindParameter(statement, object, pos, sqlType, scale);
+			super.bindParameter(statement, binding);
 		}
 	}
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
index 188f859..961f653 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
@@ -19,36 +19,18 @@
 
 package org.apache.cayenne.dba.mysql;
 
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
-import org.apache.cayenne.access.types.ByteArrayType;
-import org.apache.cayenne.access.types.CharType;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeFactory;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.*;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
-import org.apache.cayenne.dba.DefaultQuotingStrategy;
-import org.apache.cayenne.dba.JdbcAdapter;
-import org.apache.cayenne.dba.PkGenerator;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.dba.*;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
@@ -60,6 +42,11 @@
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.*;
+
 /**
  * DbAdapter implementation for <a href="http://www.mysql.com">MySQL RDBMS</a>.
  * <h3>
@@ -215,9 +202,10 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
+	public void bindParameter(PreparedStatement statement, ParameterBinding binding)
 			throws SQLException, Exception {
-		super.bindParameter(statement, object, pos, mapNTypes(sqlType), scale);
+		binding.setType(mapNTypes(binding.getType()));
+		super.bindParameter(statement, binding);
 	}
 
 	private int mapNTypes(int sqlType) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
index ac189ab..eb0cc90 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
@@ -22,6 +22,7 @@
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.Objects;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
@@ -78,7 +79,7 @@
 	}
 
 	private void processResultSet(CallableStatement statement, OperationObserver observer) throws Exception {
-		ResultSet rs = statement.getResultSet();
+		ResultSet rs = Objects.requireNonNull(statement.getResultSet());
 
 		try {
 			RowDescriptor descriptor = describeResultSet(rs, processedResultSets++);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
index 8c6a7fb..8cfdc2c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
@@ -19,33 +19,23 @@
 

 package org.apache.cayenne.dba.oracle;

 

-import java.io.OutputStream;

-import java.io.Writer;

-import java.lang.reflect.Method;

-import java.sql.Blob;

-import java.sql.Clob;

-import java.sql.Connection;

-import java.sql.PreparedStatement;

-import java.sql.ResultSet;

-import java.sql.SQLException;

-import java.sql.Types;

-import java.util.Collections;

-import java.util.List;

-

 import org.apache.cayenne.CayenneException;

 import org.apache.cayenne.CayenneRuntimeException;

 import org.apache.cayenne.access.OperationObserver;

-import org.apache.cayenne.access.translator.ParameterBinding;

+import org.apache.cayenne.access.translator.DbAttributeBinding;

 import org.apache.cayenne.dba.DbAdapter;

 import org.apache.cayenne.log.JdbcEventLogger;

 import org.apache.cayenne.map.DbAttribute;

-import org.apache.cayenne.query.BatchQuery;

-import org.apache.cayenne.query.BatchQueryRow;

-import org.apache.cayenne.query.InsertBatchQuery;

-import org.apache.cayenne.query.SQLAction;

-import org.apache.cayenne.query.UpdateBatchQuery;

+import org.apache.cayenne.query.*;

 import org.apache.cayenne.util.Util;

 

+import java.io.OutputStream;

+import java.io.Writer;

+import java.lang.reflect.Method;

+import java.sql.*;

+import java.util.Collections;

+import java.util.List;

+

 /**

  * @since 3.0

  */

@@ -55,12 +45,13 @@
 	private DbAdapter adapter;

 	private JdbcEventLogger logger;

 

-	private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)

+	private static void bind(DbAdapter adapter, PreparedStatement statement, DbAttributeBinding[] bindings)

 			throws SQLException, Exception {

 

-		for (ParameterBinding b : bindings) {

-			adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b

-					.getAttribute().getScale());

+		for (DbAttributeBinding b : bindings) {

+			DbAttributeBinding binding = new DbAttributeBinding(b.getAttribute(), adapter.getExtendedTypes()

+					.getRegisteredType(b.getValue().getClass()));

+			adapter.bindParameter(statement, binding);

 		}

 	}

 

@@ -106,7 +97,7 @@
 

 			try (PreparedStatement statement = connection.prepareStatement(updateStr);) {

 

-				ParameterBinding[] bindings = translator.updateBindings(row);

+				DbAttributeBinding[] bindings = translator.updateBindings(row);

 				logger.logQueryParameters("bind", bindings);

 

 				bind(adapter, statement, bindings);

@@ -150,7 +141,11 @@
 				Object value = qualifierValues.get(i);

 				DbAttribute attribute = qualifierAttributes.get(i);

 

-				adapter.bindParameter(selectStatement, value, i + 1, attribute.getType(), attribute.getScale());

+				DbAttributeBinding binding = new DbAttributeBinding(attribute, adapter.getExtendedTypes()

+						.getRegisteredType(value.getClass()));

+				binding.setStatementPosition(i + 1);

+				binding.setValue(value);

+				adapter.bindParameter(selectStatement,binding);

 			}

 

 			try (ResultSet result = selectStatement.executeQuery();) {

diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
index fb2deb7..d25d32f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.dba.oracle;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.translator.batch.DefaultBatchTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
@@ -111,31 +111,31 @@
     }
     
     @Override
-    protected ParameterBinding[] createBindings() {
+    protected DbAttributeBinding[] createBindings() {
         List<DbAttribute> dbAttributes = query.getDbAttributes();
         int len = dbAttributes.size();
 
-        ParameterBinding[] bindings = new ParameterBinding[len];
+        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute attribute = dbAttributes.get(i);
 
             String typeName = TypesMapping.getJavaBySqlType(attribute.getType());
             ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
-            bindings[i] = new ParameterBinding(attribute, extendedType);
+            bindings[i] = new DbAttributeBinding(attribute, extendedType);
         }
 
         return bindings;
     }
     
     @Override
-    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
 
         int len = bindings.length;
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            ParameterBinding b = bindings[i];
+            DbAttributeBinding b = bindings[i];
 
             Object value = row.getValue(i);
             DbAttribute attribute = b.getAttribute();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
index b0c6afa..adfd6dd 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
@@ -21,15 +21,12 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
-import org.apache.cayenne.access.types.ByteType;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeFactory;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
-import org.apache.cayenne.access.types.ShortType;
+import org.apache.cayenne.access.types.*;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.JdbcAdapter;
@@ -39,20 +36,11 @@
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.merge.MergerFactory;
-import org.apache.cayenne.query.BatchQuery;
-import org.apache.cayenne.query.InsertBatchQuery;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.query.UpdateBatchQuery;
+import org.apache.cayenne.query.*;
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.lang.reflect.Field;
-import java.sql.CallableStatement;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Types;
+import java.sql.*;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -230,17 +218,18 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
+	public void bindParameter(PreparedStatement statement, ParameterBinding binding)
 			throws SQLException, Exception {
 
 		// Oracle doesn't support BOOLEAN even when binding NULL, so have to
 		// intercept
 		// NULL Boolean here, as super doesn't pass it through ExtendedType...
-		if (object == null && sqlType == Types.BOOLEAN) {
+		if (binding.getValue() == null && binding.getType() == Types.BOOLEAN) {
 			ExtendedType typeProcessor = getExtendedTypes().getRegisteredType(Boolean.class);
-			typeProcessor.setJdbcObject(statement, object, pos, sqlType, scale);
+			typeProcessor.setJdbcObject(statement, binding.getValue(), binding.getStatementPosition(), binding
+							.getType(),binding.getScale());
 		} else {
-			super.bindParameter(statement, object, pos, sqlType, scale);
+			super.bindParameter(statement, binding);
 		}
 	}
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
index ee45df1..e4c7eff 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
@@ -21,6 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -130,9 +131,10 @@
 	}
 
 	@Override
-	public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale)
+	public void bindParameter(PreparedStatement statement, ParameterBinding binding)
 			throws SQLException, Exception {
-		super.bindParameter(statement, object, pos, mapNTypes(sqlType), scale);
+		binding.setType(mapNTypes(binding.getType()));
+		super.bindParameter(statement, binding);
 	}
 
 	private int mapNTypes(int sqlType) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
index f82fc3f..839b548 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
@@ -33,8 +33,6 @@
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
 import java.sql.Types;
 import java.util.Calendar;
 import java.util.Collection;
@@ -101,11 +99,6 @@
         return query.createSQLAction(new SQLiteActionBuilder(node));
     }
 
-    @Override
-    public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int scale) throws SQLException, Exception {
-        super.bindParameter(statement, object, pos, mapNTypes(sqlType), scale);
-    }
-
     private int mapNTypes(int sqlType) {
         switch (sqlType) {
             case Types.NCHAR : return Types.CHAR;
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
index 45a6f65..2b0ca86 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
@@ -19,14 +19,9 @@
 
 package org.apache.cayenne.dba.sybase;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
-import org.apache.cayenne.access.types.ByteArrayType;
-import org.apache.cayenne.access.types.ByteType;
-import org.apache.cayenne.access.types.CharType;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeFactory;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
-import org.apache.cayenne.access.types.ShortType;
+import org.apache.cayenne.access.types.*;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.DefaultQuotingStrategy;
@@ -109,22 +104,22 @@
     }
 
     @Override
-    public void bindParameter(PreparedStatement statement, Object object, int pos, int sqlType, int precision)
+    public void bindParameter(PreparedStatement statement, ParameterBinding binding)
             throws SQLException, Exception {
 
         // Sybase driver doesn't like CLOBs and BLOBs as parameters
-        if (object == null) {
-            if (sqlType == Types.CLOB) {
-                sqlType = Types.VARCHAR;
-            } else if (sqlType == Types.BLOB) {
-                sqlType = Types.VARBINARY;
+        if (binding.getValue() == null) {
+            if (binding.getType() == Types.CLOB) {
+                binding.setType(Types.VARCHAR);
+            } else if (binding.getType() == Types.BLOB) {
+                binding.setType(Types.VARBINARY);
             }
         }
 
-        if (object == null && sqlType == 0) {
-            statement.setNull(pos, Types.VARCHAR);
+        if (binding.getValue() == null && binding.getType() == 0) {
+            statement.setNull(binding.getStatementPosition(), Types.VARCHAR);
         } else {
-            super.bindParameter(statement, object, pos, sqlType, precision);
+            super.bindParameter(statement, binding);
         }
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
index 6908fcc..f9351bf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
@@ -26,7 +26,7 @@
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ExtendedEnumeration;
 import org.apache.cayenne.access.jdbc.SQLParameterBinding;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.conn.DataSourceInfo;
@@ -227,7 +227,7 @@
 
 	/**
 	 * @deprecated since 4.0 use
-	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 *             {@link #logQuery(String, DbAttributeBinding[], long)}.
 	 */
 	@Deprecated
 	@Override
@@ -295,7 +295,7 @@
 	}
 
 	@Override
-	public void logQuery(String sql, ParameterBinding[] bindings, long translatedIn) {
+	public void logQuery(String sql, DbAttributeBinding[] bindings, long translatedIn) {
 		if (isLoggable()) {
 
 			StringBuilder buffer = new StringBuilder(sql).append(" ");
@@ -335,7 +335,7 @@
 	}
 
 	@Override
-	public void logQueryParameters(String label, ParameterBinding[] bindings) {
+	public void logQueryParameters(String label, DbAttributeBinding[] bindings) {
 
 		if (isLoggable() && bindings.length > 0) {
 
@@ -348,7 +348,7 @@
 		}
 	}
 
-	private void appendParameters(StringBuilder buffer, String label, ParameterBinding[] bindings) {
+	private void appendParameters(StringBuilder buffer, String label, DbAttributeBinding[] bindings) {
 
 		int len = bindings.length;
 		if (len > 0) {
@@ -356,7 +356,7 @@
 			boolean hasIncluded = false;
 
 			for (int i = 0, j = 1; i < len; i++) {
-				ParameterBinding b = bindings[i];
+				DbAttributeBinding b = bindings[i];
 
 				if (b.isExcluded()) {
 					continue;
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
index 49ad655..5ada199 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
@@ -20,7 +20,7 @@
 
 import java.util.List;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.conn.DataSourceInfo;
 import org.apache.cayenne.map.DbAttribute;
 
@@ -77,14 +77,14 @@
 
 	/**
 	 * @deprecated since 4.0 use
-	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 *             {@link #logQuery(String, DbAttributeBinding[], long)}.
 	 */
 	@Deprecated
 	void logQuery(String sql, List<?> params);
 
 	/**
 	 * @deprecated since 4.0 use
-	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 *             {@link #logQuery(String, DbAttributeBinding[], long)}.
 	 */
 	@Deprecated
 	void logQuery(String sql, List<DbAttribute> attrs, List<?> params, long time);
@@ -92,16 +92,16 @@
 	/**
 	 * @since 4.0
 	 */
-	void logQuery(String sql, ParameterBinding[] bindings, long translatedIn);
+	void logQuery(String sql, DbAttributeBinding[] bindings, long translatedIn);
 
 	/**
 	 * @since 4.0
 	 */
-	void logQueryParameters(String label, ParameterBinding[] bindings);
+	void logQueryParameters(String label, DbAttributeBinding[] bindings);
 
 	/**
 	 * @deprecated since 4.0 in favor of
-	 *             {@link #logQueryParameters(String, ParameterBinding[])}
+	 *             {@link #logQueryParameters(String, DbAttributeBinding[])}
 	 */
 	@Deprecated
 	void logQueryParameters(String label, List<DbAttribute> attrs, List<Object> parameters, boolean isInserting);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
index 8a56d94..1ed2252 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
@@ -20,7 +20,7 @@
 
 import java.util.List;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.conn.DataSourceInfo;
 import org.apache.cayenne.map.DbAttribute;
 
@@ -82,7 +82,7 @@
 	}
 	
 	@Override
-	public void logQuery(String sql, ParameterBinding[] bindings, long translatedIn) {		
+	public void logQuery(String sql, DbAttributeBinding[] bindings, long translatedIn) {
 	}
 
 	@Override
@@ -91,7 +91,7 @@
 	}
 
 	@Override
-	public void logQueryParameters(String label, ParameterBinding[] bindings) {
+	public void logQueryParameters(String label, DbAttributeBinding[] bindings) {
 	}
 
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/DataMap.java b/cayenne-server/src/main/java/org/apache/cayenne/map/DataMap.java
index c3ac20b..500ee03 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/DataMap.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/DataMap.java
@@ -157,7 +157,7 @@
 	/**
 	 * @since 3.1
 	 */
-	protected Resource configurationSource;
+	protected transient Resource configurationSource;
 
 	/**
 	 * @since 3.1
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/Procedure.java b/cayenne-server/src/main/java/org/apache/cayenne/map/Procedure.java
index 356e1c2..d411f58 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/Procedure.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/Procedure.java
@@ -121,10 +121,12 @@
     }
 
     /**
-     * Returns procedure name including schema, if present.
+     * Returns procedure name including schema and catalog, if present.
      */
     public String getFullyQualifiedName() {
-        return (schema != null) ? schema + '.' + getName() : getName();
+        return (catalog != null ? catalog + '.' : "")
+                + (schema != null ? schema + '.' : "")
+                + name;
     }
 
     /**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
index c439ceb..5eed080 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
@@ -33,7 +33,9 @@
  */
 public class SelectQueryDescriptor extends QueryDescriptor {
 
-    protected Expression qualifier;
+	private static final long serialVersionUID = -8798258795351950215L;
+
+	protected Expression qualifier;
 
     protected List<Ordering> orderings = new ArrayList<>();
     protected List<String> prefetches = new ArrayList<>();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
index ccd7a58..94f495d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
@@ -22,6 +22,7 @@
 import java.io.PrintWriter;
 import java.io.Serializable;
 import java.io.StringWriter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -62,6 +63,22 @@
 		Collections.sort(objects, ComparatorUtils.chainedComparator(orderings));
 	}
 
+	/**
+	 * Orders a given list of objects, using a List of Orderings applied
+	 * according the default iteration order of the Orderings list. I.e. each
+	 * Ordering with lower index is more significant than any other Ordering
+	 * with higher index. List being ordered is modified in place.
+	 * 
+	 * @since 4.0
+	 */
+	public static <E> List<E> orderedList(List<E> objects, List<? extends Ordering> orderings) {
+		List<E> newList = new ArrayList<E>(objects);
+		
+		orderList(newList, orderings);
+		
+		return newList;
+	}
+	
 	public Ordering() {
 	}
 
@@ -313,9 +330,21 @@
 	}
 
 	/**
+	 * @since 4.0
+	 */
+	public <E> List<E> orderedList(List<E> objects) {
+		List<E> newList = new ArrayList<E>(objects);
+		
+		orderList(newList);
+		
+		return newList;
+	}
+
+	/**
 	 * Comparable interface implementation. Can compare two Java Beans based on
 	 * the stored expression.
 	 */
+	@Override
 	public int compare(Object o1, Object o2) {
 		Expression exp = getSortSpec();
 		Object value1 = null;
@@ -367,6 +396,7 @@
 	 * 
 	 * @since 1.1
 	 */
+	@Override
 	public void encodeAsXML(XMLEncoder encoder) {
 		encoder.print("<ordering");
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
index e44b08d..d71a7f4 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
@@ -19,6 +19,11 @@
 
 package org.apache.cayenne.query;
 
+import org.apache.cayenne.map.Entity;
+import org.apache.cayenne.util.Util;
+import org.apache.cayenne.util.XMLEncoder;
+import org.apache.cayenne.util.XMLSerializable;
+
 import java.io.ObjectStreamException;
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -26,11 +31,6 @@
 import java.util.Collections;
 import java.util.StringTokenizer;
 
-import org.apache.cayenne.map.Entity;
-import org.apache.cayenne.util.Util;
-import org.apache.cayenne.util.XMLEncoder;
-import org.apache.cayenne.util.XMLSerializable;
-
 /**
  * Defines a node in a prefetch tree.
  *
@@ -472,7 +472,7 @@
 	// implementing 'readResolve' instead of 'readObject' so that this would
 	// work with
 	// hessian
-	private Object readResolve() throws ObjectStreamException {
+	protected Object readResolve() throws ObjectStreamException {
 
 		if (hasChildren()) {
 			for (PrefetchTreeNode child : children) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
index f9e8209..6db4fa9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
@@ -134,22 +134,14 @@
 		return context.select(this);
 	}
 
-	/**
-	 * Since we are selecting by ID, multiple matched objects likely indicate a
-	 * database referential integrity problem.
-	 */
 	@Override
 	public T selectOne(ObjectContext context) {
 		return context.selectOne(this);
 	}
 
-	/**
-	 * Since we are selecting by ID, we don't need to limit fetch size. Multiple
-	 * matched objects likely indicate a database referential integrity problem.
-	 */
 	@Override
 	public T selectFirst(ObjectContext context) {
-		return selectFirst(context);
+		return context.selectFirst(this);
 	}
 
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
index b5efb89..f357846 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
@@ -1,20 +1,20 @@
 /*****************************************************************
- *   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.
+ * 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;
@@ -24,7 +24,7 @@
 
 /**
  * Interface of a Cayenne remote service.
- * 
+ *
  * @since 1.2
  * @see org.apache.cayenne.rop.ROPServlet
  */
@@ -45,4 +45,10 @@
      * 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;
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
index 2a63fbb..3716227 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
@@ -19,10 +19,6 @@
 
 package org.apache.cayenne.remote.service;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.access.ClientServerChannel;
@@ -36,6 +32,11 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+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.
@@ -97,6 +98,7 @@
 	 */
 	protected abstract ServerSession getServerSession();
 
+	@Override
 	public RemoteSession establishSession() {
 		logger.debug("Session requested by client");
 
@@ -106,6 +108,7 @@
 		return session;
 	}
 
+	@Override
 	public RemoteSession establishSharedSession(String name) {
 		logger.debug("Shared session requested by client. Group name: " + name);
 
@@ -116,6 +119,7 @@
 		return createServerSession(name).getSession();
 	}
 
+	@Override
 	public Object processMessage(ClientMessage message) throws Throwable {
 
 		if (message == null) {
@@ -150,6 +154,10 @@
 		}
 	}
 
+	@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);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPRequestContext.java b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPRequestContext.java
new file mode 100644
index 0000000..126b5d2
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPRequestContext.java
@@ -0,0 +1,107 @@
+/*****************************************************************
+ * 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.rop;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+public class ROPRequestContext {
+
+    private static final ThreadLocal<ROPRequestContext> localContext = new ThreadLocal<>();
+
+    private String serviceId;
+    private String objectId;
+    private ServletRequest request;
+    private ServletResponse response;
+    private int count;
+
+    private ROPRequestContext() {
+    }
+
+    public static void start(String serviceId, String objectId, ServletRequest request, ServletResponse response) {
+        ROPRequestContext context = localContext.get();
+
+        if (context == null) {
+            context = new ROPRequestContext();
+            localContext.set(context);
+        }
+
+        context.serviceId = serviceId;
+        context.objectId = objectId;
+        context.request = request;
+        context.response = response;
+        context.count++;
+    }
+
+    public static ROPRequestContext getROPRequestContext() {
+        return localContext.get();
+    }
+
+    public static String getContextServiceId() {
+        ROPRequestContext context = localContext.get();
+
+        if (context != null) {
+            return context.serviceId;
+        } else {
+            return null;
+        }
+    }
+
+    public static String getContextObjectId() {
+        ROPRequestContext context = localContext.get();
+
+        if (context != null) {
+            return context.objectId;
+        } else {
+            return null;
+        }
+    }
+
+    public static ServletRequest getContextRequest() {
+        ROPRequestContext context = localContext.get();
+
+        if (context != null) {
+            return context.request;
+        } else {
+            return null;
+        }
+    }
+
+    public static ServletResponse getContextResponse() {
+        ROPRequestContext context = localContext.get();
+
+        if (context != null) {
+            return context.response;
+        } else {
+            return null;
+        }
+    }
+
+    public static void end() {
+        ROPRequestContext context = localContext.get();
+
+        if (context != null && --context.count == 0) {
+            context.request = null;
+            context.response = null;
+
+            localContext.set(null);
+        }
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPSerializationService.java b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPSerializationService.java
index 486b0e9..99cccbf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPSerializationService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPSerializationService.java
@@ -22,6 +22,11 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 
+/**
+ * ROP serialization service
+ *
+ * @since 4.0
+ */
 public interface ROPSerializationService {
 
     byte[] serialize(Object object) throws IOException;
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPServlet.java b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPServlet.java
index 8c12baf..db1bb0a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPServlet.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/rop/ROPServlet.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.rop;
 
-import com.caucho.services.server.ServiceContext;
 import org.apache.cayenne.configuration.CayenneRuntime;
 import org.apache.cayenne.configuration.rop.server.ROPServerModule;
 import org.apache.cayenne.configuration.server.ServerRuntime;
@@ -98,8 +97,7 @@
                 objectId = req.getParameter("ejbid");
             }
 
-            // TODO: need to untangle HttpRemoteService from dependence on Hessian's ServiceContext thread local setup
-            ServiceContext.begin(req, resp, serviceId, objectId);
+            ROPRequestContext.start(serviceId, objectId, req, resp);
 
             String operation = req.getParameter(ROPConstants.OPERATION_PARAMETER);
 
@@ -128,6 +126,8 @@
             throw e;
         } catch (Throwable e) {
             throw new ServletException(e);
+        } finally {
+            ROPRequestContext.end();
         }
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/rop/ServerHttpRemoteService.java b/cayenne-server/src/main/java/org/apache/cayenne/rop/ServerHttpRemoteService.java
index 22aba9f..9099b04 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/rop/ServerHttpRemoteService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/rop/ServerHttpRemoteService.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.rop;
 
-import com.caucho.services.server.ServiceContext;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.ObjectContextFactory;
 import org.apache.cayenne.di.Inject;
@@ -37,7 +36,7 @@
 
 	@Override
 	protected HttpSession getSession(boolean create) {
-		HttpServletRequest request = (HttpServletRequest) ServiceContext.getContextRequest();
+		HttpServletRequest request = (HttpServletRequest) ROPRequestContext.getContextRequest();
 		if (request == null) {
 			throw new IllegalStateException(
 					"Attempt to access HttpSession outside the request scope.");
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
index 82a0ae6..41f4a54 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
@@ -33,6 +33,7 @@
 import org.apache.cayenne.QueryResponse;
 import org.apache.cayenne.cache.QueryCache;
 import org.apache.cayenne.cache.QueryCacheEntryFactory;
+import org.apache.cayenne.map.EntityInheritanceTree;
 import org.apache.cayenne.query.ObjectIdQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.QueryCacheStrategy;
@@ -167,7 +168,7 @@
             ObjectIdQuery oidQuery = (ObjectIdQuery) query;
 
             if (!oidQuery.isFetchMandatory() && !oidQuery.isFetchingDataRows()) {
-                Object object = actingContext.getGraphManager().getNode(
+                Object object = polymorphicObjectFromCache(
                         oidQuery.getObjectId());
                 if (object != null) {
 
@@ -184,6 +185,39 @@
 
         return !DONE;
     }
+    
+    // TODO: bunch of copy/paset from DataDomainQueryAction
+    protected Object polymorphicObjectFromCache(ObjectId superOid) {
+		Object object = actingContext.getGraphManager().getNode(superOid);
+		if (object != null) {
+			return object;
+		}
+
+		EntityInheritanceTree inheritanceTree = actingContext.getEntityResolver().getInheritanceTree(superOid.getEntityName());
+		if (!inheritanceTree.getChildren().isEmpty()) {
+			object = polymorphicObjectFromCache(inheritanceTree, superOid.getIdSnapshot());
+		}
+
+		return object;
+	}
+    
+	private Object polymorphicObjectFromCache(EntityInheritanceTree superNode, Map<String, ?> idSnapshot) {
+
+		for (EntityInheritanceTree child : superNode.getChildren()) {
+			ObjectId id = new ObjectId(child.getEntity().getName(), idSnapshot);
+			Object object = actingContext.getGraphManager().getNode(id);
+			if (object != null) {
+				return object;
+			}
+			
+			object = polymorphicObjectFromCache(child, idSnapshot);
+			if (object != null) {
+				return object;
+			}
+		}
+
+		return null;
+	}
 
     protected boolean interceptRelationshipQuery() {
 
diff --git a/cayenne-server/src/main/resources/org/apache/cayenne/schema/8/modelMap.xsd b/cayenne-server/src/main/resources/org/apache/cayenne/schema/8/modelMap.xsd
index 37f0f91..3d77151 100644
--- a/cayenne-server/src/main/resources/org/apache/cayenne/schema/8/modelMap.xsd
+++ b/cayenne-server/src/main/resources/org/apache/cayenne/schema/8/modelMap.xsd
@@ -269,6 +269,8 @@
 			</xs:sequence>
 			<xs:attribute name="name" use="required" type="xs:string"/>
 			<xs:attribute name="schema" type="xs:string"/>
+			<xs:attribute name="catalog" type="xs:string"/>
+			<xs:attribute name="returningValue" type="xs:string"/>
 		</xs:complexType>
 	</xs:element>
 	<xs:element name="pre-update">
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java
new file mode 100644
index 0000000..f2c703c
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java
@@ -0,0 +1,86 @@
+package org.apache.cayenne.access;
+
+import static org.junit.Assert.assertTrue;
+
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.apache.cayenne.Cayenne;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectIdQuery;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.inheritance_people.AbstractPerson;
+import org.apache.cayenne.testdo.inheritance_people.Manager;
+import org.apache.cayenne.unit.di.DataChannelInterceptor;
+import org.apache.cayenne.unit.di.UnitTestClosure;
+import org.apache.cayenne.unit.di.server.PeopleProjectCase;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DataContextObjectIdQuery_PolymorphicIT extends PeopleProjectCase {
+
+	@Inject
+	private DataContext context1;
+
+	@Inject
+	private DataContext context2;
+
+	@Inject
+	private DataChannelInterceptor queryInterceptor;
+
+	private TableHelper tPerson;
+
+	@Before
+	public void before() {
+		tPerson = new TableHelper(dbHelper, "PERSON").setColumns("PERSON_ID", "NAME", "PERSON_TYPE")
+				.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.CHAR);
+	}
+
+	@Test
+	public void testPolymorphicSharedCache() throws SQLException {
+
+		tPerson.insert(1, "P1", "EM");
+
+		final ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("AbstractPerson", "PERSON_ID", 1), false,
+				ObjectIdQuery.CACHE);
+
+		AbstractPerson ap1 = (AbstractPerson) Cayenne.objectForQuery(context1, q1);
+		assertTrue(ap1 instanceof Manager);
+
+		queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
+
+			@Override
+			public void execute() {
+				// use different context to ensure we hit shared cache
+				AbstractPerson ap2 = (AbstractPerson) Cayenne.objectForQuery(context2, q1);
+				assertTrue(ap2 instanceof Manager);
+			}
+		});
+	}
+
+	@Test
+	public void testPolymorphicLocalCache() throws SQLException {
+
+		tPerson.insert(1, "P1", "EM");
+
+		final ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("AbstractPerson", "PERSON_ID", 1), false,
+				ObjectIdQuery.CACHE);
+
+		AbstractPerson ap1 = (AbstractPerson) Cayenne.objectForQuery(context1, q1);
+		assertTrue(ap1 instanceof Manager);
+
+		queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
+
+			@Override
+			public void execute() {
+				// use same context to ensure we hit local cache
+				// note that this does not guarantee test correctness. If local
+				// cache polymorphic ID lookup is broken, shared cache will pick
+				// it up
+				AbstractPerson ap2 = (AbstractPerson) Cayenne.objectForQuery(context1, q1);
+				assertTrue(ap2 instanceof Manager);
+			}
+		});
+	}
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
index 4426e78..6fe3937 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
@@ -38,17 +38,14 @@
 import java.util.Calendar;
 import java.util.Date;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeThat;
+import static org.junit.Assert.*;
 import static org.junit.Assume.assumeTrue;
 
 /**
  * Test Types mapping for selected columns
  */
 @UseServerRuntime(CayenneProjects.RETURN_TYPES_PROJECT)
-public class ReturnTypesMappingIT extends ServerCase {
+public class    ReturnTypesMappingIT extends ServerCase {
 
     @Inject
     private DataContext context;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
index 0fdc48b..a1998be 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.di.AdhocObjectFactory;
@@ -55,13 +55,13 @@
             }
 
             @Override
-            protected ParameterBinding[] createBindings() {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] createBindings() {
+                return new DbAttributeBinding[0];
             }
 
             @Override
-            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new DbAttributeBinding[0];
             }
         };
 
@@ -81,13 +81,13 @@
             }
 
             @Override
-            protected ParameterBinding[] createBindings() {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] createBindings() {
+                return new DbAttributeBinding[0];
             }
 
             @Override
-            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new DbAttributeBinding[0];
             }
         };
 
@@ -116,13 +116,13 @@
             }
 
             @Override
-            protected ParameterBinding[] createBindings() {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] createBindings() {
+                return new DbAttributeBinding[0];
             }
 
             @Override
-            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new ParameterBinding[0];
+            protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new DbAttributeBinding[0];
             }
         };
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/types/ExtendedTypeMapEnumsTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/types/ExtendedTypeMapEnumsTest.java
index 37b8afc..21ac50b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/types/ExtendedTypeMapEnumsTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/types/ExtendedTypeMapEnumsTest.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.access.types;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
@@ -28,58 +26,81 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import org.apache.cayenne.access.types.InnerEnumHolder.InnerEnum;
+import org.junit.Before;
+import org.junit.Test;
+
 public class ExtendedTypeMapEnumsTest {
 
-    @Test
-    public void testCreateType1_5() {
-        ExtendedTypeMap map = new ExtendedTypeMap();
+	private ExtendedTypeMap map;
 
-        assertNull(map.createType(Object.class.getName()));
+	@Before
+	public void before() {
+		this.map = new ExtendedTypeMap();
+	}
 
-        ExtendedType type = map.createType(MockEnum.class.getName());
-        assertTrue(type instanceof EnumType);
-        assertEquals(MockEnum.class, ((EnumType) type).enumClass);
+	@Test
+	public void testCreateType_NoFactory() {
+		assertNull(map.createType(Object.class.getName()));
+	}
 
-        ExtendedType type2 = map.createType(MockEnum2.class.getName());
-        assertNotSame(type, type2);
-    }
+	@Test
+	public void testCreateType_Enum() {
 
-    @Test
-    public void testCreateType1_5InnerEnum() {
-        ExtendedTypeMap map = new ExtendedTypeMap();
+		ExtendedType type1 = map.createType(MockEnum.class.getName());
+		assertTrue(type1 instanceof EnumType);
+		assertEquals(MockEnum.class, ((EnumType<?>) type1).enumClass);
 
-        ExtendedType type = map.createType(InnerEnumHolder.InnerEnum.class.getName());
-        assertTrue(type instanceof EnumType);
-        assertEquals(InnerEnumHolder.InnerEnum.class, ((EnumType) type).enumClass);
+		ExtendedType type2 = map.createType(MockEnum2.class.getName());
+		assertNotSame(type1, type2);
+	}
 
-        // use a string name with $
-        ExtendedType type1 = map.createType(InnerEnumHolder.class.getName()
-                + "$InnerEnum");
-        assertNotNull(type1);
-        assertSame(type.getClassName(), type1.getClassName());
+	@Test
+	public void testCreateType_InnerEnum() {
 
-        // use a string name with .
-        ExtendedType type2 = map.createType(InnerEnumHolder.class.getName()
-                + ".InnerEnum");
-        assertNotNull(type2);
-        assertSame(type.getClassName(), type2.getClassName());
-    }
+		ExtendedType type = map.createType(InnerEnumHolder.InnerEnum.class.getName());
+		assertTrue(type instanceof EnumType);
+		assertEquals(InnerEnumHolder.InnerEnum.class, ((EnumType<?>) type).enumClass);
 
-    @Test
-    public void testGetDefaultType1_4() {
-        ExtendedTypeMap map = new ExtendedTypeMap();
-        map.internalTypeFactories.clear();
+		// use a string name with $
+		ExtendedType type1 = map.createType(InnerEnumHolder.class.getName() + "$InnerEnum");
+		assertNotNull(type1);
+		assertEquals(type.getClassName(), type1.getClassName());
 
-        assertNull(map.createType(Object.class.getName()));
-        assertNull(map.createType(MockEnum.class.getName()));
-        assertNull(map.createType(MockEnum2.class.getName()));
-    }
+		// use a string name with .
+		ExtendedType type2 = map.createType(InnerEnumHolder.class.getName() + ".InnerEnum");
+		assertNotNull(type2);
+		assertEquals(type.getClassName(), type2.getClassName());
+	}
 
-    @Test
-    public void testGetType() {
-        ExtendedTypeMap map = new ExtendedTypeMap();
-        ExtendedType type = map.getRegisteredType(MockEnum.class.getName());
-        assertNotNull(type);
-        assertTrue(type instanceof EnumType);
-    }
+	@Test
+	public void testGetRegisteredType() {
+		ExtendedType type = map.getRegisteredType(MockEnum.class);
+		assertNotNull(type);
+		assertTrue(type instanceof EnumType);
+
+		assertSame(type, map.getRegisteredType(MockEnum.class));
+		assertSame(type, map.getRegisteredType(MockEnum.class.getName()));
+	}
+
+	@Test
+	public void testGetRegisteredType_InnerEnum() {
+
+		assertEquals(0, map.extendedTypeFactories.size());
+
+		ExtendedType byType = map.getRegisteredType(InnerEnum.class);
+
+		// this and subsequent tests verify that no memory leak occurs per
+		// CAY-2066
+		assertEquals(1, map.extendedTypeFactories.size());
+
+		assertSame(byType, map.getRegisteredType(InnerEnum.class));
+		assertEquals(1, map.extendedTypeFactories.size());
+
+		assertSame(byType, map.getRegisteredType(InnerEnumHolder.class.getName() + "$InnerEnum"));
+		assertEquals(1, map.extendedTypeFactories.size());
+
+		assertSame(byType, map.getRegisteredType(InnerEnumHolder.class.getName() + ".InnerEnum"));
+		assertEquals(1, map.extendedTypeFactories.size());
+	}
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/datasource/ManagedPoolingDataSourceIT.java b/cayenne-server/src/test/java/org/apache/cayenne/datasource/ManagedPoolingDataSourceIT.java
new file mode 100644
index 0000000..04a4540
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/datasource/ManagedPoolingDataSourceIT.java
@@ -0,0 +1,181 @@
+package org.apache.cayenne.datasource;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.sql.DataSource;
+
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.OngoingStubbing;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ManagedPoolingDataSourceIT {
+
+	private static final Log LOGGER = LogFactory.getLog(ManagedPoolingDataSourceIT.class);
+
+	private int poolSize;
+	private OnOffDataSourceManager dataSourceManager;
+	private UnmanagedPoolingDataSource unmanagedPool;
+	private ManagedPoolingDataSource managedPool;
+
+	@Before
+	public void before() throws SQLException {
+
+		this.poolSize = 4;
+		this.dataSourceManager = new OnOffDataSourceManager();
+
+		PoolingDataSourceParameters parameters = new PoolingDataSourceParameters();
+		parameters.setMaxConnections(poolSize);
+		parameters.setMinConnections(poolSize / 2);
+		parameters.setMaxQueueWaitTime(1000);
+		parameters.setValidationQuery("SELECT 1");
+		this.unmanagedPool = new UnmanagedPoolingDataSource(dataSourceManager.mockDataSource, parameters);
+		this.managedPool = new ManagedPoolingDataSource(unmanagedPool, 10000);
+	}
+
+	@After
+	public void after() {
+		if (managedPool != null) {
+			managedPool.close();
+		}
+	}
+
+	private Collection<PoolTask> createTasks(int size) {
+		Collection<PoolTask> tasks = new ArrayList<>();
+
+		for (int i = 0; i < size; i++) {
+			tasks.add(new PoolTask());
+		}
+		return tasks;
+	}
+
+	@Test
+	public void testGetConnection_OnBackendShutdown() throws SQLException, InterruptedException {
+
+		// note that this assertion can only work reliably when the pool is inactive...
+		assertEquals(poolSize, managedPool.poolSize() + managedPool.canExpandSize());
+
+		Collection<PoolTask> tasks = createTasks(4);
+		ExecutorService executor = Executors.newFixedThreadPool(4);
+
+		for (int j = 0; j < 10; j++) {
+			for (PoolTask task : tasks) {
+				executor.submit(task);
+			}
+		}
+
+		dataSourceManager.off();
+		Thread.sleep(500);
+
+		for (int j = 0; j < 10; j++) {
+			for (PoolTask task : tasks) {
+				executor.submit(task);
+			}
+		}
+
+		Thread.sleep(100);
+
+		dataSourceManager.on();
+
+		for (int j = 0; j < 10; j++) {
+			for (PoolTask task : tasks) {
+				executor.submit(task);
+			}
+		}
+
+		executor.shutdown();
+		executor.awaitTermination(2, TimeUnit.SECONDS);
+
+		// note that this assertion can only work reliably when the pool is inactive...
+		assertEquals(poolSize, managedPool.poolSize() + managedPool.canExpandSize());
+	}
+
+	class PoolTask implements Runnable {
+
+		@Override
+		public void run() {
+
+			try (Connection c = managedPool.getConnection();) {
+				try (Statement s = c.createStatement()) {
+					try {
+						Thread.sleep(40);
+					} catch (InterruptedException e) {
+						// ignore
+					}
+				}
+			} catch (SQLException e) {
+				if (OnOffDataSourceManager.NO_CONNECTIONS_MESSAGE.equals(e.getMessage())) {
+					LOGGER.info("db down...");
+				} else {
+					LOGGER.warn("error getting connection", e);
+				}
+			}
+		}
+	}
+
+	static class OnOffDataSourceManager {
+
+		static final String NO_CONNECTIONS_MESSAGE = "no connections at the moment";
+
+		private DataSource mockDataSource;
+		private OngoingStubbing<Connection> createConnectionMock;
+
+		OnOffDataSourceManager() throws SQLException {
+			this.mockDataSource = mock(DataSource.class);
+			this.createConnectionMock = when(mockDataSource.getConnection());
+			on();
+		}
+
+		void off() throws SQLException {
+			createConnectionMock.thenAnswer(new Answer<Connection>() {
+				@Override
+				public Connection answer(InvocationOnMock invocation) throws Throwable {
+					throw new SQLException(NO_CONNECTIONS_MESSAGE);
+				}
+			});
+		}
+
+		void on() throws SQLException {
+			createConnectionMock.thenAnswer(new Answer<Connection>() {
+				@Override
+				public Connection answer(InvocationOnMock invocation) throws Throwable {
+					Connection c = mock(Connection.class);
+					when(c.createStatement()).thenAnswer(new Answer<Statement>() {
+						@Override
+						public Statement answer(InvocationOnMock invocation) throws Throwable {
+
+							ResultSet mockRs = mock(ResultSet.class);
+							when(mockRs.next()).thenReturn(true, false, false, false);
+
+							Statement mockStatement = mock(Statement.class);
+							when(mockStatement.executeQuery(anyString())).thenReturn(mockRs);
+							return mockStatement;
+						}
+					});
+
+					return c;
+				}
+			});
+		}
+	}
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/datasource/PoolingDataSourceIT.java b/cayenne-server/src/test/java/org/apache/cayenne/datasource/PoolingDataSourceIT.java
index 68bbef2..d7d8bf1 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/datasource/PoolingDataSourceIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/datasource/PoolingDataSourceIT.java
@@ -174,8 +174,8 @@
 		ExecutorService executor = Executors.newFixedThreadPool(tasks.length);
 
 		for (int j = 0; j < 100; j++) {
-			for (int i = 0; i < tasks.length; i++) {
-				executor.submit(tasks[i]);
+			for (PoolTask task : tasks) {
+				executor.submit(task);
 			}
 		}
 
@@ -192,8 +192,8 @@
 			throw new RuntimeException(e);
 		}
 
-		for (int i = 0; i < tasks.length; i++) {
-			assertEquals(100, tasks[i].i.get());
+		for (PoolTask task : tasks) {
+			assertEquals(100, task.i.get());
 		}
 	}
 
@@ -204,7 +204,6 @@
 		@Override
 		public void run() {
 
-			i.incrementAndGet();
 			try {
 				Connection c = dataSource.getConnection();
 				try {
@@ -220,6 +219,7 @@
 						} finally {
 							rs.close();
 						}
+
 					} finally {
 						st.close();
 					}
@@ -228,6 +228,9 @@
 					c.close();
 				}
 
+				// increment only after success
+				i.incrementAndGet();
+
 			} catch (SQLException e) {
 				e.printStackTrace();
 			}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
index ab67596..89b889c 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
@@ -85,6 +85,19 @@
 		assertNotNull(a2);
 		assertEquals("artist2", a2.getArtistName());
 	}
+	
+	@Test
+	public void testIntPk_SelectFirst() throws Exception {
+		createTwoArtists();
+
+		Artist a3 = SelectById.query(Artist.class, 3).selectFirst(context);
+		assertNotNull(a3);
+		assertEquals("artist3", a3.getArtistName());
+
+		Artist a2 = SelectById.query(Artist.class, 2).selectFirst(context);
+		assertNotNull(a2);
+		assertEquals("artist2", a2.getArtistName());
+	}
 
 	@Test
 	public void testMapPk() throws Exception {
@@ -129,13 +142,13 @@
 
 	@Test
 	public void testMetadataCacheKey() throws Exception {
-		SelectById<Painting> q1 = SelectById.query(Painting.class, 4).useLocalCache();
+		SelectById<Painting> q1 = SelectById.query(Painting.class, 4).localCache();
 		QueryMetadata md1 = q1.getMetaData(resolver);
 		assertNotNull(md1);
 		assertNotNull(md1.getCacheKey());
 
 		SelectById<Painting> q2 = SelectById.query(Painting.class, singletonMap(Painting.PAINTING_ID_PK_COLUMN, 4))
-				.useLocalCache();
+				.localCache();
 		QueryMetadata md2 = q2.getMetaData(resolver);
 		assertNotNull(md2);
 		assertNotNull(md2.getCacheKey());
@@ -144,20 +157,20 @@
 		// cache entry
 		assertEquals(md1.getCacheKey(), md2.getCacheKey());
 
-		SelectById<Painting> q3 = SelectById.query(Painting.class, 5).useLocalCache();
+		SelectById<Painting> q3 = SelectById.query(Painting.class, 5).localCache();
 		QueryMetadata md3 = q3.getMetaData(resolver);
 		assertNotNull(md3);
 		assertNotNull(md3.getCacheKey());
 		assertNotEquals(md1.getCacheKey(), md3.getCacheKey());
 
-		SelectById<Artist> q4 = SelectById.query(Artist.class, 4).useLocalCache();
+		SelectById<Artist> q4 = SelectById.query(Artist.class, 4).localCache();
 		QueryMetadata md4 = q4.getMetaData(resolver);
 		assertNotNull(md4);
 		assertNotNull(md4.getCacheKey());
 		assertNotEquals(md1.getCacheKey(), md4.getCacheKey());
 
-		SelectById<Painting> q5 = SelectById.query(Painting.class,
-				new ObjectId("Painting", Painting.PAINTING_ID_PK_COLUMN, 4)).useLocalCache();
+		SelectById<Painting> q5 = SelectById
+				.query(Painting.class, new ObjectId("Painting", Painting.PAINTING_ID_PK_COLUMN, 4)).localCache();
 		QueryMetadata md5 = q5.getMetaData(resolver);
 		assertNotNull(md5);
 		assertNotNull(md5.getCacheKey());
@@ -177,7 +190,7 @@
 
 			@Override
 			public void execute() {
-				a3[0] = SelectById.query(Artist.class, 3).useLocalCache("g1").selectOne(context);
+				a3[0] = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
 				assertNotNull(a3[0]);
 				assertEquals("artist3", a3[0].getArtistName());
 			}
@@ -187,7 +200,7 @@
 
 			@Override
 			public void execute() {
-				Artist a3cached = SelectById.query(Artist.class, 3).useLocalCache("g1").selectOne(context);
+				Artist a3cached = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
 				assertSame(a3[0], a3cached);
 			}
 		});
@@ -198,7 +211,7 @@
 
 			@Override
 			public void execute() {
-				SelectById.query(Artist.class, 3).useLocalCache("g1").selectOne(context);
+				SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
 			}
 		}));
 	}
@@ -218,7 +231,7 @@
 				assertNotNull(a3);
 				assertEquals("artist3", a3.getArtistName());
 				assertEquals(2, a3.getPaintingArray().size());
-				
+
 				a3.getPaintingArray().get(0).getPaintingTitle();
 				a3.getPaintingArray().get(1).getPaintingTitle();
 			}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java b/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
index 66892d2..f1ecab2 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
@@ -34,4 +34,7 @@
         return null;
     }
 
+    @Override
+    public void close() throws RemoteException {
+    }
 }
diff --git a/cayenne-tools/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java b/cayenne-tools/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
index 0b9a555..f36fd68 100644
--- a/cayenne-tools/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
+++ b/cayenne-tools/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
@@ -66,11 +66,14 @@
             }
         }
     }
-    
+
     @Override
     public void addQueries(Collection<QueryDescriptor> queries) {
-        if (queries != null) {
-            artifacts.add(new ClientDataMapArtifact(dataMap, queries));
+        if (artifactsGenerationMode == ArtifactsGenerationMode.DATAMAP
+                || artifactsGenerationMode == ArtifactsGenerationMode.ALL) {
+            if (queries != null) {
+                artifacts.add(new ClientDataMapArtifact(dataMap, queries));
+            }
         }
     }
 }
diff --git a/cayenne-tools/src/main/java/org/apache/cayenne/gen/DataMapUtils.java b/cayenne-tools/src/main/java/org/apache/cayenne/gen/DataMapUtils.java
index a6caf08..64a3a38 100644
--- a/cayenne-tools/src/main/java/org/apache/cayenne/gen/DataMapUtils.java
+++ b/cayenne-tools/src/main/java/org/apache/cayenne/gen/DataMapUtils.java
@@ -40,10 +40,10 @@
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.map.PathComponent;
+import org.apache.cayenne.map.QueryDescriptor;
+import org.apache.cayenne.map.SelectQueryDescriptor;
 import org.apache.cayenne.map.naming.NameConverter;
 import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.util.CayenneMapEntry;
 import org.apache.commons.collections.set.ListOrderedSet;
 
@@ -63,7 +63,7 @@
 	 * @param query
 	 * @return Method name that perform query.
 	 */
-	public String getQueryMethodName(Query query) {
+	public String getQueryMethodName(QueryDescriptor query) {
 		return NameConverter.underscoredToJava(query.getName(), true);
 	}
 
@@ -73,7 +73,7 @@
 	 * @param query
 	 * @return Parameter names.
 	 */
-	public Collection getParameterNames(SelectQuery<?> query) {
+	public Collection getParameterNames(SelectQueryDescriptor query) {
 
 		if (query.getQualifier() == null) {
 			return Collections.EMPTY_SET;
@@ -89,7 +89,7 @@
 		return parseQualifier(query.getQualifier().toString());
 	}
 
-	public Boolean isValidParameterNames(SelectQuery<?> query) {
+	public Boolean isValidParameterNames(SelectQueryDescriptor query) {
 
 		if (query.getQualifier() == null) {
 			return true;
@@ -106,18 +106,16 @@
 			}
 		}
 
-		if (query instanceof SelectQuery) {
-			for (Ordering ordering : ((SelectQuery<?>) query).getOrderings()) {
-				// validate paths in ordering
-				String path = ordering.getSortSpecString();
-				Iterator<CayenneMapEntry> it = ((ObjEntity) query.getRoot()).resolvePathComponents(path);
-				while (it.hasNext()) {
-					try {
-						it.next();
-					} catch (ExpressionException e) {
-						// if we have wrong path in orderings return false.
-						return false;
-					}
+		for (Ordering ordering : query.getOrderings()) {
+			// validate paths in ordering
+			String path = ordering.getSortSpecString();
+			Iterator<CayenneMapEntry> it = ((ObjEntity) query.getRoot()).resolvePathComponents(path);
+			while (it.hasNext()) {
+				try {
+					it.next();
+				} catch (ExpressionException e) {
+					// if we have wrong path in orderings return false.
+					return false;
 				}
 			}
 		}
@@ -144,7 +142,7 @@
 		return result;
 	}
 
-	public boolean hasParameters(SelectQuery<?> query) {
+	public boolean hasParameters(SelectQueryDescriptor query) {
 		Map queryParameters = queriesMap.get(query.getName());
 
 		if (queryParameters == null) {
@@ -162,7 +160,7 @@
 	 * @param name
 	 * @return Parameter type.
 	 */
-	public String getParameterType(SelectQuery<?> query, String name) {
+	public String getParameterType(SelectQueryDescriptor query, String name) {
 		return queriesMap.get(query.getName()).get(name);
 	}
 
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index e1118b2..88cabdd 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -19,10 +19,23 @@
 CAY-2062 MappedSelect and MappedExec fluent query API
 CAY-2063 ProcedureCall fluent query API
 CAY-2065 Pluggable serialization and connectivity layers for ROP
+CAY-2073 Ordering.orderedList methods
+CAY-2074 Support for catalogs in stored procedures
+CAY-2076 Implement Jetty HTTP/1.1 and HTTP/2 Client support for ROP Client
+CAY-2083 Implement Protostuff as serialization service for Cayenne ROP
+CAY-2090 Untangle HttpRemoteService from ServiceContext thread local setup
 
 Bug Fixes:
 
 CAY-2064 Issue with BeanAccessor for classes with complex inheritance
+CAY-2066 Fixes for inner enums handling in ExtendedTypeMap
+CAY-2067 Cayenne 4.0 connection pool is occasionally running out of connections
+CAY-2078 Client code gen bug. Unnecessary DataMap class generation setting datamap gen to false.
+CAY-2080 Cayenne doesn't pick up reverse engineering file changes
+CAY-2084 ObjectIdQuery - no cache access polymorphism
+CAY-2086 SelectById.selectFirst stack overflow
+CAY-2087 PostCommitFilter is confused about changes made by Pre* listeners
+CAY-2089 HTTP connections aren't always closed in new ROP implementation
 
 ----------------------------------
 Release: 4.0.M3
diff --git a/docs/doc/src/main/resources/UPGRADE.txt b/docs/doc/src/main/resources/UPGRADE.txt
index fb5408d..e8fc253 100644
--- a/docs/doc/src/main/resources/UPGRADE.txt
+++ b/docs/doc/src/main/resources/UPGRADE.txt
@@ -6,8 +6,10 @@
 -------------------------------------------------------------------------------
 UPGRADING TO 4.0.M4
 
-* 4.0.M4 changes the way queries are stored in the mapping files, so all existing *.map.xml files should be upgraded.
-  To do that open each of your existing projects in the new CayenneModeler. Agree to perform an upgrade when asked.
+* Per CAY-2060 4.0.M4 changes the way queries are stored in the mapping files, so all existing *.map.xml files should be upgraded.
+  To do that open each of your existing projects in the new CayenneModeler. Agree to perform an upgrade when asked. 
+
+  Also EntityResolver.getQuery(String) method is removed. If you relied on it, consider switching to MappedSelect or MappedExec query, or if you absolutely need to get a hold of specific query, use EntityResolver.getQueryDescriptor(String).buildQuery().
 
 * Per CAY-2065 ROPHessianServlet has been discarded in favor of new implementation called ROPServlet,
   so if you were using org.apache.cayenne.configuration.rop.server.ROPHessianServlet in your web.xml configuration,
diff --git a/docs/docbook/cayenne-guide/src/docbkx/including-cayenne-in-project.xml b/docs/docbook/cayenne-guide/src/docbkx/including-cayenne-in-project.xml
index 7b1358d..ae7f81a 100644
--- a/docs/docbook/cayenne-guide/src/docbkx/including-cayenne-in-project.xml
+++ b/docs/docbook/cayenne-guide/src/docbkx/including-cayenne-in-project.xml
@@ -192,7 +192,7 @@
                             <td><code>overwrite</code></td>
                             <td>boolean</td>
                             <td>Only has effect when "makePairs" is set to "false". If "overwrite"
-                                os "true", will overwrite older versions of generated classes.</td>
+                                is "true", will overwrite older versions of generated classes.</td>
                         </tr>
                         <tr>
                             <td><code>superPkg</code></td>
diff --git a/docs/docbook/cayenne-guide/src/docbkx/orderings.xml b/docs/docbook/cayenne-guide/src/docbkx/orderings.xml
index f1b9c0e..a3a6bde 100644
--- a/docs/docbook/cayenne-guide/src/docbkx/orderings.xml
+++ b/docs/docbook/cayenne-guide/src/docbkx/orderings.xml
@@ -19,7 +19,7 @@
     <title>Orderings</title>
         <para>An Ordering object defines how a list of objects should be ordered. Orderings are
             essentially path expressions combined with a sorting strategy. Creating an Ordering:
-            <programlisting language="java">Ordering o = new Ordering(Painting.NAME_PROPERTY, SortOrder.ASENDING);</programlisting></para>
+            <programlisting language="java">Ordering o = new Ordering(Painting.NAME_PROPERTY, SortOrder.ASCENDING);</programlisting></para>
         <para>Like expressions, orderings are translated into SQL as parts of queries (and the sorting
         occurs in the database). Also like expressions, orderings can be used in memory, naturally -
         to sort
diff --git a/docs/docbook/cayenne-guide/src/docbkx/performance-tuning.xml b/docs/docbook/cayenne-guide/src/docbkx/performance-tuning.xml
index e0aa8d0..41f2d98 100644
--- a/docs/docbook/cayenne-guide/src/docbkx/performance-tuning.xml
+++ b/docs/docbook/cayenne-guide/src/docbkx/performance-tuning.xml
@@ -26,20 +26,23 @@
             application of prefetching is to refresh stale object relationships, so more generally
             it can be viewed as a technique for managing subsets of the object graph.</para>
         <para>Prefetching example:
-            <programlisting language="java">SelectQuery query = new SelectQuery(Artist.class);
+            <programlisting language="java">ObjectSelect&lt;Artist> query = ObjectSelect.query(Artist.class);
 
-// this instructs Cayenne to prefetch one of Artist's relationships
-query.addPrefetch("paintings");
+// instructs Cayenne to prefetch one of Artist's relationships
+query.prefetch(Artist.PAINTINGS.disjoint());
+
+// the above line is equivalent to the following: 
+// query.prefetch("paintings", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
 
 // query is expecuted as usual, but the resulting Artists will have
 // their paintings "inflated"
-List&lt;Artist> artists = context.performQuery(query);</programlisting>All
-            types of relationships can be preftetched - to-one, to-many, flattened. </para>
-        <para>A prefetch can span multiple relationships:
-            <programlisting language="java"> query.addPrefetch("paintings.gallery");</programlisting></para>
+List&lt;Artist> artists = query.select(context);</programlisting>All
+            types of relationships can be preftetched - to-one, to-many, flattened. A prefetch can
+            span multiple relationships:
+            <programlisting language="java">query.prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());</programlisting></para>
         <para>A query can have multiple
-            prefetches:<programlisting language="java">query.addPrefetch("paintings");
-query.addPrefetch("paintings.gallery"); </programlisting></para>
+            prefetches:<programlisting language="java">query.prefetch(Artist.PAINTINGS.disjoint());
+query.prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());</programlisting></para>
         <para>If a query is fetching DataRows, all "disjoint" prefetches are ignored, only "joint"
             prefetches are executed (see prefetching semantics discussion below for what disjoint and
             joint prefetches mean).</para>
@@ -51,28 +54,23 @@
                 query root objects with related objects fully resolved. However semantics can affect
                 preformance, in some cases significantly. There are 3 types of prefetch semantics,
                 all defined as constants in
-                org.apache.cayenne.query.PrefetchTreeNode:<programlisting language="java">PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS
+                <code>org.apache.cayenne.query.PrefetchTreeNode</code>:<programlisting language="java">PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS
 PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS
 PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS</programlisting></para>
-            <para>Each query has a default prefetch semantics, so generally users do not have to
-                worry about changing it, except when performance is a concern, or a few special
-                cases when a default sematics can't produce the correct result. SelectQuery uses
-                DISJOINT_PREFETCH_SEMANTICS by default. Semantics can be changed as
-                follows:<programlisting language="java">SelectQuery query = new SelectQuery(Artist.class);
-query.addPrefetch("paintings").setSemantics(
-                PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); </programlisting></para>
-            <para>There's no limitation on mixing different types of semantics in the same
-                SelectQuery. Multiple prefetches each can have its own semantics. </para>
-            <para>SQLTemplate and ProcedureQuery are both using JOINT_PREFETCH_SEMANTICS and it can
-                not be changed due to the nature of these two queries.</para>
+            <para>There's no limitation on mixing different types of semantics in the same query.
+                Each prefetch can have its own semantics. <code>SelectQuery</code> uses
+                    <code>DISJOINT_PREFETCH_SEMANTICS</code> by default. <code>ObjectSelect</code>
+                requires explicit semantics as we've seen above. <code>SQLTemplate</code> and
+                    <code>ProcedureQuery</code> are both using <code>JOINT_PREFETCH_SEMANTICS</code>
+                and it can not be changed due to the nature of those two queries.</para>
         </section>
         <section xml:id="disjoint-prefetch-semantics">
             <title>Disjoint Prefetching Semantics</title>
-            <para>This semantics (only applicable to SelectQuery) results in Cayenne generatiing one
-                SQL statement for the main objects, and a separate statement for each prefetch path
-                (hence "disjoint" - related objects are not fetched with the main query). Each
-                additional SQL statement uses a qualifier of the main query plus a set of joins
-                traversing the preftech path between the main and related entity. </para>
+            <para>This semantics results in Cayenne generatiing one SQL statement for the main
+                objects, and a separate statement for each prefetch path (hence "disjoint" - related
+                objects are not fetched with the main query). Each additional SQL statement uses a
+                qualifier of the main query plus a set of joins traversing the preftech path between
+                the main and related entity. </para>
             <para>This strategy has an advantage of efficient JVM memory use, and faster overall
                 result processing by Cayenne, but it requires (1+N) SQL statements to be executed,
                 where N is the number of prefetched relationships.</para>
@@ -141,15 +139,12 @@
             in the application in many cases. So performance sensitive selects should consider
             DataRows - it saves memory and CPU cycles. All selecting queries support DataRows
             option,
-            e.g.:<programlisting language="java">SelectQuery query = new SelectQuery(Artist.class);
-query.setFetchingDataRows(true);
+            e.g.:<programlisting language="java">ObjectSelect&lt;DataRow> query = ObjectSelect.dataRowQuery(Artist.class);
 
-List&lt;DataRow> rows = context.performQuery(query); </programlisting><programlisting language="java">SQLTemplate query = new SQLTemplate(Artist.class, "SELECT * FROM ARTIST");
-query.setFetchingDataRows(true);
-
-List&lt;DataRow> rows = context.performQuery(query);</programlisting></para>
-        <para>Moreover DataRows may be converted to Persistent objects later as needed. So e.g. you
-            may implement some in-memory filtering, only converting a subset of fetched
+List&lt;DataRow> rows = query.select(context);</programlisting><programlisting language="java">SQLSelect&lt;DataRow> query = SQLSelect.dataRowQuery("SELECT * FROM ARTIST");
+List&lt;DataRow> rows = query.select(context);</programlisting></para>
+        <para>Individual DataRows may be converted to Persistent objects as needed. So e.g. you may
+            implement some in-memory filtering, only converting a subset of fetched
             objects:<programlisting language="java">// you need to cast ObjectContext to DataContext to get access to 'objectFromDataRow'
 DataContext dataContext = (DataContext) context;
 
@@ -213,9 +208,9 @@
     // do something with the object...
     ...
 });</programlisting></para>
-        <para>Another example is a BatchIterator that allows to process more than one object in each
-            iteration. This is a common scenario in various data processing jobs - read a batch of
-            objects, process them, commit  the results, and then repeat. This allows to further
+        <para>Another example is a batch iterator that allows to process more than one object in
+            each iteration. This is a common scenario in various data processing jobs - read a batch
+            of objects, process them, commit the results, and then repeat. This allows to further
             optimize processing (e.g. by avoiding frequent
             commits).<programlisting>try(ResultBatchIterator&lt;Artist> it = ObjectSelect.query(Artist.class).iterator(context)) {
     for(List&lt;Artist> list : it) {
@@ -234,19 +229,14 @@
             appears to be a list of Persistent objects - there's no iterator to close or DataRows to
             convert to objects:</para>
         <para>
-            <programlisting language="java">SelectQuery query = new SelectQuery(Artist.class);
-query.setPageSize(50);
-
-// the fact that result is paginated is transparent
-List&lt;Artist> artists = ctxt.performQuery(query);</programlisting>
+            <programlisting language="java">// the fact that result is paginated is transparent
+List&lt;Artist> artists = 
+    ObjectSelect.query(Artist.class).pageSize(50).select(context);</programlisting>
         </para>
         <para>Having said that, DataRows option can be combined with pagination, providing the best
             of both
-            worlds:<programlisting language="java">SelectQuery query = new SelectQuery(Artist.class);
-query.setPageSize(50);
-query.setFetchingDataRows(true);
-
-List&lt;DataRow> rows = ctxt.performQuery(query);</programlisting></para>
+            worlds:<programlisting language="java">List&lt;DataRow> rows = 
+    ObjectSelect.dataRowQuery(Artist.class).pageSize(50).select(context);</programlisting></para>
         <para>The way pagination works internally, it first fetches a list of IDs for the root
             entity of the query. This is very fast and initially takes very little memory. Then when
             an object is requested at an arbitrary index in the list, this object and adjacent
diff --git a/docs/docbook/cayenne-guide/src/docbkx/persistent-objects-objectcontext.xml b/docs/docbook/cayenne-guide/src/docbkx/persistent-objects-objectcontext.xml
index a47af25..404cab1 100644
--- a/docs/docbook/cayenne-guide/src/docbkx/persistent-objects-objectcontext.xml
+++ b/docs/docbook/cayenne-guide/src/docbkx/persistent-objects-objectcontext.xml
@@ -267,7 +267,7 @@
             that spans more than one Cayenne operation. E.g. two sequential commits that need to be
             rolled back together in case of failure. This can be done via
                 <code>ServerRuntime.performInTransaction</code>
-            method:<programlisting>Integer result = runtime.performInTransaction(new TransactionalOperation&lt;Integer>() {
+            method:<programlisting>Integer result = runtime.performInTransaction(() -> {
     // commit one or more contexts
     context1.commitChanges();
     context2.commitChanges();
@@ -279,5 +279,10 @@
     // return an arbitrary result or null if we don't care about the result
     return 5;
 });</programlisting></para>
+        <para>When inside the transaction, current thread Transaction object can be accessed via a
+            static method. E.g. here is an example that initializes transaction JDBC connection with
+            a custom connection object
+            :<programlisting>Transaction tx = BaseTransaction.getThreadTransaction();
+tx.addConnection("mydatanode", myConnection); </programlisting></para>
     </section>
 </chapter>
diff --git a/docs/docbook/cayenne-guide/src/docbkx/rop-deployment.xml b/docs/docbook/cayenne-guide/src/docbkx/rop-deployment.xml
index 3c327da..4fdc932 100644
--- a/docs/docbook/cayenne-guide/src/docbkx/rop-deployment.xml
+++ b/docs/docbook/cayenne-guide/src/docbkx/rop-deployment.xml
@@ -23,7 +23,7 @@
         <note><para>Recent versions of Tomcat and Jetty containers (e.g. Tomcat 6 and 7, Jetty 8) contain code
                 addressing a security concern related to "session fixation problem" by resetting the
                 existing session ID of any request that requires BASIC authentcaition. If ROP
-                service is protected with declarative security (see the the ROP tutorial and the
+                service is protected with declarative security (see the ROP tutorial and the
                 following chapters on security), this feature prevents the ROP client from attaching
                 to its session, resulting in MissingSessionExceptions. To solve that you will need
                 to either switch to an alternative security mechanism, or disable "session fixation
diff --git a/docs/docbook/getting-started-rop/src/docbkx/authentication.xml b/docs/docbook/getting-started-rop/src/docbkx/authentication.xml
index 0872044..ebf4241 100644
--- a/docs/docbook/getting-started-rop/src/docbkx/authentication.xml
+++ b/docs/docbook/getting-started-rop/src/docbkx/authentication.xml
@@ -115,14 +115,14 @@
         <para>Which is exactly what you'd expect, as the client is not authenticating itself. So
             change the line in Main.java where we obtained an ROP connection to this:</para>
         <programlisting language="java">Map&lt;String,String> properties = new HashMap&lt;>();
-properties.put(Constants.ROP_SERVICE_URL_PROPERTY, "http://localhost:8080/tutorial-rop-server/cayenne-service");
+properties.put(Constants.ROP_SERVICE_URL_PROPERTY, "http://localhost:8080/tutorial/cayenne-service");
 properties.put(Constants.ROP_SERVICE_USERNAME_PROPERTY, "cayenne-user");
 properties.put(Constants.ROP_SERVICE_PASSWORD_PROPERTY, "secret");
 
 ClientRuntime runtime = new ClientRuntime(properties);</programlisting>
         <para>Try running again, and everything should work as before. Obviously in production
             environment, in addition to authentication you'll need to use HTTPS to access the server
-            to prevent third-party evesdropping on your password and data.</para>
+            to prevent third-party eavesdropping on your password and data.</para>
         <para>Congratulations, you are done with the ROP tutorial!</para>
     </section>
 </chapter>
diff --git a/docs/docbook/getting-started-rop/src/docbkx/client-code.xml b/docs/docbook/getting-started-rop/src/docbkx/client-code.xml
index 6dc3902..76dd9fd 100644
--- a/docs/docbook/getting-started-rop/src/docbkx/client-code.xml
+++ b/docs/docbook/getting-started-rop/src/docbkx/client-code.xml
@@ -37,7 +37,7 @@
         <para>Now the part that is actually different from regular Cayenne - establishing the server
             connection and obtaining the ObjectContext:</para>
         <programlisting language="java">Map&lt;String, String> properties = new HashMap&lt;>();
-properties.put(Constants.ROP_SERVICE_URL_PROPERTY, "http://localhost:8080/tutorial-rop-server/cayenne-service");
+properties.put(Constants.ROP_SERVICE_URL_PROPERTY, "http://localhost:8080/tutorial/cayenne-service");
 properties.put(Constants.ROP_SERVICE_USERNAME_PROPERTY, "cayenne-user");
 properties.put(Constants.ROP_SERVICE_PASSWORD_PROPERTY, "secret");
 
diff --git a/docs/docbook/getting-started-rop/src/docbkx/web-service.xml b/docs/docbook/getting-started-rop/src/docbkx/web-service.xml
index ad60d44..62ad70f 100644
--- a/docs/docbook/getting-started-rop/src/docbkx/web-service.xml
+++ b/docs/docbook/getting-started-rop/src/docbkx/web-service.xml
@@ -73,7 +73,7 @@
     <section xml:id="configuring-web-xml">
         <title>Configuring web.xml</title>
         <para>Cayenne web service is declared in the web.xml. It is implemented as a servlet
-            "org.apache.cayenne.configuration.rop.server.ROPHessianServlet". Open
+            "org.apache.cayenne.rop.ROPServlet". Open
             tutorial/src/main/webapp/WEB-INF/web.xml in Eclipse and add a service declaration: </para>
         <programlisting>&lt;?xml version="1.0" encoding="utf-8"?>
  &lt;!DOCTYPE web-app
diff --git a/docs/docbook/getting-started/src/docbkx/webapp.xml b/docs/docbook/getting-started/src/docbkx/webapp.xml
index 8882b87..bec1e8b 100644
--- a/docs/docbook/getting-started/src/docbkx/webapp.xml
+++ b/docs/docbook/getting-started/src/docbkx/webapp.xml
@@ -90,7 +90,7 @@
 
 &lt;% 
     SelectQuery query = new SelectQuery(Artist.class);
-    query.addOrdering(Artist.NAME_PROPERTY, SortOrder.ASCENDING);
+    query.addOrdering(Artist.NAME.getName(), SortOrder.ASCENDING);
 
     ObjectContext context = BaseContext.getThreadObjectContext();
     List&lt;Artist&gt; artists = context.performQuery(query);
@@ -192,9 +192,17 @@
     </section>
     <section xml:id="running-webapp">
         <title>Running Web Application</title>
-        <para>To run the web application we'll use "maven-jetty-plugin". To activate it, let's add
-            the following piece of code to the "pom.xml" file, following the "dependencies" section
-            and save the POM:</para>
+        <para>We need to provide javax servlet-api for our application.</para>
+        <programlisting>&lt;dependency&gt;
+        &lt;groupId>javax.servlet&lt;/groupId&gt;
+        &lt;artifactId>javax.servlet-api&lt;/artifactId&gt;
+        &lt;version>3.1.0&lt;/version&gt;
+        &lt;scope>provided&lt;/scope&gt;
+&lt;/dependency&gt;</programlisting>
+
+        <para>Also to run the web application we'll use "maven-jetty-plugin". To activate it,
+            let's add the following piece of code to the "pom.xml" file, following the "dependencies"
+            section and save the POM:</para>
         <programlisting>&lt;build&gt;
     &lt;plugins&gt;
         &lt;plugin&gt;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectFileChangeTracker.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectFileChangeTracker.java
index bbb8e74..20bb12b 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectFileChangeTracker.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectFileChangeTracker.java
@@ -84,6 +84,12 @@
             while (it.hasNext()) {
                 DataMap dm = it.next();
                 addFile(dm.getConfigurationSource().getURL().getPath());
+
+                if (dm.getReverseEngineering() != null) {
+                    if (dm.getReverseEngineering().getConfigurationSource() != null) {
+                        addFile(dm.getReverseEngineering().getConfigurationSource().getURL().getPath());
+                    }
+                }
             }
 
         }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/CreateProcedureAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/CreateProcedureAction.java
index 830ebca..1c5397f 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/CreateProcedureAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/CreateProcedureAction.java
@@ -71,6 +71,7 @@
 	public void createProcedure(DataMap map, Procedure procedure) {
 		ProjectController mediator = getProjectController();
 		procedure.setSchema(map.getDefaultSchema());
+		procedure.setCatalog(map.getDefaultCatalog());
 		map.addProcedure(procedure);
 		fireProcedureEvent(this, mediator, map, procedure);
 	}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ProcedureTab.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ProcedureTab.java
index 041c04a..6d1d0a9 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ProcedureTab.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ProcedureTab.java
@@ -40,8 +40,7 @@
 import org.apache.cayenne.util.Util;
 import org.apache.cayenne.validation.ValidationException;
 
-import com.jgoodies.forms.builder.PanelBuilder;
-import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.builder.DefaultFormBuilder;
 import com.jgoodies.forms.layout.FormLayout;
 
 /**
@@ -54,6 +53,7 @@
     protected ProjectController eventController;
     protected TextAdapter name;
     protected TextAdapter schema;
+    protected TextAdapter catalog;
     protected JCheckBox returnsValue;
     protected boolean ignoreChange;
 
@@ -81,27 +81,28 @@
             }
         };
 
-        this.returnsValue = new JCheckBox();
+        this.catalog = new TextAdapter(new JTextField()) {
+
+            protected void updateModel(String text) {
+                setCatalog(text);
+            }
+        };
 
         JLabel returnValueHelp = new JLabel("(first parameter will be used as return value)");
         returnValueHelp.setFont(returnValueHelp.getFont().deriveFont(10));
 
-        // assemble
-        FormLayout layout = new FormLayout("right:max(50dlu;pref), 3dlu, left:max(20dlu;pref), 3dlu, fill:150dlu",
-                "p, 3dlu, p, 3dlu, p, 3dlu, p");
+        this.returnsValue = new JCheckBox();
+        this.returnsValue.setToolTipText(returnValueHelp.getText());
 
-        CellConstraints cc = new CellConstraints();
-        PanelBuilder builder = new PanelBuilder(layout);
+        FormLayout layout = new FormLayout("right:pref, 3dlu, fill:200dlu", "");
+        DefaultFormBuilder builder = new DefaultFormBuilder(layout);
         builder.setDefaultDialogBorder();
 
-        builder.addSeparator("Stored Procedure Configuration", cc.xywh(1, 1, 5, 1));
-        builder.addLabel("Procedure Name:", cc.xy(1, 3));
-        builder.add(name.getComponent(), cc.xywh(3, 3, 3, 1));
-        builder.addLabel("Schema:", cc.xy(1, 5));
-        builder.add(schema.getComponent(), cc.xywh(3, 5, 3, 1));
-        builder.addLabel("Returns Value:", cc.xy(1, 7));
-        builder.add(returnsValue, cc.xy(3, 7));
-        builder.add(returnValueHelp, cc.xy(5, 7));
+        builder.appendSeparator("Stored Procedure Configuration");
+        builder.append("Procedure Name:", name.getComponent());
+        builder.append("Catalog:", catalog.getComponent());
+        builder.append("Schema:", schema.getComponent());
+        builder.append("Returns Value:", returnsValue);
 
         this.setLayout(new BorderLayout());
         this.add(builder.getPanel(), BorderLayout.CENTER);
@@ -139,6 +140,7 @@
 
         name.setText(procedure.getName());
         schema.setText(procedure.getSchema());
+        catalog.setText(procedure.getCatalog());
 
         ignoreChange = true;
         returnsValue.setSelected(procedure.isReturningValue());
@@ -181,4 +183,17 @@
             eventController.fireProcedureEvent(new ProcedureEvent(this, procedure));
         }
     }
+
+    void setCatalog(String text) {
+        if (text != null && text.trim().length() == 0) {
+            text = null;
+        }
+
+        Procedure procedure = eventController.getCurrentProcedure();
+
+        if (procedure != null && !Util.nullSafeEquals(procedure.getCatalog(), text)) {
+            procedure.setCatalog(text);
+            eventController.fireProcedureEvent(new ProcedureEvent(this, procedure));
+        }
+    }
 }
diff --git a/plugins/maven-cayenne-plugin/src/main/java/org/apache/cayenne/tools/DbImporterMojo.java b/plugins/maven-cayenne-plugin/src/main/java/org/apache/cayenne/tools/DbImporterMojo.java
index f3198d8..ace3369 100644
--- a/plugins/maven-cayenne-plugin/src/main/java/org/apache/cayenne/tools/DbImporterMojo.java
+++ b/plugins/maven-cayenne-plugin/src/main/java/org/apache/cayenne/tools/DbImporterMojo.java
@@ -152,23 +152,23 @@
     private final OldFilterConfigBridge filterBuilder = new OldFilterConfigBridge();
 
     /**
-     * If true, would use primitives instead of numeric and boolean classes.
+     * An object that contains reverse engineering rules.
      *
      * @parameter reverseEngineering="reverseEngineering"
      */
     private ReverseEngineering reverseEngineering = new ReverseEngineering();
 
-    /**
-     * Flag which defines from where to take the configuration of cdbImport.
-     * If we define the config of cdbImport in pom.xml
-     * we should set it to true or it will be setted to true automatically
-     * if we will define some configuration parameters in pom.xml
-     * Else it remains default(false) and for cdbImport
-     * we use the configuration defined in signed dataMap
-     *
-     *  @parameter isReverseEngineeringDefined="isReverseEngineeringDefined" default-value="false"
-     */
-    private boolean isReverseEngineeringDefined = false;
+	/**
+	 * Flag which defines from where to take the configuration of cdbImport. If
+	 * we define the config of cdbImport in pom.xml we should set it to true or
+	 * it will be set to true automatically if we define some configuration
+	 * parameters in pom.xml. Else it remains default(false) and for cdbImport
+	 * we use the configuration defined in signed dataMap
+	 *
+	 * @parameter isReverseEngineeringDefined="isReverseEngineeringDefined"
+	 *            default-value="false"
+	 */
+	private boolean isReverseEngineeringDefined = false;
 
     public void setIsReverseEngineeringDefined(boolean isReverseEngineeringDefined) {
         this.isReverseEngineeringDefined = isReverseEngineeringDefined;
diff --git a/pom.xml b/pom.xml
index 3b070b3..16bd539 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,9 +39,9 @@
 		<project.build.datetime>${maven.build.timestamp}</project.build.datetime>
         <jacoco.version>0.7.1.201405082137</jacoco.version>
 
-        <pmd.skip>false</pmd.skip>
-        <checkstyle.skip>false</checkstyle.skip>
-        <findbugs.skip>false</findbugs.skip>
+        <pmd.skip>true</pmd.skip>
+        <checkstyle.skip>true</checkstyle.skip>
+        <findbugs.skip>true</findbugs.skip>
 
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@@ -64,7 +64,7 @@
 		<module>plugins</module>
 		<module>tutorials</module>
 		<module>docs</module>
-    </modules>
+	</modules>
 	<issueManagement>
 		<system>jira</system>
 		<url>https://issues.apache.org/jira/browse/CAY</url>
@@ -1457,7 +1457,9 @@
 			</activation>
 			<modules>
 				<module>cayenne-java8</module>
-                <module>assembly</module>
+				<module>cayenne-client-jetty</module>
+				<module>cayenne-protostuff</module>
+				<module>assembly</module>
 			</modules>
 		</profile>
 	</profiles>
diff --git a/tutorials/pom.xml b/tutorials/pom.xml
index 17b4c06..fc467ab 100644
--- a/tutorials/pom.xml
+++ b/tutorials/pom.xml
@@ -35,4 +35,17 @@
 		<module>tutorial-rop-server</module>
 		<module>tutorial-rop-client</module>
 	</modules>
+
+	<profiles>
+		<profile>
+			<id>cayenne-java8-module-to-build</id>
+			<activation>
+				<jdk>[1.8,)</jdk>
+			</activation>
+			<modules>
+				<module>tutorial-rop-server-http2</module>
+				<module>tutorial-rop-client-http2</module>
+			</modules>
+		</profile>
+	</profiles>
 </project>
diff --git a/tutorials/tutorial-rop-client-http2/pom.xml b/tutorials/tutorial-rop-client-http2/pom.xml
new file mode 100644
index 0000000..15e57de
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+	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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>cayenne-tutorials-parent</artifactId>
+        <groupId>org.apache.cayenne.tutorials</groupId>
+        <version>4.0.M4-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>tutorial-rop-client-http2</artifactId>
+    <name>Cayenne ROP HTTP/2 Client Tutorial</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <main.class>org.apache.cayenne.tutorial.Http2Client</main.class>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-client-jetty</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-protostuff</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-java8</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/Http2Client.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/Http2Client.java
new file mode 100644
index 0000000..97b589e
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/Http2Client.java
@@ -0,0 +1,127 @@
+/*****************************************************************
+ * 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.tutorial;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.rop.client.ClientJettyHttp2Module;
+import org.apache.cayenne.configuration.rop.client.ClientRuntime;
+import org.apache.cayenne.configuration.rop.client.ProtostuffModule;
+import org.apache.cayenne.java8.CayenneJava8Module;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.rop.JettyHttp2ClientConnectionProvider;
+import org.apache.cayenne.rop.http.JettyHttpROPConnector;
+import org.apache.cayenne.rop.protostuff.ProtostuffROPSerializationService;
+import org.apache.cayenne.tutorial.persistent.client.Artist;
+import org.apache.cayenne.tutorial.persistent.client.Gallery;
+import org.apache.cayenne.tutorial.persistent.client.Painting;
+
+import javax.net.ssl.HttpsURLConnection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This example uses {@link ProtostuffROPSerializationService} which is bound in {@link ProtostuffModule}
+ * and {@link JettyHttpROPConnector} initialized by {@link JettyHttp2ClientConnectionProvider}, which is bound
+ * in {@link ClientJettyHttp2Module}. It works without ALPN by default.
+ * <p>
+ * In order to run it with ALPN, you have to set {@link Constants#ROP_SERVICE_USE_ALPN_PROPERTY} to true
+ * and provide the alpn-boot-XXX.jar into the bootstrap classpath.
+ */
+public class Http2Client {
+
+    public static void main(String[] args) throws Exception {
+        HttpsURLConnection.setDefaultHostnameVerifier((hostname, sslSession) -> hostname.equals("localhost"));
+        System.setProperty("javax.net.ssl.trustStore", Http2Client.class.getResource("/keystore").getPath());
+
+        // Setting Protostuff properties
+        System.setProperty("protostuff.runtime.collection_schema_on_repeated_fields", "true");
+        System.setProperty("protostuff.runtime.morph_collection_interfaces", "true");
+        System.setProperty("protostuff.runtime.morph_map_interfaces", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_collection_fields", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_map_fields", "true");
+
+        Map<String, String> properties = new HashMap<>();
+        properties.put(Constants.ROP_SERVICE_URL_PROPERTY, "https://localhost:8443/");
+        properties.put(Constants.ROP_SERVICE_USE_ALPN_PROPERTY, "false");
+        properties.put(Constants.ROP_SERVICE_USERNAME_PROPERTY, "cayenne-user");
+        properties.put(Constants.ROP_SERVICE_PASSWORD_PROPERTY, "secret");
+        properties.put(Constants.ROP_SERVICE_REALM_PROPERTY, "Cayenne Realm");
+
+        ClientRuntime runtime = new ClientRuntime(properties,
+                new ClientJettyHttp2Module(),
+                new ProtostuffModule(),
+                new CayenneJava8Module());
+
+        ObjectContext context = runtime.newContext();
+
+        newObjectsTutorial(context);
+        selectTutorial(context);
+        deleteTutorial(context);
+
+        runtime.shutdown();
+    }
+
+    static void newObjectsTutorial(ObjectContext context) {
+
+        // creating new Artist
+        Artist picasso = context.newObject(Artist.class);
+        picasso.setName("Pablo Picasso");
+
+        // Creating other objects
+        Gallery metropolitan = context.newObject(Gallery.class);
+        metropolitan.setName("Metropolitan Museum of Art");
+
+        Painting girl = context.newObject(Painting.class);
+        girl.setName("Girl Reading at a Table");
+
+        Painting stein = context.newObject(Painting.class);
+        stein.setName("Gertrude Stein");
+
+        // connecting objects together via relationships
+        picasso.addToPaintings(girl);
+        picasso.addToPaintings(stein);
+
+        girl.setGallery(metropolitan);
+        stein.setGallery(metropolitan);
+
+        // saving all the changes above
+        context.commitChanges();
+    }
+
+    static void selectTutorial(ObjectContext context) {
+        // ObjectSelect examples
+        List<Painting> paintings1 = ObjectSelect.query(Painting.class).select(context);
+
+        List<Painting> paintings2 = ObjectSelect.query(Painting.class)
+                .where(Painting.NAME.likeIgnoreCase("gi%")).select(context);
+    }
+
+    static void deleteTutorial(ObjectContext context) {
+        // Delete object example
+        Artist picasso = ObjectSelect.query(Artist.class).where(Artist.NAME.eq("Pablo Picasso")).selectOne(context);
+
+        if (picasso != null) {
+            context.deleteObjects(picasso);
+            context.commitChanges();
+        }
+    }
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java
new file mode 100644
index 0000000..efe03bd
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Artist;
+
+/**
+ * A persistent class mapped as "Artist" Cayenne entity.
+ */
+public class Artist extends _Artist {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java
new file mode 100644
index 0000000..54553d9
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Gallery;
+
+/**
+ * A persistent class mapped as "Gallery" Cayenne entity.
+ */
+public class Gallery extends _Gallery {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java
new file mode 100644
index 0000000..2084706
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Painting;
+
+/**
+ * A persistent class mapped as "Painting" Cayenne entity.
+ */
+public class Painting extends _Painting {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java
new file mode 100644
index 0000000..d98d444
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java
@@ -0,0 +1,96 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Painting;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "Artist" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Artist extends PersistentObject {
+
+    public static final Property<LocalDate> DATE_OF_BIRTH = new Property<LocalDate>("dateOfBirth");
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    protected LocalDate dateOfBirth;
+    protected String name;
+    protected List<Painting> paintings;
+
+    public LocalDate getDateOfBirth() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateOfBirth", false);
+        }
+
+        return dateOfBirth;
+    }
+    public void setDateOfBirth(LocalDate dateOfBirth) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateOfBirth", false);
+        }
+
+        Object oldValue = this.dateOfBirth;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "dateOfBirth", oldValue, dateOfBirth);
+        }
+        
+        this.dateOfBirth = dateOfBirth;
+    }
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public List<Painting> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        return paintings;
+    }
+    public void addToPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+    public void removeFromPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java
new file mode 100644
index 0000000..6189902
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java
@@ -0,0 +1,72 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Painting;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "Gallery" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Gallery extends PersistentObject {
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    protected String name;
+    protected List<Painting> paintings;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public List<Painting> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        return paintings;
+    }
+    public void addToPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+    public void removeFromPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java
new file mode 100644
index 0000000..23c14e2
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java
@@ -0,0 +1,98 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.ValueHolder;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Artist;
+import org.apache.cayenne.tutorial.persistent.client.Gallery;
+import org.apache.cayenne.util.PersistentObjectHolder;
+
+/**
+ * A generated persistent class mapped as "Painting" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Painting extends PersistentObject {
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<Artist> ARTIST = new Property<Artist>("artist");
+    public static final Property<Gallery> GALLERY = new Property<Gallery>("gallery");
+
+    protected String name;
+    protected ValueHolder artist;
+    protected ValueHolder gallery;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public Artist getArtist() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder(this, "artist");
+		}
+
+        return (Artist) artist.getValue();
+    }
+    public void setArtist(Artist artist) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder(this, "artist");
+		}
+
+        // note how we notify ObjectContext of change BEFORE the object is actually
+        // changed... this is needed to take a valid current snapshot
+        Object oldValue = this.artist.getValueDirectly();
+        if (objectContext != null) {
+        	objectContext.propertyChanged(this, "artist", oldValue, artist);
+        }
+        
+        this.artist.setValue(artist);
+    }
+
+    public Gallery getGallery() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "gallery", true);
+        } else if (this.gallery == null) {
+        	this.gallery = new PersistentObjectHolder(this, "gallery");
+		}
+
+        return (Gallery) gallery.getValue();
+    }
+    public void setGallery(Gallery gallery) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "gallery", true);
+        } else if (this.gallery == null) {
+        	this.gallery = new PersistentObjectHolder(this, "gallery");
+		}
+
+        // note how we notify ObjectContext of change BEFORE the object is actually
+        // changed... this is needed to take a valid current snapshot
+        Object oldValue = this.gallery.getValueDirectly();
+        if (objectContext != null) {
+        	objectContext.propertyChanged(this, "gallery", oldValue, gallery);
+        }
+        
+        this.gallery.setValue(gallery);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-client-http2/src/main/resources/keystore b/tutorials/tutorial-rop-client-http2/src/main/resources/keystore
new file mode 100644
index 0000000..d6592f9
--- /dev/null
+++ b/tutorials/tutorial-rop-client-http2/src/main/resources/keystore
Binary files differ
diff --git a/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java b/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
index ef75d21..ddb00e5 100644
--- a/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
+++ b/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
@@ -18,15 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.tutorial.persistent.client;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.rop.client.ClientRuntime;
 import org.apache.cayenne.query.ObjectSelect;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 public class Main {
 
     public static void main(String[] args) {
@@ -43,6 +43,7 @@
         newObjectsTutorial(context);
         selectTutorial(context);
         deleteTutorial(context);
+        runtime.shutdown();
     }
 
     static void newObjectsTutorial(ObjectContext context) {
diff --git a/tutorials/tutorial-rop-server-http2/pom.xml b/tutorials/tutorial-rop-server-http2/pom.xml
new file mode 100644
index 0000000..0ba2ddd
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>cayenne-tutorials-parent</artifactId>
+        <groupId>org.apache.cayenne.tutorials</groupId>
+        <version>4.0.M4-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>tutorial-rop-server-http2</artifactId>
+    <name>Cayenne ROP HTTP/2 Server Tutorial</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <main.class>org.apache.cayenne.tutorial.Http2Server</main.class>
+        <jetty.version>9.3.10.v20160621</jetty.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-protostuff</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-java8</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.derby</groupId>
+            <artifactId>derby</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- Jetty's dependencies-->
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlets</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2ROPServlet.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2ROPServlet.java
new file mode 100644
index 0000000..1ae9865
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2ROPServlet.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ * 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.tutorial;
+
+import org.apache.cayenne.configuration.rop.client.ProtostuffModule;
+import org.apache.cayenne.configuration.rop.server.ROPServerModule;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.configuration.web.WebConfiguration;
+import org.apache.cayenne.configuration.web.WebUtil;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.java8.CayenneJava8Module;
+import org.apache.cayenne.remote.RemoteService;
+import org.apache.cayenne.rop.ROPSerializationService;
+import org.apache.cayenne.rop.ROPServlet;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import java.util.Collection;
+import java.util.Map;
+
+public class Http2ROPServlet extends ROPServlet {
+
+    @Override
+    public void init(ServletConfig configuration) throws ServletException {
+
+        checkAlreadyConfigured(configuration.getServletContext());
+
+        this.servletContext = configuration.getServletContext();
+
+        WebConfiguration configAdapter = new WebConfiguration(configuration);
+
+        String configurationLocation = configAdapter.getConfigurationLocation();
+        Map<String, String> eventBridgeParameters = configAdapter.getOtherParameters();
+
+        Collection<Module> modules = configAdapter.createModules(
+                new ROPServerModule(eventBridgeParameters),
+                new ProtostuffModule(),
+                new CayenneJava8Module());
+
+        ServerRuntime runtime = new ServerRuntime(configurationLocation, modules
+                .toArray(new Module[modules.size()]));
+
+        this.remoteService = runtime.getInjector().getInstance(RemoteService.class);
+        this.serializationService = runtime.getInjector().getInstance(ROPSerializationService.class);
+
+        WebUtil.setCayenneRuntime(servletContext, runtime);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2Server.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2Server.java
new file mode 100644
index 0000000..20028fd
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/Http2Server.java
@@ -0,0 +1,107 @@
+/*****************************************************************
+ * 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.tutorial;
+
+import org.eclipse.jetty.http2.HTTP2Cipher;
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.security.ConstraintMapping;
+import org.eclipse.jetty.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Credential;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import static org.eclipse.jetty.util.resource.Resource.newClassPathResource;
+
+/**
+ * Based on the example org.eclipse.jetty.embedded.Http2Server included in the jetty-project distribution.
+ * <p>
+ * This server works without ALPN and could handle only HTTP/2 protocol.
+ */
+public class Http2Server {
+
+    public static void main(String... args) throws Exception {
+        // Setting Protostuff properties
+        System.setProperty("protostuff.runtime.collection_schema_on_repeated_fields", "true");
+        System.setProperty("protostuff.runtime.morph_collection_interfaces", "true");
+        System.setProperty("protostuff.runtime.morph_map_interfaces", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_collection_fields", "true");
+        System.setProperty("protostuff.runtime.pojo_schema_on_map_fields", "true");
+
+        Server server = new Server();
+
+        ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
+        context.addServlet(new ServletHolder("cayenne-project", new Http2ROPServlet()), "/");
+        context.setSecurityHandler(basicAuth("cayenne-user", "secret", "Cayenne Realm"));
+        server.setHandler(context);
+
+        // HTTPS Configuration
+        HttpConfiguration httpsConfig = new HttpConfiguration();
+        httpsConfig.setSecureScheme("https");
+        httpsConfig.setSecurePort(8443);
+        httpsConfig.addCustomizer(new SecureRequestCustomizer());
+
+        // SSL Context Factory for HTTPS and HTTP/2
+        SslContextFactory sslContextFactory = new SslContextFactory();
+        sslContextFactory.setKeyStoreResource(newClassPathResource("keystore"));
+        sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
+        sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+        sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
+
+        // SSL Connection Factory
+        SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, "h2");
+
+        // HTTP/2 Connector
+        ServerConnector http2Connector = new ServerConnector(server, ssl, new HTTP2ServerConnectionFactory(httpsConfig));
+        http2Connector.setPort(8443);
+        server.addConnector(http2Connector);
+
+        server.start();
+        server.join();
+    }
+
+    private static SecurityHandler basicAuth(String username, String password, String realm) {
+        HashLoginService loginService = new HashLoginService();
+        loginService.putUser(username, Credential.getCredential(password), new String[]{"cayenne-service-user"});
+        loginService.setName(realm);
+
+        Constraint constraint = new Constraint();
+        constraint.setName(Constraint.__BASIC_AUTH);
+        constraint.setRoles(new String[]{"cayenne-service-user"});
+        constraint.setAuthenticate(true);
+
+        ConstraintMapping constraintMapping = new ConstraintMapping();
+        constraintMapping.setConstraint(constraint);
+        constraintMapping.setPathSpec("/*");
+
+        ConstraintSecurityHandler constraintSecurityHandler = new ConstraintSecurityHandler();
+        constraintSecurityHandler.setAuthenticator(new BasicAuthenticator());
+        constraintSecurityHandler.setRealmName(realm);
+        constraintSecurityHandler.addConstraintMapping(constraintMapping);
+        constraintSecurityHandler.setLoginService(loginService);
+
+        return constraintSecurityHandler;
+    }
+}
\ No newline at end of file
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
new file mode 100644
index 0000000..9d627d4
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.tutorial.persistent;
+
+import org.apache.cayenne.tutorial.persistent.auto._Artist;
+
+public class Artist extends _Artist {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
new file mode 100644
index 0000000..21b8349
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.tutorial.persistent;
+
+import org.apache.cayenne.tutorial.persistent.auto._Gallery;
+
+public class Gallery extends _Gallery {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
new file mode 100644
index 0000000..d0165f0
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.tutorial.persistent;
+
+import org.apache.cayenne.tutorial.persistent.auto._Painting;
+
+public class Painting extends _Painting {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
new file mode 100644
index 0000000..f9ee738
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
@@ -0,0 +1,52 @@
+package org.apache.cayenne.tutorial.persistent.auto;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.Painting;
+
+/**
+ * Class _Artist was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Artist extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<LocalDate> DATE_OF_BIRTH = new Property<LocalDate>("dateOfBirth");
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    public void setDateOfBirth(LocalDate dateOfBirth) {
+        writeProperty("dateOfBirth", dateOfBirth);
+    }
+    public LocalDate getDateOfBirth() {
+        return (LocalDate)readProperty("dateOfBirth");
+    }
+
+    public void setName(String name) {
+        writeProperty("name", name);
+    }
+    public String getName() {
+        return (String)readProperty("name");
+    }
+
+    public void addToPaintings(Painting obj) {
+        addToManyTarget("paintings", obj, true);
+    }
+    public void removeFromPaintings(Painting obj) {
+        removeToManyTarget("paintings", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<Painting> getPaintings() {
+        return (List<Painting>)readProperty("paintings");
+    }
+
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
new file mode 100644
index 0000000..bac8eb9
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
@@ -0,0 +1,43 @@
+package org.apache.cayenne.tutorial.persistent.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.Painting;
+
+/**
+ * Class _Gallery was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Gallery extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    public void setName(String name) {
+        writeProperty("name", name);
+    }
+    public String getName() {
+        return (String)readProperty("name");
+    }
+
+    public void addToPaintings(Painting obj) {
+        addToManyTarget("paintings", obj, true);
+    }
+    public void removeFromPaintings(Painting obj) {
+        removeToManyTarget("paintings", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<Painting> getPaintings() {
+        return (List<Painting>)readProperty("paintings");
+    }
+
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
new file mode 100644
index 0000000..876ad07
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
@@ -0,0 +1,49 @@
+package org.apache.cayenne.tutorial.persistent.auto;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.Artist;
+import org.apache.cayenne.tutorial.persistent.Gallery;
+
+/**
+ * Class _Painting was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Painting extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<Artist> ARTIST = new Property<Artist>("artist");
+    public static final Property<Gallery> GALLERY = new Property<Gallery>("gallery");
+
+    public void setName(String name) {
+        writeProperty("name", name);
+    }
+    public String getName() {
+        return (String)readProperty("name");
+    }
+
+    public void setArtist(Artist artist) {
+        setToOneTarget("artist", artist, true);
+    }
+
+    public Artist getArtist() {
+        return (Artist)readProperty("artist");
+    }
+
+
+    public void setGallery(Gallery gallery) {
+        setToOneTarget("gallery", gallery, true);
+    }
+
+    public Gallery getGallery() {
+        return (Gallery)readProperty("gallery");
+    }
+
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java
new file mode 100644
index 0000000..efe03bd
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Artist.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Artist;
+
+/**
+ * A persistent class mapped as "Artist" Cayenne entity.
+ */
+public class Artist extends _Artist {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java
new file mode 100644
index 0000000..54553d9
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Gallery.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Gallery;
+
+/**
+ * A persistent class mapped as "Gallery" Cayenne entity.
+ */
+public class Gallery extends _Gallery {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java
new file mode 100644
index 0000000..2084706
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/Painting.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.tutorial.persistent.client;
+
+import org.apache.cayenne.tutorial.persistent.client.auto._Painting;
+
+/**
+ * A persistent class mapped as "Painting" Cayenne entity.
+ */
+public class Painting extends _Painting {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java
new file mode 100644
index 0000000..d98d444
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Artist.java
@@ -0,0 +1,96 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Painting;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "Artist" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Artist extends PersistentObject {
+
+    public static final Property<LocalDate> DATE_OF_BIRTH = new Property<LocalDate>("dateOfBirth");
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    protected LocalDate dateOfBirth;
+    protected String name;
+    protected List<Painting> paintings;
+
+    public LocalDate getDateOfBirth() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateOfBirth", false);
+        }
+
+        return dateOfBirth;
+    }
+    public void setDateOfBirth(LocalDate dateOfBirth) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "dateOfBirth", false);
+        }
+
+        Object oldValue = this.dateOfBirth;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "dateOfBirth", oldValue, dateOfBirth);
+        }
+        
+        this.dateOfBirth = dateOfBirth;
+    }
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public List<Painting> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        return paintings;
+    }
+    public void addToPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+    public void removeFromPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java
new file mode 100644
index 0000000..6189902
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Gallery.java
@@ -0,0 +1,72 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Painting;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "Gallery" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Gallery extends PersistentObject {
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<List<Painting>> PAINTINGS = new Property<List<Painting>>("paintings");
+
+    protected String name;
+    protected List<Painting> paintings;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public List<Painting> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        return paintings;
+    }
+    public void addToPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+    public void removeFromPaintings(Painting object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java
new file mode 100644
index 0000000..23c14e2
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/java/org/apache/cayenne/tutorial/persistent/client/auto/_Painting.java
@@ -0,0 +1,98 @@
+package org.apache.cayenne.tutorial.persistent.client.auto;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.ValueHolder;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.tutorial.persistent.client.Artist;
+import org.apache.cayenne.tutorial.persistent.client.Gallery;
+import org.apache.cayenne.util.PersistentObjectHolder;
+
+/**
+ * A generated persistent class mapped as "Painting" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _Painting extends PersistentObject {
+
+    public static final Property<String> NAME = new Property<String>("name");
+    public static final Property<Artist> ARTIST = new Property<Artist>("artist");
+    public static final Property<Gallery> GALLERY = new Property<Gallery>("gallery");
+
+    protected String name;
+    protected ValueHolder artist;
+    protected ValueHolder gallery;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        return name;
+    }
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        Object oldValue = this.name;
+        // notify objectContext about simple property change
+        if(objectContext != null) {
+            objectContext.propertyChanged(this, "name", oldValue, name);
+        }
+        
+        this.name = name;
+    }
+
+    public Artist getArtist() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder(this, "artist");
+		}
+
+        return (Artist) artist.getValue();
+    }
+    public void setArtist(Artist artist) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder(this, "artist");
+		}
+
+        // note how we notify ObjectContext of change BEFORE the object is actually
+        // changed... this is needed to take a valid current snapshot
+        Object oldValue = this.artist.getValueDirectly();
+        if (objectContext != null) {
+        	objectContext.propertyChanged(this, "artist", oldValue, artist);
+        }
+        
+        this.artist.setValue(artist);
+    }
+
+    public Gallery getGallery() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "gallery", true);
+        } else if (this.gallery == null) {
+        	this.gallery = new PersistentObjectHolder(this, "gallery");
+		}
+
+        return (Gallery) gallery.getValue();
+    }
+    public void setGallery(Gallery gallery) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "gallery", true);
+        } else if (this.gallery == null) {
+        	this.gallery = new PersistentObjectHolder(this, "gallery");
+		}
+
+        // note how we notify ObjectContext of change BEFORE the object is actually
+        // changed... this is needed to take a valid current snapshot
+        Object oldValue = this.gallery.getValueDirectly();
+        if (objectContext != null) {
+        	objectContext.propertyChanged(this, "gallery", oldValue, gallery);
+        }
+        
+        this.gallery.setValue(gallery);
+    }
+
+}
diff --git a/tutorials/tutorial-rop-server-http2/src/main/resources/cayenne-project.xml b/tutorials/tutorial-rop-server-http2/src/main/resources/cayenne-project.xml
new file mode 100644
index 0000000..05c2351
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/resources/cayenne-project.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain project-version="8">
+	<map name="datamap"/>
+
+	<node name="datanode"
+		 factory="org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory"
+		 schema-update-strategy="org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy"
+		>
+		<map-ref name="datamap"/>
+		<data-source>
+			<driver value="org.apache.derby.jdbc.EmbeddedDriver"/>
+			<url value="jdbc:derby:memory:testdb;create=true"/>
+			<connectionPool min="1" max="1"/>
+			<login/>
+		</data-source>
+	</node>
+</domain>
diff --git a/tutorials/tutorial-rop-server-http2/src/main/resources/datamap.map.xml b/tutorials/tutorial-rop-server-http2/src/main/resources/datamap.map.xml
new file mode 100644
index 0000000..c77fcf6
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/resources/datamap.map.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/8/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/8/modelMap http://cayenne.apache.org/schema/8/modelMap.xsd"
+	 project-version="8">
+	<property name="defaultPackage" value="org.apache.cayenne.tutorial.persistent"/>
+	<property name="clientSupported" value="true"/>
+	<property name="defaultClientPackage" value="org.apache.cayenne.tutorial.persistent.client"/>
+	<db-entity name="ARTIST">
+		<db-attribute name="DATE_OF_BIRTH" type="DATE"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<db-entity name="GALLERY">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<db-entity name="PAINTING">
+		<db-attribute name="ARTIST_ID" type="INTEGER"/>
+		<db-attribute name="GALLERY_ID" type="INTEGER"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<obj-entity name="Artist" className="org.apache.cayenne.tutorial.persistent.Artist" clientClassName="org.apache.cayenne.tutorial.persistent.client.Artist" dbEntityName="ARTIST">
+		<obj-attribute name="dateOfBirth" type="java.time.LocalDate" db-attribute-path="DATE_OF_BIRTH"/>
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<obj-entity name="Gallery" className="org.apache.cayenne.tutorial.persistent.Gallery" clientClassName="org.apache.cayenne.tutorial.persistent.client.Gallery" dbEntityName="GALLERY">
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<obj-entity name="Painting" className="org.apache.cayenne.tutorial.persistent.Painting" clientClassName="org.apache.cayenne.tutorial.persistent.client.Painting" dbEntityName="PAINTING">
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<db-relationship name="paintings" source="ARTIST" target="PAINTING" toMany="true">
+		<db-attribute-pair source="ID" target="ARTIST_ID"/>
+	</db-relationship>
+	<db-relationship name="paintings" source="GALLERY" target="PAINTING" toMany="true">
+		<db-attribute-pair source="ID" target="GALLERY_ID"/>
+	</db-relationship>
+	<db-relationship name="artist" source="PAINTING" target="ARTIST" toMany="false">
+		<db-attribute-pair source="ARTIST_ID" target="ID"/>
+	</db-relationship>
+	<db-relationship name="gallery" source="PAINTING" target="GALLERY" toMany="false">
+		<db-attribute-pair source="GALLERY_ID" target="ID"/>
+	</db-relationship>
+	<obj-relationship name="paintings" source="Artist" target="Painting" deleteRule="Cascade" db-relationship-path="paintings"/>
+	<obj-relationship name="paintings" source="Gallery" target="Painting" deleteRule="Nullify" db-relationship-path="paintings"/>
+	<obj-relationship name="artist" source="Painting" target="Artist" deleteRule="Nullify" db-relationship-path="artist"/>
+	<obj-relationship name="gallery" source="Painting" target="Gallery" deleteRule="Nullify" db-relationship-path="gallery"/>
+</data-map>
diff --git a/tutorials/tutorial-rop-server-http2/src/main/resources/keystore b/tutorials/tutorial-rop-server-http2/src/main/resources/keystore
new file mode 100644
index 0000000..d6592f9
--- /dev/null
+++ b/tutorials/tutorial-rop-server-http2/src/main/resources/keystore
Binary files differ
diff --git a/tutorials/tutorial-rop-server/pom.xml b/tutorials/tutorial-rop-server/pom.xml
index 96a6286..0bba067 100644
--- a/tutorials/tutorial-rop-server/pom.xml
+++ b/tutorials/tutorial-rop-server/pom.xml
@@ -32,6 +32,10 @@
 			<version>${project.version}</version>
 		</dependency>
 		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>servlet-api</artifactId>
+		</dependency>
+		<dependency>
 			<groupId>org.apache.derby</groupId>
 			<artifactId>derby</artifactId>
 			<scope>compile</scope>
diff --git a/tutorials/tutorial-rop-server/src/main/resources/cayenne-project.xml b/tutorials/tutorial-rop-server/src/main/resources/cayenne-project.xml
index 9c4a8a5..5957e5c 100644
--- a/tutorials/tutorial-rop-server/src/main/resources/cayenne-project.xml
+++ b/tutorials/tutorial-rop-server/src/main/resources/cayenne-project.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<domain project-version="6">
+<domain project-version="7">
 	<map name="datamap"/>
 
 	<node name="datanode"
diff --git a/tutorials/tutorial-rop-server/src/main/resources/datamap.map.xml b/tutorials/tutorial-rop-server/src/main/resources/datamap.map.xml
index 4b05708..466cd06 100644
--- a/tutorials/tutorial-rop-server/src/main/resources/datamap.map.xml
+++ b/tutorials/tutorial-rop-server/src/main/resources/datamap.map.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-<data-map xmlns="http://cayenne.apache.org/schema/3.0/modelMap"
+<data-map xmlns="http://cayenne.apache.org/schema/7/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/3.0/modelMap http://cayenne.apache.org/schema/3.0/modelMap.xsd"
-	 project-version="6">
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/7/modelMap http://cayenne.apache.org/schema/7/modelMap.xsd"
+	 project-version="7">
 	<property name="defaultPackage" value="org.apache.cayenne.tutorial.persistent"/>
 	<property name="clientSupported" value="true"/>
 	<property name="defaultClientPackage" value="org.apache.cayenne.tutorial.persistent.client"/>
diff --git a/tutorials/tutorial/pom.xml b/tutorials/tutorial/pom.xml
index 6c2daf6..88d2a19 100644
--- a/tutorials/tutorial/pom.xml
+++ b/tutorials/tutorial/pom.xml
@@ -31,8 +31,13 @@
 			<version>${project.version}</version>
 		</dependency>
 		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>servlet-api</artifactId>
+		</dependency>
+		<dependency>
 			<groupId>org.apache.derby</groupId>
 			<artifactId>derby</artifactId>
+			<scope>compile</scope>
 		</dependency>
 	</dependencies>
 
diff --git a/tutorials/tutorial/src/main/resources/cayenne-project.xml b/tutorials/tutorial/src/main/resources/cayenne-project.xml
index 9c4a8a5..5957e5c 100644
--- a/tutorials/tutorial/src/main/resources/cayenne-project.xml
+++ b/tutorials/tutorial/src/main/resources/cayenne-project.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<domain project-version="6">
+<domain project-version="7">
 	<map name="datamap"/>
 
 	<node name="datanode"
diff --git a/tutorials/tutorial/src/main/resources/datamap.map.xml b/tutorials/tutorial/src/main/resources/datamap.map.xml
index 614fba1..4875f89 100644
--- a/tutorials/tutorial/src/main/resources/datamap.map.xml
+++ b/tutorials/tutorial/src/main/resources/datamap.map.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-<data-map xmlns="http://cayenne.apache.org/schema/3.0/modelMap"
+<data-map xmlns="http://cayenne.apache.org/schema/7/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/3.0/modelMap http://cayenne.apache.org/schema/3.0/modelMap.xsd"
-	 project-version="6">
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/7/modelMap http://cayenne.apache.org/schema/7/modelMap.xsd"
+	 project-version="7">
 	<property name="defaultPackage" value="org.apache.cayenne.tutorial.persistent"/>
 	<db-entity name="ARTIST">
 		<db-attribute name="DATE_OF_BIRTH" type="DATE"/>
diff --git a/tutorials/tutorial/src/main/webapp/detail.jsp b/tutorials/tutorial/src/main/webapp/detail.jsp
index d031a8d..d012be0 100644
--- a/tutorials/tutorial/src/main/webapp/detail.jsp
+++ b/tutorials/tutorial/src/main/webapp/detail.jsp
@@ -21,7 +21,6 @@
 <%@ page language="java" contentType="text/html" %>
 <%@ page import="org.apache.cayenne.tutorial.persistent.*" %>
 <%@ page import="org.apache.cayenne.*" %>
-<%@ page import="java.util.*" %>
 <%@ page import="java.text.*" %>
 
 <% 
diff --git a/tutorials/tutorial/src/main/webapp/index.jsp b/tutorials/tutorial/src/main/webapp/index.jsp
index da44af1..b2de13e 100644
--- a/tutorials/tutorial/src/main/webapp/index.jsp
+++ b/tutorials/tutorial/src/main/webapp/index.jsp
@@ -22,12 +22,11 @@
 <%@ page import="org.apache.cayenne.tutorial.persistent.*" %>
 <%@ page import="org.apache.cayenne.*" %>
 <%@ page import="org.apache.cayenne.query.*" %>
-<%@ page import="org.apache.cayenne.exp.*" %>
 <%@ page import="java.util.*" %>
 
 <% 
     SelectQuery query = new SelectQuery(Artist.class);
-    query.addOrdering(Artist.NAME_PROPERTY, SortOrder.ASCENDING);
+    query.addOrdering(Artist.NAME.getName(), SortOrder.ASCENDING);
 
     ObjectContext context = BaseContext.getThreadObjectContext();
     List<Artist> artists = context.performQuery(query);