/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.communications.command.impl.remotepojo.server;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Map;
import mazz.i18n.Logger;
import org.jboss.remoting.invocation.NameBasedInvocation;
import org.rhq.core.communications.command.annotation.LimitedConcurrency;
import org.rhq.core.util.exception.WrappedRemotingException;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.communications.command.Command;
import org.rhq.enterprise.communications.command.CommandExecutor;
import org.rhq.enterprise.communications.command.CommandResponse;
import org.rhq.enterprise.communications.command.CommandType;
import org.rhq.enterprise.communications.command.client.RemoteInputStream;
import org.rhq.enterprise.communications.command.client.RemoteOutputStream;
import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommand;
import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommandResponse;
import org.rhq.enterprise.communications.command.server.CommandMBean;
import org.rhq.enterprise.communications.command.server.CommandService;
import org.rhq.enterprise.communications.command.server.CommandServiceMBean;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
import org.rhq.enterprise.communications.util.ClassUtil;
import org.rhq.enterprise.communications.util.ConcurrencyManager;
import org.rhq.enterprise.communications.util.ConcurrencyManager.Permit;
import org.rhq.enterprise.communications.util.NotPermittedException;
/**
* Processes a client request to invoke a remoted POJO.
*
* @author John Mazzitelli
*/
public class RemotePojoInvocationCommandService extends CommandService implements
RemotePojoInvocationCommandServiceMBean {
private static final Logger LOG = CommI18NFactory.getLogger(RemotePojoInvocationCommandService.class);
/**
* The set of remoted POJOs keyed on their classnames (as Strings).
*/
private Map<String, Object> m_remotedPojos;
/**
* @see CommandMBean#startService()
*/
@Override
public void startService() {
super.startService();
m_remotedPojos = new Hashtable<String, Object>();
}
/**
* @see CommandMBean#stopService()
*/
@Override
public void stopService() {
super.stopService();
m_remotedPojos.clear();
}
/**
* @see RemotePojoInvocationCommandServiceMBean#addPojo(Object, String)
*/
public void addPojo(Object pojo, String interfaceName) {
try {
Class<?> interfc = Class.forName(interfaceName, true, pojo.getClass().getClassLoader());
if (!interfc.isAssignableFrom(pojo.getClass())) {
throw new IllegalArgumentException(LOG.getMsgString(CommI18NResourceKeys.INVALID_POJO_INTERFACE, pojo,
interfaceName));
}
} catch (ClassNotFoundException cnfe) {
throw new IllegalArgumentException(LOG.getMsgString(CommI18NResourceKeys.INVALID_INTERFACE, interfaceName,
cnfe));
}
m_remotedPojos.put(interfaceName, pojo);
return;
}
/**
* @see RemotePojoInvocationCommandServiceMBean#addPojo(Object, Class)
*/
public <T> void addPojo(T pojo, Class<T> remoteInterface) {
m_remotedPojos.put(remoteInterface.getName(), pojo);
}
/**
* @see RemotePojoInvocationCommandServiceMBean#removePojo(String)
*/
public void removePojo(String remoteInterfaceName) {
m_remotedPojos.remove(remoteInterfaceName);
}
/**
* @see RemotePojoInvocationCommandServiceMBean#removePojo(Class)
*/
public void removePojo(Class<?> remoteInterface) {
m_remotedPojos.remove(remoteInterface.getName());
}
/**
* Takes the remote POJO invocation request, which has the NameBasedInvocation parameter, and convert that to a
* method call on the target POJO (using reflection). Then return the Object returned from the method call on the
* target POJO in the response.
*
* @see CommandExecutor#execute(Command, java.io.InputStream, java.io.OutputStream)
*/
public CommandResponse execute(Command command, InputStream in, OutputStream out) {
RemotePojoInvocationCommand remote_pojo_command = new RemotePojoInvocationCommand(command);
NameBasedInvocation invocation = remote_pojo_command.getNameBasedInvocation();
String target_interface_name = remote_pojo_command.getTargetInterfaceName();
String method_name = invocation.getMethodName();
Object[] params = invocation.getParameters();
String[] signature = invocation.getSignature();
Class<?>[] class_signature = new Class[signature.length];
Permit permit = null;
Method pojo_method = null;
RemotePojoInvocationCommandResponse response;
ConcurrencyManager concurrency_manager = getServiceContainer().getConcurrencyManager();
try {
// look up the remote POJO that has the target interface
Object pojo = m_remotedPojos.get(target_interface_name);
if (pojo == null) {
throw new NoSuchMethodException(LOG.getMsgString(CommI18NResourceKeys.NO_POJO_SERVICE, command));
}
for (int x = 0; x < signature.length; x++) {
class_signature[x] = ClassUtil.getClassFromTypeName(signature[x]);
}
// If the remote POJO interface method has limited concurrency allowed, we need to make
// sure we have permission to invoke that method. None of these calls should throw an exception.
Class<?> target_interface = Class.forName(target_interface_name);
Method target_method = target_interface.getMethod(method_name, class_signature);
LimitedConcurrency limited_concurrency = target_method.getAnnotation(LimitedConcurrency.class);
if ((limited_concurrency != null) && (concurrency_manager != null)) {
permit = concurrency_manager.getPermit(limited_concurrency.value());
}
// if a parameter is a remote stream, we have to create a sender for it to use
// this is needed in case the remote server that is serving the stream data requires SSL - in that
// case, our sender needs to have SSL configured properly
for (int x = 0; x < signature.length; x++) // yes use signature, not param - avoids possible NPE
{
if (params[x] instanceof RemoteInputStream) {
prepareRemoteInputStream((RemoteInputStream) params[x]);
} else if (params[x] instanceof RemoteOutputStream) {
prepareRemoteOutputStream((RemoteOutputStream) params[x]);
}
}
// use reflection to make the call
pojo_method = pojo.getClass().getMethod(method_name, class_signature);
Object response_object = pojo_method.invoke(pojo, params);
response = new RemotePojoInvocationCommandResponse(remote_pojo_command, response_object);
} catch (InvocationTargetException e) {
// we want to make sure we keep the exception as intact as possible
// so we still want to put the invocation target exception in the response,
// but that is a java.* exception, so we need to drill down into the cause and wrap that if need be
Throwable response_exception;
if (e.getCause() != null) {
response_exception = new InvocationTargetException(getWrappedException(e.getCause(), pojo_method), e
.getMessage());
} else {
response_exception = getWrappedException(e, pojo_method);
}
response = new RemotePojoInvocationCommandResponse(remote_pojo_command, response_exception);
} catch (NotPermittedException npe) {
LOG.debug(CommI18NResourceKeys.COMMAND_NOT_PERMITTED, target_interface_name + '.' + method_name, npe
.getSleepBeforeRetry());
response = new RemotePojoInvocationCommandResponse(remote_pojo_command, npe);
} catch (Exception e) {
LOG.warn(e, CommI18NResourceKeys.REMOTE_POJO_EXECUTE_FAILURE);
response = new RemotePojoInvocationCommandResponse(remote_pojo_command, getWrappedException(e, pojo_method));
} finally {
if (concurrency_manager != null) {
concurrency_manager.releasePermit(permit);
}
}
return response;
}
/**
* Supports {@link RemotePojoInvocationCommand#COMMAND_TYPE}.
*
* @see CommandServiceMBean#getSupportedCommandTypes()
*/
public CommandType[] getSupportedCommandTypes() {
return new CommandType[] { RemotePojoInvocationCommand.COMMAND_TYPE };
}
/**
* Examines the given exception and if it matches a type that can be thrown from the given method, it is returned
* as-is; otherwise, it is wrapped in an exception that is suitable for sending to a remote client. This method is
* used because we want to ensure that the given exception can be processed on the other side. If we can be sure
* that a remote client has the exception class definitions available to it, then we don't have to wrap the
* exceptions (and we can be sure of this only if the exception is in the throws clause since the client has to have
* the method definition for it to be able to remotely call the method in the first place). If the given exception
* is not of a type explicitly declared in the method's throws clause, the exception is wrapped in a serializable,
* "stringified" form. When wrapped, the client cannot catch the given exception's type explicitly - but since the
* exception isn't declared in the method's throws clause, the client should have no reason to explicitly catch that
* specific type (the client should catch java.lang.Exception if it wants to catch this wrapped exception).
*
* <p>If <code>e</code> is an exception in a package under "java.", the exception is assumed to be available to all
* clients and thus not wrapped and returned as-is.</p>
*
* <p>If the exception is not one of the <code>java.</code> exceptions, and if <code>method</code> is <code>
* null</code>, the exception will be wrapped.</p>
*
* <p>If the given exception is not serializable, it will be wrapped no matter what.</p>
*
* @param e the exception to check to see if it matches one of the types thrown by the given method
* @param method the method whose throws clause is to be searched to see if any matches the type of <code>e</code>.
*
* @return the exception, possibly wrapped in an exception suitable for sending over the wire to a client if need be
*/
private Throwable getWrappedException(Throwable e, Method method) {
// if its already wrapped, don't bother wrapping it again
if (e instanceof WrappedRemotingException) {
return e;
}
// we assume everyone has java.* exception definitions available
// see if the exception and all its causes are all java.* exceptions
Throwable check_for_java = e;
boolean all_java_exceptions = true; // if false, e or one of its causes is not a java.* exception
while (check_for_java != null) {
if (!check_for_java.getClass().getName().startsWith("java.")) {
all_java_exceptions = false;
break; // don't bother continuing, we found a non-java.* exception
}
if (check_for_java.getCause() == check_for_java) {
check_for_java = null; // reached the end of the causes chain
} else {
check_for_java = check_for_java.getCause();
}
}
// if the exception and all its causes are java.*, then just return e as-is, unless its not serializable
if (all_java_exceptions) {
try {
// make sure its serializable
StreamUtil.serialize(e);
return e;
} catch (Exception ignore) {
// not serializable for some reason (a cause within the exception was an inner class within a non-serialized class?)
return new WrappedRemotingException(e);
}
}
// if the exception occurred outside of invoking any method - wrap the exception
if (method == null) {
return new WrappedRemotingException(e);
}
Class<?>[] declared_exceptions = method.getExceptionTypes();
// if the method did not declare any exceptions, wrap the exception
if ((declared_exceptions == null) || (declared_exceptions.length == 0)) {
return new WrappedRemotingException(e);
}
// check all declared exceptions and do not wrap the given exception if its one of those types
for (Class<?> declared_exception : declared_exceptions) {
// don't use isAssignableFrom since we a declared "java.lang.Exception" would match everything.
if (declared_exception.getName().equals(e.getClass().getName())) {
try {
// one final check - make sure its serializable
StreamUtil.serialize(e);
return e;
} catch (Exception ignore) {
// not serializable for some reason (exception was an inner class within a non-serialized class?)
return new WrappedRemotingException(e);
}
}
}
// didn't match any of the method's declared exceptions - wrap it
return new WrappedRemotingException(e);
}
}