CAY-2076 Adding close() method to ROP API; Taking out some common methods to ROPUtil;
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
index 06b842c..4ab182e 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/HttpClientConnection.java
@@ -19,6 +19,7 @@
 package org.apache.cayenne.rop;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.di.BeforeScopeEnd;
 import org.apache.cayenne.event.EventBridge;
 import org.apache.cayenne.event.EventBridgeFactory;
 import org.apache.cayenne.remote.BaseConnection;
@@ -26,6 +27,8 @@
 import org.apache.cayenne.remote.RemoteService;
 import org.apache.cayenne.remote.RemoteSession;
 
+import java.rmi.RemoteException;
+
 public class HttpClientConnection extends BaseConnection {
 
 	private RemoteService remoteService;
@@ -71,6 +74,11 @@
         return createServerEventBridge(session);
 	}
 
+    @BeforeScopeEnd
+    public void shutdown() throws RemoteException {
+            remoteService.close();
+    }
+
 	protected synchronized void connect() {
 		if (session != null) {
 			return;
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
index 6a0a8a6..943dca0 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ProxyRemoteService.java
@@ -64,4 +64,13 @@
             throw new RemoteException(e.getMessage(), e);
         }
     }
+
+    @Override
+    public void close() throws RemoteException {
+        try {
+            ropConnector.close();
+        } catch (IOException e) {
+            throw new RemoteException("Exception while closing ROP resources", e);
+        }
+    }
 }
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
index 5855cf8..38a0579 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPConnector.java
@@ -37,11 +37,16 @@
 	 * Creates a new session with the specified or joins an existing one. This method is
 	 * used to bootstrap collaborating clients of a single "group chat".
 	 */
-    InputStream establishSharedSession(String name) throws IOException;
+    InputStream establishSharedSession(String sharedSessionName) throws IOException;
 
 	/**
 	 * Processes message on a remote server, returning the result of such processing.
 	 */
     InputStream sendMessage(byte[] message) throws IOException;
+
+	/**
+	 * Close all resources related to ROP Connector.
+	 */
+	void close() throws IOException;
     
 }
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java
new file mode 100644
index 0000000..cb5cca9
--- /dev/null
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/ROPUtil.java
@@ -0,0 +1,150 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.rop;
+
+import java.util.Map;
+
+public class ROPUtil {
+
+    public static String getLogConnect(String url, String username, boolean password) {
+        return getLogConnect(url, username, password, null);
+    }
+
+    public static String getLogConnect(String url, String username, boolean password, String sharedSessionName) {
+        StringBuilder log = new StringBuilder("Connecting to [");
+        if (username != null) {
+            log.append(username);
+
+            if (password) {
+                log.append(":*******");
+            }
+
+            log.append("@");
+        }
+
+        log.append(url);
+        log.append("]");
+
+        if (sharedSessionName != null) {
+            log.append(" - shared session '").append(sharedSessionName).append("'");
+        } else {
+            log.append(" - dedicated session.");
+        }
+
+        return log.toString();
+    }
+
+    public static String getLogDisconnect(String url, String username, boolean password) {
+        StringBuilder log = new StringBuilder("Disconnecting from [");
+        if (username != null) {
+            log.append(username);
+
+            if (password) {
+                log.append(":*******");
+            }
+
+            log.append("@");
+        }
+
+        log.append(url);
+        log.append("]");
+
+        return log.toString();
+    }
+
+    public static String getParamsAsString(Map<String, String> params) {
+        StringBuilder urlParams = new StringBuilder();
+
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            if (urlParams.length() > 0) {
+                urlParams.append('&');
+            }
+
+            urlParams.append(entry.getKey());
+            urlParams.append('=');
+            urlParams.append(entry.getValue());
+        }
+
+        return urlParams.toString();
+    }
+
+    public static String getBasicAuth(String username, String password) {
+        if (username != null && password != null) {
+            return "Basic " + base64(username + ":" + password);
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates the Base64 value.
+     */
+    public static String base64(String value) {
+        StringBuffer cb = new StringBuffer();
+
+        int i = 0;
+        for (i = 0; i + 2 < value.length(); i += 3) {
+            long chunk = (int) value.charAt(i);
+            chunk = (chunk << 8) + (int) value.charAt(i + 1);
+            chunk = (chunk << 8) + (int) value.charAt(i + 2);
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append(encode(chunk >> 6));
+            cb.append(encode(chunk));
+        }
+
+        if (i + 1 < value.length()) {
+            long chunk = (int) value.charAt(i);
+            chunk = (chunk << 8) + (int) value.charAt(i + 1);
+            chunk <<= 8;
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append(encode(chunk >> 6));
+            cb.append('=');
+        } else if (i < value.length()) {
+            long chunk = (int) value.charAt(i);
+            chunk <<= 16;
+
+            cb.append(encode(chunk >> 18));
+            cb.append(encode(chunk >> 12));
+            cb.append('=');
+            cb.append('=');
+        }
+
+        return cb.toString();
+    }
+
+    public static char encode(long d) {
+        d &= 0x3f;
+        if (d < 26)
+            return (char) (d + 'A');
+        else if (d < 52)
+            return (char) (d + 'a' - 26);
+        else if (d < 62)
+            return (char) (d + '0' - 52);
+        else if (d == 62)
+            return '+';
+        else
+            return '/';
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java b/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
index 15a54ed..bdae52b 100644
--- a/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
+++ b/cayenne-client/src/main/java/org/apache/cayenne/rop/http/HttpROPConnector.java
@@ -22,6 +22,7 @@
 import org.apache.cayenne.rop.HttpClientConnection;
 import org.apache.cayenne.rop.ROPConnector;
 import org.apache.cayenne.rop.ROPConstants;
+import org.apache.cayenne.rop.ROPUtil;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -66,7 +67,7 @@
     @Override
     public InputStream establishSession() throws IOException {
         if (logger.isInfoEnabled()) {
-            logConnect(null);
+            logger.info(ROPUtil.getLogConnect(url, username, password != null));
         }
 		
 		Map<String, String> requestParams = new HashMap<>();
@@ -76,14 +77,14 @@
     }
 
     @Override
-    public InputStream establishSharedSession(String name) throws IOException {
+    public InputStream establishSharedSession(String sharedSessionName) throws IOException {
         if (logger.isInfoEnabled()) {
-            logConnect(name);
+            logger.info(ROPUtil.getLogConnect(url, username, password != null, sharedSessionName));
         }
 
 		Map<String, String> requestParams = new HashMap<>();
 		requestParams.put(ROPConstants.OPERATION_PARAMETER, ROPConstants.ESTABLISH_SHARED_SESSION_OPERATION);
-		requestParams.put(ROPConstants.SESSION_NAME_PARAMETER, name);
+		requestParams.put(ROPConstants.SESSION_NAME_PARAMETER, sharedSessionName);
 
 		return doRequest(requestParams);
     }
@@ -92,40 +93,35 @@
     public InputStream sendMessage(byte[] message) throws IOException {
         return doRequest(message);
     }
-	
-	protected InputStream doRequest(Map<String, String> params) throws IOException {
-		URLConnection connection = new URL(url).openConnection();
 
-		StringBuilder urlParams = new StringBuilder();
+    @Override
+    public void close() throws IOException {
+        if (logger.isInfoEnabled()) {
+            logger.info(ROPUtil.getLogDisconnect(url, username, password != null));
+        }
+    }
 
-		for (Map.Entry<String, String> entry : params.entrySet()) {
-			if (urlParams.length() > 0) {
-				urlParams.append('&');
-			}
+    protected InputStream doRequest(Map<String, String> params) throws IOException {
+        URLConnection connection = new URL(url).openConnection();
 
-			urlParams.append(entry.getKey());
-			urlParams.append('=');
-			urlParams.append(entry.getValue());
-		}
+        if (readTimeout != null) {
+            connection.setReadTimeout(readTimeout.intValue());
+        }
 
-		if (readTimeout != null) {
-			connection.setReadTimeout(readTimeout.intValue());
-		}
+        addAuthHeader(connection);
 
-		addAuthHeader(connection);
+        connection.setDoOutput(true);
 
-		connection.setDoOutput(true);
-		
-		connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-		connection.setRequestProperty("charset", "utf-8");
+        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        connection.setRequestProperty("charset", "utf-8");
 
-		try (OutputStream output = connection.getOutputStream()) {
-			output.write(urlParams.toString().getBytes(StandardCharsets.UTF_8));
+        try (OutputStream output = connection.getOutputStream()) {
+            output.write(ROPUtil.getParamsAsString(params).getBytes(StandardCharsets.UTF_8));
             output.flush();
-		}
+        }
 
-		return connection.getInputStream();
-	} 
+        return connection.getInputStream();
+    }
 
     protected InputStream doRequest(byte[] data) throws IOException {
         URLConnection connection = new URL(url).openConnection();
@@ -151,7 +147,7 @@
     }
 
     protected void addAuthHeader(URLConnection connection) {
-        String basicAuth = getBasicAuth(username, password);
+        String basicAuth = ROPUtil.getBasicAuth(username, password);
 
         if (basicAuth != null) {
             connection.addRequestProperty("Authorization", basicAuth);
@@ -167,91 +163,4 @@
         }
     }
 
-    public String getBasicAuth(String user, String password) {
-        if (user != null && password != null) {
-            return "Basic " + base64(user + ":" + password);
-        }
-
-        return null;
-    }
-
-    /**
-     * Creates the Base64 value.
-     */
-    private String base64(String value) {
-        StringBuffer cb = new StringBuffer();
-
-        int i = 0;
-        for (i = 0; i + 2 < value.length(); i += 3) {
-            long chunk = (int) value.charAt(i);
-            chunk = (chunk << 8) + (int) value.charAt(i + 1);
-            chunk = (chunk << 8) + (int) value.charAt(i + 2);
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append(encode(chunk >> 6));
-            cb.append(encode(chunk));
-        }
-
-        if (i + 1 < value.length()) {
-            long chunk = (int) value.charAt(i);
-            chunk = (chunk << 8) + (int) value.charAt(i + 1);
-            chunk <<= 8;
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append(encode(chunk >> 6));
-            cb.append('=');
-        }
-        else if (i < value.length()) {
-            long chunk = (int) value.charAt(i);
-            chunk <<= 16;
-
-            cb.append(encode(chunk >> 18));
-            cb.append(encode(chunk >> 12));
-            cb.append('=');
-            cb.append('=');
-        }
-
-        return cb.toString();
-    }
-
-    public static char encode(long d) {
-        d &= 0x3f;
-        if (d < 26)
-            return (char) (d + 'A');
-        else if (d < 52)
-            return (char) (d + 'a' - 26);
-        else if (d < 62)
-            return (char) (d + '0' - 52);
-        else if (d == 62)
-            return '+';
-        else
-            return '/';
-    }
-
-    private void logConnect(String sharedSessionName) {
-        StringBuilder log = new StringBuilder("Connecting to [");
-        if (username != null) {
-            log.append(username);
-
-            if (password != null) {
-                log.append(":*******");
-            }
-
-            log.append("@");
-        }
-
-        log.append(url);
-        log.append("]");
-
-        if (sharedSessionName != null) {
-            log.append(" - shared session '").append(sharedSessionName).append("'");
-        }
-        else {
-            log.append(" - dedicated session.");
-        }
-
-        logger.info(log.toString());
-    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
index b5efb89..f357846 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/RemoteService.java
@@ -1,20 +1,20 @@
 /*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
  ****************************************************************/
 
 package org.apache.cayenne.remote;
@@ -24,7 +24,7 @@
 
 /**
  * Interface of a Cayenne remote service.
- * 
+ *
  * @since 1.2
  * @see org.apache.cayenne.rop.ROPServlet
  */
@@ -45,4 +45,10 @@
      * Processes message on a remote server, returning the result of such processing.
      */
     Object processMessage(ClientMessage message) throws RemoteException, Throwable;
+
+    /**
+     * Close remote service resources.
+     * @sine 4.0
+     */
+    void close() throws RemoteException;
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
index 2a63fbb..3716227 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/service/BaseRemoteService.java
@@ -19,10 +19,6 @@
 
 package org.apache.cayenne.remote.service;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.access.ClientServerChannel;
@@ -36,6 +32,11 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import java.rmi.RemoteException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * A generic implementation of an RemoteService. Can be subclassed to work with
  * different remoting mechanisms, such as Hessian or JAXRPC.
@@ -97,6 +98,7 @@
 	 */
 	protected abstract ServerSession getServerSession();
 
+	@Override
 	public RemoteSession establishSession() {
 		logger.debug("Session requested by client");
 
@@ -106,6 +108,7 @@
 		return session;
 	}
 
+	@Override
 	public RemoteSession establishSharedSession(String name) {
 		logger.debug("Shared session requested by client. Group name: " + name);
 
@@ -116,6 +119,7 @@
 		return createServerSession(name).getSession();
 	}
 
+	@Override
 	public Object processMessage(ClientMessage message) throws Throwable {
 
 		if (message == null) {
@@ -150,6 +154,10 @@
 		}
 	}
 
+	@Override
+	public void close() throws RemoteException {
+	}
+
 	protected RemoteSession createRemoteSession(String sessionId, String name, boolean enableEvents) {
 		RemoteSession session = (enableEvents) ? new RemoteSession(sessionId, eventBridgeFactoryName,
 				eventBridgeParameters) : new RemoteSession(sessionId);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java b/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
index 66892d2..f1ecab2 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/remote/MockRemoteService.java
@@ -34,4 +34,7 @@
         return null;
     }
 
+    @Override
+    public void close() throws RemoteException {
+    }
 }
diff --git a/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java b/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
index ef75d21..ddb00e5 100644
--- a/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
+++ b/tutorials/tutorial-rop-client/src/main/java/org/apache/cayenne/tutorial/persistent/client/Main.java
@@ -18,15 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.tutorial.persistent.client;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.rop.client.ClientRuntime;
 import org.apache.cayenne.query.ObjectSelect;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 public class Main {
 
     public static void main(String[] args) {
@@ -43,6 +43,7 @@
         newObjectsTutorial(context);
         selectTutorial(context);
         deleteTutorial(context);
+        runtime.shutdown();
     }
 
     static void newObjectsTutorial(ObjectContext context) {