/* * 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.client; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import mazz.i18n.Logger; import org.apache.commons.logging.LogFactory; import org.jboss.remoting.CannotConnectException; import org.jboss.remoting.invocation.NameBasedInvocation; import org.rhq.core.communications.command.annotation.Asynchronous; import org.rhq.core.communications.command.annotation.DisableSendThrottling; import org.rhq.core.communications.command.annotation.Timeout; import org.rhq.core.util.exception.ThrowableUtil; import org.rhq.enterprise.communications.command.CommandResponse; import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommand; import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommandResponse; import org.rhq.enterprise.communications.i18n.CommI18NFactory; import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys; /** * This class is a factory of proxies that can be used to invoke methods on a remote POJO. This class provides * configuration settings like {@link #isDeliveryGuaranteed() guaranteed delivery}, {@link #getTimeout() the timeout}, * {@link #isAsynch() async send mode} and {@link #isSendThrottled() send throttling}. These configuration settings * define how the proxies will behave. If a remote POJO interface is annotated to indicate those settings (e.g. with * {@link Timeout} or other remote POJO annotations), the annotations will take effect and override the settings in this * class unless this object was told to {@link #setIgnoreAnnotations(boolean) ignore those annotations}. * * @author John Mazzitelli */ public class ClientRemotePojoFactory { /** * Settings describing whether this factory generates proxies that have guaranteed delivery by default. This * is overridden by annotations on the service interface unless set to DISABLED, which forces a NO setting * for all calls. DISABLED is required if the underlying environment can not support guaranteed delivery. */ public enum GuaranteedDelivery { YES, NO, DISABLED } /** * Logger */ private static final Logger LOG = CommI18NFactory.getLogger(ClientRemotePojoFactory.class); /** * Used to send the invocation command. */ private final ClientCommandSender m_clientCommandSender; /** * If <code>true</code>, any annotations found in the remote POJO interface will be ignored; all settings will be * derived from the settings in this object (thus allowing you override settings hardcoded in a POJO interface's * annotations). */ private boolean m_ignoreAnnotations; /** * Will be <code>true</code> if remote invocations will be sent asynchronously. Will be <code>false</code> when the * remote POJO invocations should be made synchronously and thus act like a "normal" POJO method invocation. The * current value of this flag will be the one set on the proxy when {@link #getRemotePojo(Class)} is called. */ private boolean m_asyncModeEnabled; /** * This is only used when this object's asynchronous mode is enabled and will be the callback that will be notified * when an asynchronous call is completed. This may be <code>null</code> indicating that nothing needs to be * notified when the asynchronous call is finished. The current value will be the one set on the proxy when * {@link #getRemotePojo(Class)} is called. */ private CommandResponseCallback m_asyncCallback; /** * If not <code>null</code>, this is the request timeout that is to be used when sending remote POJO command * requests. It overrides the default timeout as configured in the client command sender. */ private Long m_timeoutMillis; /** * Flag to indicate if POJO calls are to be submitted with guaranteed delivery enabled. */ private GuaranteedDelivery m_deliveryGuaranteed; /** * Flag to indicate if POJO calls are to be send-throttleable, that is, they must pass the send-throttle before it * can be sent. */ private boolean m_sendThrottled; /** * Constructor for {@link ClientRemotePojoFactory}. * * @param ccs the object that will be used to send the remote POJO invocation command */ public ClientRemotePojoFactory(ClientCommandSender ccs) { m_clientCommandSender = ccs; m_ignoreAnnotations = false; m_asyncModeEnabled = false; m_asyncCallback = null; m_timeoutMillis = null; m_deliveryGuaranteed = GuaranteedDelivery.NO; m_sendThrottled = true; return; } /** * Called {@link #getRemotePojo(Class, Long)} with a null timeout. See javadoc of that method for more info. * * @param targetInterface * @return the proxy to the remote POJO */ public <T> T getRemotePojo(Class<T> targetInterface) { return getRemotePojo(targetInterface, null); } /** * This method returns a proxy to the remote POJO. The returned object can be used to make RPC calls to the remote * POJO as if the POJO was a local object. The returned proxy will have its async mode set * {@link #isAsynch() accordingly}. If asynchronous mode is to be enabled in this proxy, its callback will be set to * the object that was passed to {@link #setAsynch(boolean, CommandResponseCallback)} (which may or may not be a * <code>null</code> callback). * * @param targetInterface * @param timeoutOverride overrides the default timeout that this factory would have used * * @return the proxy to the remote POJO */ @SuppressWarnings({ "unchecked", "rawtypes" }) public <T> T getRemotePojo(Class<T> targetInterface, Long timeoutOverride) { // lets quickly get in and out of a synchronous block - store our current async mode and timeout in local variables boolean async_mode; CommandResponseCallback async_callback; Long timeout_millis; GuaranteedDelivery guaranteed_delivery; boolean send_throttled; boolean ignore_annotations; synchronized (this) { ignore_annotations = m_ignoreAnnotations; async_mode = m_asyncModeEnabled; async_callback = m_asyncCallback; timeout_millis = (timeoutOverride != null) ? timeoutOverride : m_timeoutMillis; guaranteed_delivery = m_deliveryGuaranteed; send_throttled = m_sendThrottled; } Class[] interfaces = new Class[] { targetInterface }; ClassLoader class_loader = targetInterface.getClassLoader(); RemotePojoProxyHandler proxy_handler = new RemotePojoProxyHandler(targetInterface.getName(), ignore_annotations, async_mode, async_callback, timeout_millis, guaranteed_delivery, send_throttled); T proxy = (T) Proxy.newProxyInstance(class_loader, interfaces, proxy_handler); return proxy; } /** * If <code>true</code>, any remote POJO proxy returned by {@link #getRemotePojo(Class)} will ignore all annotations * define in that remote POJO's interface which allows you to override settings hardcoded in the annotations with * settings defined in this class (e.g. {@link #isSendThrottled()} will override any {@link DisableSendThrottling} * annotation defined in the remote POJO interface). If <code>false</code>, annotations that exist in the remote * POJO interface will take effect - thus overriding any of the settings defined in this object. * * @return flag to indicate of remote POJO interface annotations are ignored or not */ public boolean isIgnoreAnnotations() { return m_ignoreAnnotations; } /** * Indicates if remote POJO interface annotations are to be ignored or not. Please see the description of * {@link #isIgnoreAnnotations()} for more information. * * @param ignore flag to indicate of remote POJO interface annotations are ignored or not */ public void setIgnoreAnnotations(boolean ignore) { m_ignoreAnnotations = ignore; } /** * Returns <code>true</code> if new remote POJO proxies should make their invocations asynchronously; <code> * false</code> means the invocations will be synchronous (and thus act like a "normal" method call). * * @return the asynchronous flag * * @see #setAsynch(boolean, CommandResponseCallback) */ public boolean isAsynch() { synchronized (this) { return m_asyncModeEnabled; } } /** * Tells this object to make any new remote POJO proxies (via {@link #getRemotePojo(Class)}) such that they send * their invocations asynchronously. If <code>is_async</code> is <code>true</code>, then the optional <code> * callback</code> will be used as the callback object that will be notified when the asynchronous remote invocation * has been completed. The callback will receive {@link RemotePojoInvocationCommandResponse} objects as the * response. If <code>is_async</code> is <code>false</code>, all proxy remote POJO invocations will be synchronous, * and will thus act just like a "normal" POJO method call. * * <p>Setting the async mode will <b>not</b> effect any currently existing remote POJO proxies. If you change the * async mode, you will need to create a new proxy via {@link #getRemotePojo(Class)} and use that proxy to pick up * the new async mode.</p> * * <p>You can call this method to set a <code>callback</code> with <code>is_async</code> set to <code>false</code>; * this allows you to set a callback for use with any POJOs that might be annotated as {@link Asynchronous}.</p> * * <p>It is recommended that, if you want a callback, you use {@link RemotePojoInvocationFuture} as the callback * implementation because that object is able to directly handle {@link RemotePojoInvocationCommandResponse} objects * and can perform blocked waits.</p> * * @param is_async indicates if new proxies to remote POJOs should be in asynchronous mode * @param callback the callback to be notified when the invocation is complete (may be <code>null</code>) * * @see RemotePojoInvocationFuture */ public void setAsynch(boolean is_async, CommandResponseCallback callback) { synchronized (this) { m_asyncModeEnabled = is_async; m_asyncCallback = callback; } return; } /** * Returns the timeout (in milliseconds) that any new {@link #getRemotePojo(Class) proxies} will use for all POJO * calls. If <code>null</code> is returned, the default will be used (the default is defined by the * {@link ClientCommandSender} that was passed into this object's constructor). The timeout indicates how much time * the client should wait for the command to return with a response. If the timeout is exceeded, the command will * abort. * * @return the timeout (in milliseconds) that each command will be configured with; if <code>null</code>, a default * will be used */ public Long getTimeout() { synchronized (this) { return m_timeoutMillis; } } /** * Sets the timeout (in milliseconds) that all POJO calls will be configured with. See {@link #getTimeout()} for * more information. * * <p>Note that setting a new timeout value will <b>not</b> affect any existing proxies that this object previously * created via prior calls to {@link #getRemotePojo(Class)}. You must get a new remote POJO proxy in order for this * new timeout to take effect.</p> * * @param timeoutMillis the timeout (in milliseconds) that each command will be configured with; * if<code>null</code>, a default will be used */ public void setTimeout(Long timeoutMillis) { synchronized (this) { m_timeoutMillis = timeoutMillis; } } /** * Returns the flag to indicate if the remote POJO calls should be made with guaranteed delivery. * * @return <code>true</code> if POJO calls should be made with guaranteed delivery; <code>false</code> otherwise */ public boolean isDeliveryGuaranteed() { synchronized (this) { return (GuaranteedDelivery.YES == m_deliveryGuaranteed); } } /** * Sets the flag to indicate if the remote POJO calls should be made with guaranteed delivery. * * <p>Note that setting a new guaranteed delivery value will <b>not</b> affect any existing proxies that this object * previously created via prior calls to {@link #getRemotePojo(Class)}. You must get a new remote POJO proxy in * order for this new flag to take effect.</p> * * @param guaranteed <code>true</code> if the remote POJO call should be made with guaranteed delivery */ public void setDeliveryGuaranteed(GuaranteedDelivery guaranteed) { synchronized (this) { m_deliveryGuaranteed = guaranteed; } } /** * Returns the flag to indicate if the remote POJO calls should be "send-throttleable", that is, will need to pass * the send-throttle before it will be sent. * * @return <code>true</code> if POJO calls are to be send-throttled */ public boolean isSendThrottled() { synchronized (this) { return m_sendThrottled; } } /** * Sets the flag to indicate if the remote POJO calls should be "send-throttleable". * * <p>Note that setting a new send-throttled flag will <b>not</b> affect any existing proxies that this object * previously created via prior calls to {@link #getRemotePojo(Class)}. You must get a new remote POJO proxy in * order for this new flag to take effect.</p> * * @param throttled <code>true</code> if the remote POJO call should be send-throttled */ public void setSendThrottled(boolean throttled) { synchronized (this) { m_sendThrottled = throttled; } } /** * The actual proxy object that submits the remote POJO invocation request. Each proxy will have its own * asynchronous mode enabled or disabled. */ private class RemotePojoProxyHandler implements InvocationHandler { private String m_targetInterfaceName; private boolean m_proxyHandlerIgnoreAnnotations; private boolean m_proxyHandlerAsyncModeEnabled; private CommandResponseCallback m_proxyHandlerAsyncCallback; private Long m_proxyHandlerTimeout; private GuaranteedDelivery m_proxyHandlerDeliveryGuaranteed; private boolean m_proxyHandlerSendThrottled; /** * Creates a new {@link RemotePojoProxyHandler} object with the async mode set appropriately. Note that the * <code>async_callback</code> will be ignored if <code>async_mode_enabled</code> is <code>false</code>. * * <p>If <code>timeout</code> is not <code>null</code>, it will be the timeout value that will override the * timeout value as configured in the client command sender. If it is <code>null</code>, the timeout used will * be the timeout that the client command sender defaults to.</p> * * @param target_interface_name the target interface that our proxy is to mimic * @param ignore_annotations if <code>true</code>, the remote interface's annotations will be ignored * @param async_mode_enabled if <code>true</code>, the method invocations to the remote POJO will be * asynchronous * @param async_callback if not <code>null</code>, this is the callback that will be notified when * asynchronous invocations are complete * @param timeout_millis if not <code>null</code>, will be the requests' timeout in milliseconds * @param delivery_guaranteed if <code>true</code>, the call should be made with guaranteed delivery * @param send_throttled if <code>true</code>, the call will only be made after it passes the send * throttle */ public RemotePojoProxyHandler(String target_interface_name, boolean ignore_annotations, boolean async_mode_enabled, CommandResponseCallback async_callback, Long timeout_millis, GuaranteedDelivery delivery_guaranteed, boolean send_throttled) { m_targetInterfaceName = target_interface_name; m_proxyHandlerIgnoreAnnotations = ignore_annotations; m_proxyHandlerAsyncModeEnabled = async_mode_enabled; m_proxyHandlerAsyncCallback = async_callback; m_proxyHandlerTimeout = timeout_millis; m_proxyHandlerDeliveryGuaranteed = delivery_guaranteed; m_proxyHandlerSendThrottled = send_throttled; } /** * This is called when a method on the remote POJO should be invoked. * * @see java.lang.reflect.InvocationHandler#invoke(Object, Method, Object[]) */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); String[] paramSig = createParamSignature(method.getParameterTypes()); NameBasedInvocation invocation = new NameBasedInvocation(methodName, args, paramSig); RemotePojoInvocationCommandResponse response = null; Throwable throwable = null; RemotePojoInvocationCommand cmd = new RemotePojoInvocationCommand(); cmd.setNameBasedInvocation(invocation); cmd.setTargetInterfaceName(m_targetInterfaceName); boolean async_mode_enabled = determineAsynchronous(method); Long timeout = determineTimeout(method); boolean guaranteed_delivery = determineGuaranteedDelivery(method); boolean send_throttle_enabled = determineSendThrottling(method); if (send_throttle_enabled) { cmd.getConfiguration().setProperty(ClientCommandSender.CMDCONFIG_PROP_SEND_THROTTLE, Boolean.toString(true)); } if (timeout != null) { cmd.getConfiguration().setProperty(ClientCommandSender.CMDCONFIG_PROP_TIMEOUT, timeout.toString()); } try { if (async_mode_enabled) { if (guaranteed_delivery) { m_clientCommandSender.sendAsynchGuaranteed(cmd, m_proxyHandlerAsyncCallback); } else { m_clientCommandSender.sendAsynch(cmd, m_proxyHandlerAsyncCallback); } } else { CommandResponse sendsync_response = m_clientCommandSender.sendSynch(cmd); response = new RemotePojoInvocationCommandResponse(sendsync_response); // throw the exception if one occurred if (response.getException() != null) { throw response.getException(); } } } catch (CannotConnectException cce) { // we can perform some retry or failover mechanism here - see JBoss/Remoting TransporterClient for an example throwable = cce; } catch (InvocationTargetException ite) { Throwable root = ite.getCause(); throwable = (root == null) ? ite : root; } catch (Throwable e) { throwable = e; } // if some failure occured, we need to do different things depending on the enablement of the async mode. // if async mode is enabled, let's create a command response to indicate the error and notify the callback // if async mode is disabled (i.e. we are in sync mode), then just throw the exception. // TODO [mazz]: do we want this behavior - the callback will get the failure response and that same response // will be returned not sure if we just want to throw the exception no matter what if (throwable != null) { LOG.debug(CommI18NResourceKeys.CLIENT_REMOTE_POJO_INVOKER_EXECUTION_FAILURE, methodName, ThrowableUtil .getAllMessages(throwable, true)); if (async_mode_enabled) { response = new RemotePojoInvocationCommandResponse(cmd, throwable); if (m_proxyHandlerAsyncCallback != null) { m_proxyHandlerAsyncCallback.commandSent(response); } } else { throw throwable; } } Object invoke_return = null; if (response != null) { invoke_return = response.getResults(); // if the returned object is a remote stream, we need to tell it how to request stream data // by giving it the client command sender that will be used to send those requests if (invoke_return instanceof RemoteInputStream) { ((RemoteInputStream) invoke_return).setClientCommandSender(m_clientCommandSender); } else if (invoke_return instanceof RemoteOutputStream) { ((RemoteOutputStream) invoke_return).setClientCommandSender(m_clientCommandSender); } } return invoke_return; } /** * Converts the Class array to a String array of consisting of the class names * * @param args class args * * @return the class array converted to strings */ private String[] createParamSignature(Class<?>[] args) { if ((args == null) || (args.length == 0)) { return new String[] {}; } String[] paramSig = new String[args.length]; for (int x = 0; x < args.length; x++) { paramSig[x] = args[x].getName(); } return paramSig; } /** * Determines if the method invocation should be performed asynchronously or not. * * @param method the method to be invoked * * @return <code>true</code> if method to be invoked asynchronously; <code>false</code> otherwise */ private boolean determineAsynchronous(Method method) { boolean ret_async_mode_enabled = m_proxyHandlerAsyncModeEnabled; if (!m_proxyHandlerIgnoreAnnotations) { Asynchronous annotation = method.getAnnotation(Asynchronous.class); if (annotation == null) { annotation = method.getDeclaringClass().getAnnotation(Asynchronous.class); } if (annotation != null) { ret_async_mode_enabled = annotation.value(); } } return ret_async_mode_enabled; } /** * Determines what the timeout value should be for the method invocation. * * @param method the method to be invoked * * @return the timeout value - will be <code>null</code> if no timeout is defined */ private Long determineTimeout(Method method) { Long ret_timeout = m_proxyHandlerTimeout; if (!m_proxyHandlerIgnoreAnnotations) { Timeout annotation = method.getAnnotation(Timeout.class); if (annotation == null) { annotation = method.getDeclaringClass().getAnnotation(Timeout.class); } if (annotation != null) { ret_timeout = Long.valueOf(annotation.value()); } } return ret_timeout; } /** * Determines if the method invocation should have invoked with guaranteed delivery enabled. * * @param method the method to be invoked * * @return <code>true</code> if the method invocation is guaranteed to be delivered; <code>false</code> * otherwise */ private boolean determineGuaranteedDelivery(Method method) { boolean ret_delivery_guaranteed = (GuaranteedDelivery.YES == m_proxyHandlerDeliveryGuaranteed) ? true : false; if (!m_proxyHandlerIgnoreAnnotations) { Asynchronous annotation = method.getAnnotation(Asynchronous.class); if (annotation == null) { annotation = method.getDeclaringClass().getAnnotation(Asynchronous.class); } if (annotation != null) { ret_delivery_guaranteed = annotation.guaranteedDelivery(); if (ret_delivery_guaranteed && (GuaranteedDelivery.DISABLED == m_proxyHandlerDeliveryGuaranteed)) { ret_delivery_guaranteed = false; LogFactory.getLog(ClientRemotePojoFactory.class).error( "Illegal use of Guaranteed Delivery, Remove 'guaranteedDelivery=true' from method: " + method.getName()); } } } return ret_delivery_guaranteed; } /** * Determines if the method invocation should have send throttling enabled or disabled. * * @param method the method to be invoked * * @return <code>true</code> if send throttling is to be enabled; <code>false</code> otherwise */ private boolean determineSendThrottling(Method method) { boolean ret_send_throttled = m_proxyHandlerSendThrottled; if (!m_proxyHandlerIgnoreAnnotations) { DisableSendThrottling annotation = method.getAnnotation(DisableSendThrottling.class); if (annotation == null) { annotation = method.getDeclaringClass().getAnnotation(DisableSendThrottling.class); } if (annotation != null) { ret_send_throttled = !annotation.value(); } } return ret_send_throttled; } } }