/*
* Copyright 2009 DuraSpace.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mulgara.connection;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.mulgara.query.QueryException;
import org.mulgara.server.Session;
import org.mulgara.util.Rmi;
/**
* Creates new connections or reloads from a cache when possible connections.
* This class is designed to be thread-safe, so that connections obtained from a factory
* simultaneously from different threads will be backed by different Sessions and will not
* interfere with each other. When a connection is closed, it will release its underlying
* Session back to factory to be added to a cache for re-use by other clients.
* This factory must NOT be shared between users, as it is designed to cache security credentials!
*
* @created 2007-08-21
* @author Paula Gearon
* @copyright © 2007 <a href="mailto:pgearon@users.sourceforge.net">Paula Gearon</a>
* @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a>
*/
public class ConnectionFactory {
/** The logger. */
private final static Logger logger = Logger.getLogger(ConnectionFactory.class.getName());
/** String constant for localhost */
private final static String LOCALHOST_NAME = "localhost";
/** IP constant for localhost, saved as a string */
private final static String LOCALHOST_IP = "127.0.0.1";
/** Canonical hostname, used to normalize RMI connections on localhost */
private static String LOCALHOST_CANONICAL;
/** The scheme name for the local protocol */
private final static String LOCAL_PROTOCOL = "local";
/** The scheme name for the RMI protocol */
private final static String RMI_PROTOCOL = "rmi";
/** The list of known protocols. */
private final static String[] PROTOCOLS = { RMI_PROTOCOL, "beep", LOCAL_PROTOCOL };
/** The list of known local host aliases. */
private final static List<String> LOCALHOSTS = new LinkedList<String>();
/** Initialize the list of local host aliases. */
static {
LOCALHOSTS.add(LOCALHOST_NAME);
LOCALHOSTS.add(LOCALHOST_IP);
try {
LOCALHOSTS.add(InetAddress.getLocalHost().getHostAddress());
LOCALHOSTS.add(InetAddress.getLocalHost().getHostName());
String name = InetAddress.getLocalHost().getCanonicalHostName();
LOCALHOSTS.add(name);
LOCALHOST_CANONICAL = name;
} catch (UnknownHostException e) {
LOCALHOST_CANONICAL = LOCALHOST_NAME;
logger.error("Unable to get local host address", e);
}
}
/** Cache to hold Sessions that have been released by closed connections. */
private Map<URI,Set<Session>> cacheOnUri;
/**
* Maintain references to all active sessions to prevent them from being
* garbage-collected. This is necessary because we attempt to reclaim sessions from
* connections that have been abandoned but not closed.
*/
private Set<Session> sessionsInUse;
/** Flag to determine whether to use interruptible RMI operations for remote session connections. */
private final boolean useInterruptibleRmi;
/**
* Default constructor. Uses the configured system default behavior for interruptible RMI.
*/
public ConnectionFactory() {
this(Rmi.getDefaultInterrupt());
}
/**
* Construct a connection factory, with optional support for interruptible RMI operations
* on remote session connections.
* @param useInterruptibleRmi If <tt>true</tt>, then the connections created by this
* factory will support interrupting RMI operations. This behavior must also be enabled
* on the server in order to take advantage of this feature.
*/
public ConnectionFactory(boolean useInterruptibleRmi) {
this.useInterruptibleRmi = useInterruptibleRmi;
cacheOnUri = new HashMap<URI,Set<Session>>();
sessionsInUse = new HashSet<Session>();
}
/**
* Retrieve a connection based on a server URI. If there is already a cached Session
* for the server URI, it will be used; otherwise a new Session will be created when
* the SessionConnection is instantiated.
* @param serverUri The URI to get the connection to.
* @return The new Connection.
* @throws ConnectionException There was an error getting a connection.
*/
public Connection newConnection(URI serverUri) throws ConnectionException {
SessionConnection c = null;
Session session = null;
// Try to map all addresses for localhost to the same server URI so they can share Sessions
serverUri = normalizeLocalUri(serverUri);
synchronized(cacheOnUri) {
session = getFromCache(serverUri);
}
// Let the existing re-try mecanism attempt to re-establish connectivity if necessary.
if (session != null && !isSessionValid(session)) {
session = null;
}
if (session == null) {
boolean isRemote = !isLocalServer(serverUri);
c = new SessionConnection(serverUri, isRemote, useInterruptibleRmi);
} else {
c = new SessionConnection(session, null, serverUri, useInterruptibleRmi);
}
c.setFactory(this);
// Maintain a reference to prevent garbage collection of the Session.
synchronized(cacheOnUri) {
sessionsInUse.add(c.getSession());
}
return c;
}
/**
* Retrieve a connection for a given session. This method bypasses the cache altogether
* and it is the responsibility of the client to manage the lifecycle of Connections and
* Sessions used with this method.
* @param session The Session the Connection will use..
* @return The new Connection.
* @throws ConnectionException There was an error getting a connection.
*/
public Connection newConnection(Session session) throws ConnectionException {
return new SessionConnection(session, null, null, useInterruptibleRmi);
}
/**
* Close all Sessions cached by this factory. Sessions belonging to connections which are
* still in use will not be affected. Exceptions are logged, but not acted on.
*/
public void closeAll() {
Set<Session> sessionsToClose = null;
synchronized(cacheOnUri) {
sessionsToClose = clearCache();
sessionsToClose.addAll(sessionsInUse);
sessionsInUse.clear();
}
safeCloseAll(sessionsToClose);
}
/**
* Closes all sessions in a collection. Exceptions are logged, but not acted on.
* @param sessions The sessions to close.
*/
private void safeCloseAll(Iterable<Session> sessions) {
for (Session s: sessions) {
try {
s.close();
} catch (QueryException qe) {
logger.warn("Unable to close session", qe);
}
}
}
/**
* Returns a session to the cache to be re-used by new connections. Removes it from the
* list of active sessions.
* @param serverUri The URI of the
*/
void releaseSession(URI serverUri, Session session) {
synchronized(cacheOnUri) {
addToCache(serverUri, session);
// The session is now referenced by the cache, no need to hold on to a second reference
sessionsInUse.remove(session);
}
}
/**
* Remove the session from the list of active sessions so it may be garbage-collected.
*/
void disposeSession(Session session) {
synchronized(cacheOnUri) {
// The session was closed by the SessionConnection, no need to hold on to it any more.
sessionsInUse.remove(session);
}
}
/**
* If the given server URI uses the RMI scheme and the host is an alias for localhost,
* then attempt to construct a canonical server URI. The purpose of this method is to
* allow multiple aliased URI's to the same server to share the same cached Sessions.
* @param serverUri A server URI
* @return The normalized server URI.
*/
public static URI normalizeLocalUri(URI serverUri)
{
if (serverUri == null) {
return null;
}
URI normalized = serverUri;
if (RMI_PROTOCOL.equals(serverUri.getScheme())) {
String host = serverUri.getHost();
boolean isLocal = false;
for (String h : LOCALHOSTS) {
if (h.equalsIgnoreCase(host)) {
isLocal = true;
break;
}
}
if (isLocal) {
try {
normalized = new URI(RMI_PROTOCOL, null, LOCALHOST_CANONICAL, serverUri.getPort(),
serverUri.getPath(), serverUri.getQuery(), serverUri.getFragment());
} catch (URISyntaxException use) {
logger.info("Error normalizing server URI to local host", use);
}
}
}
return normalized;
}
/**
* Test if a given URI is a local URI.
* @param serverUri The URI to test.
* @return <code>true</code> if the URI is local.
*/
static boolean isLocalServer(URI serverUri) {
if (serverUri == null) return false;
String scheme = serverUri.getScheme();
if (LOCAL_PROTOCOL.equals(scheme)) return true;
// check for known protocols
boolean found = false;
for (String protocol: PROTOCOLS) {
if (protocol.equals(serverUri.getScheme())) {
found = true;
break;
}
}
if (found == false) return false;
// protocol found. Now test if the host appears in the localhost list
String host = serverUri.getHost();
for (String h: LOCALHOSTS) if (h.equalsIgnoreCase(host)) return true;
// no matching hostnames
return false;
}
/**
* Tests whether the given cached Session is still valid. This method uses the
* {@link Session#ping()} method to check connectivity with the remote server, and relies
* on the retry mechanism build into the remote session proxy to re-establish connectivity
* if it is lost.
* @param session A session.
* @return <code>true</code> if connectivity on the session was established.
*/
static boolean isSessionValid(Session session) {
boolean valid;
try {
valid = session.ping();
}
catch (QueryException qe) {
logger.info("Error establishing connection with remote session", qe);
valid = false;
}
return valid;
}
/**
* Retrieves a cached session for the given server URI. If multiple sessions were
* cached for this URI, the first one found is returned in no particular order. The
* calling code is responsible for synchronizing access to this method. If a session is
* found, then it is removed from the cache and returned.
* @param serverURI A server URI
* @return A cached session for the server URI, or <code>null</code> if none was found.
*/
private Session getFromCache(URI serverURI) {
Session session = null;
Set<Session> sessions = cacheOnUri.get(serverURI);
if (sessions != null) {
Iterator<Session> iter = sessions.iterator();
if (iter.hasNext()) {
session = iter.next();
}
sessions.remove(session);
}
return session;
}
/**
* Adds a session to the cache for the given URI. The calling code is responsible for
* synchronizing access to this method.
* @param serverURI A server URI.
* @param session The session to cache for the server URI.
*/
private void addToCache(URI serverURI, Session session) {
Set<Session> sessions = cacheOnUri.get(serverURI);
if (sessions == null) {
sessions = new HashSet<Session>();
cacheOnUri.put(serverURI, sessions);
}
sessions.add(session);
}
/**
* Clears all the contents of the cache, and returns a collection of all the Sessions that
* were in the cache. The calling code is responsible for synchronizing access to this method.
* @return The cached Sessions.
*/
private Set<Session> clearCache() {
Set<Session> sessions = new HashSet<Session>();
for (Map.Entry<URI,Set<Session>> entry : cacheOnUri.entrySet()) {
Set<Session> set = entry.getValue();
sessions.addAll(set);
set.clear();
}
cacheOnUri.clear();
return sessions;
}
}