CAY-2076 Implement Jetty HTTP/1.1 and HTTP/2 Client support for ROP Client
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-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/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 6cf687e..88cabdd 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -21,6 +21,7 @@
 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
 
diff --git a/pom.xml b/pom.xml
index a68e6b5..16bd539 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1457,6 +1457,7 @@
 			</activation>
 			<modules>
 				<module>cayenne-java8</module>
+				<module>cayenne-client-jetty</module>
 				<module>cayenne-protostuff</module>
 				<module>assembly</module>
 			</modules>
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-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>