/*
* Copyright 2002-2014 the original author or authors.
*
* 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.springframework.erlang.connection;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.erlang.OtpIOException;
import org.springframework.util.Assert;
import com.ericsson.otp.erlang.OtpAuthException;
import com.ericsson.otp.erlang.OtpPeer;
import com.ericsson.otp.erlang.OtpSelf;
/**
* A {@link ConnectionFactory} implementation that returns the same Connections from all
* {@link #createConnection()} calls, and ignores calls to {@link Connection#close()}.
*
* Provides a more traditional API to creating a connection to a remote erlang
* node than the JInterface API.
*
* <p>
* The following is taken from the JInterface javadocs that describe the valid
* node names that can be used. These naming constraints apply to the string
* values you pass into the node names in SimpleConnectionFactory's constructor.
* <p>
* About nodenames: Erlang nodenames consist of two components, an alivename and
* a hostname separated by '@'. Additionally, there are two nodename formats:
* short and long. Short names are of the form "alive@hostname", while long
* names are of the form "alive@host.fully.qualified.domainname". Erlang has
* special requirements regarding the use of the short and long formats, in
* particular they cannot be mixed freely in a network of communicating nodes,
* however Jinterface makes no distinction. See the Erlang documentation for
* more information about nodenames.
* </p>
*
* <p>
* The constructors for the AbstractNode classes will create names exactly as
* you provide them as long as the name contains '@'. If the string you provide
* contains no '@', it will be treated as an alivename and the name of the local
* host will be appended, resulting in a shortname. Nodenames longer than 255
* characters will be truncated without warning.
* </p>
*
* <p>
* Upon initialization, this class attempts to read the file .erlang.cookie in
* the user's home directory, and uses the trimmed first line of the file as the
* default cookie by those constructors lacking a cookie argument. If for any
* reason the file cannot be found or read, the default cookie will be set to
* the empty string (""). The location of a user's home directory is determined
* using the system property "user.home", which may not be automatically set on
* all platforms.
* </p>
*
* @author Mark Pollack
*/
public class SingleConnectionFactory implements ConnectionFactory,
InitializingBean, DisposableBean {
protected final Log logger = LogFactory.getLog(getClass());
private boolean uniqueSelfNodeName = true;
private final String selfNodeName;
private String cookie;
private final String peerNodeName;
private OtpSelf otpSelf;
private OtpPeer otpPeer;
/** Raw JInterface Connection */
private Connection targetConnection;
/** Proxy Connection */
private Connection connection;
/** Synchronization monitor for the shared Connection */
private final Object connectionMonitor = new Object();
public SingleConnectionFactory(String selfNodeName, String cookie,
String peerNodeName) {
this.selfNodeName = selfNodeName;
this.cookie = cookie;
this.peerNodeName = peerNodeName;
}
public SingleConnectionFactory(String selfNodeName, String peerNodeName) {
this.selfNodeName = selfNodeName;
this.peerNodeName = peerNodeName;
}
public boolean isUniqueSelfNodeName() {
return uniqueSelfNodeName;
}
public void setUniqueSelfNodeName(boolean uniqueSelfNodeName) {
this.uniqueSelfNodeName = uniqueSelfNodeName;
}
@Override
public Connection createConnection() throws UnknownHostException,
OtpAuthException {
synchronized (this.connectionMonitor) {
if (this.connection == null) {
try {
initConnection();
} catch (IOException e) {
throw new OtpIOException("failed to connect from '"
+ this.selfNodeName + "' to peer node '"
+ this.peerNodeName + "'", e);
}
}
return this.connection;
}
}
public void initConnection() throws IOException, OtpAuthException {
synchronized (this.connectionMonitor) {
if (this.targetConnection != null) {
closeConnection(this.targetConnection);
}
this.targetConnection = doCreateConnection();
prepareConnection(this.targetConnection);
if (logger.isInfoEnabled()) {
logger.info("Established shared Rabbit Connection: "
+ this.targetConnection);
}
this.connection = getSharedConnectionProxy(this.targetConnection);
}
}
/**
* Close the underlying shared connection.
* The provider of this ConnectionFactory needs to care for proper shutdown.
* <p>As this bean implements DisposableBean, a bean factory will
* automatically invoke this on destruction of its cached singletons.
*/
@Override
public void destroy() {
resetConnection();
}
/**
* Reset the underlying shared Connection, to be reinitialized on next access.
*/
public void resetConnection() {
synchronized (this.connectionMonitor) {
if (this.targetConnection != null) {
closeConnection(this.targetConnection);
}
this.targetConnection = null;
this.connection = null;
}
}
/**
* Close the given Connection.
*
* @param connection
* the Connection to close
*/
protected void closeConnection(Connection connection) {
if (logger.isDebugEnabled()) {
logger.debug("Closing shared Rabbit Connection: "
+ this.targetConnection);
}
try {
// TODO there are other close overloads close(int closeCode,
// java.lang.String closeMessage, int timeout)
connection.close();
} catch (Throwable ex) {
logger.debug("Could not close shared Rabbit Connection", ex);
}
}
/**
* Create a JInterface Connection via this class's ConnectionFactory.
*
* @return the new Otp Connection
* @throws OtpAuthException Any.
* @throws IOException Any.
*/
protected Connection doCreateConnection() throws IOException,
OtpAuthException {
return new DefaultConnection(otpSelf.connect(otpPeer));
}
protected void prepareConnection(Connection con) throws IOException {
}
/**
* Wrap the given OtpConnection with a proxy that delegates every method
* call to it but suppresses close calls. This is useful for allowing
* application code to handle a special framework Connection just like an
* ordinary Connection from a Rabbit ConnectionFactory.
*
* @param target
* the original Connection to wrap
* @return the wrapped Connection
*/
protected Connection getSharedConnectionProxy(Connection target) {
List<Class<?>> classes = new ArrayList<Class<?>>(1);
classes.add(Connection.class);
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
classes.toArray(new Class<?>[classes.size()]),
new SharedConnectionInvocationHandler(target));
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
Assert.isTrue(this.selfNodeName != null || this.peerNodeName != null,
"'selfNodeName' or 'peerNodeName' is required");
String selfNodeNameToUse = this.selfNodeName;
if (isUniqueSelfNodeName()) {
selfNodeNameToUse = this.selfNodeName + "-" + UUID.randomUUID().toString();
logger.debug("Creating OtpSelf with node name = [" + selfNodeNameToUse + "]");
}
try {
if (this.cookie == null) {
this.otpSelf = new OtpSelf(selfNodeNameToUse.trim());
} else {
this.otpSelf = new OtpSelf(selfNodeNameToUse.trim(), this.cookie);
}
} catch (IOException e) {
throw new OtpIOException(e);
}
this.otpPeer = new OtpPeer(this.peerNodeName.trim());
}
private class SharedConnectionInvocationHandler implements
InvocationHandler {
private final Connection target;
public SharedConnectionInvocationHandler(Connection target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
} else if (method.getName().equals("hashCode")) {
// Use hashCode of Connection proxy.
return System.identityHashCode(proxy);
} else if (method.getName().equals("toString")) {
return "Shared Otp Connection: " + this.target;
} else if (method.getName().equals("close")) {
// Handle close method: don't pass the call on.
return null;
}
try {
return method.invoke(this.target, args);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
}