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>