/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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.jboss.ejb.client;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.SocketAddress;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.UnaryOperator;
import javax.ejb.CreateException;
import javax.net.ssl.SSLContext;
import javax.transaction.UserTransaction;
import org.jboss.ejb._private.Logs;
import org.wildfly.common.Assert;
import org.wildfly.discovery.FilterSpec;
import org.wildfly.discovery.ServicesQueue;
import org.wildfly.security.auth.client.AuthenticationConfiguration;
import org.wildfly.transaction.client.RemoteTransactionContext;
/**
* The main EJB client API class. This class contains helper methods which may be used to create proxies, open sessions,
* and associate the current invocation context.
*
* @author jpai
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
public final class EJBClient {
private static final Logs log = Logs.MAIN;
static {
log.greeting(Version.getVersionString());
}
private EJBClient() {
}
private static final ThreadLocal<Future<?>> FUTURE_RESULT = new ThreadLocal<Future<?>>();
/**
* An invocation context key which is set to the source socket address of the invocation request, if any. The
* value will be of type {@link SocketAddress}.
*/
public static final String SOURCE_ADDRESS_KEY = "jboss.source-address";
/**
* Get an asynchronous view of a proxy. Any {@code void} method on the proxy will be invoked fully asynchronously
* without a server round-trip delay. Any method which returns a {@link java.util.concurrent.Future Future} will
* continue to be asynchronous. Any other method invoked on the returned proxy will return {@code null} (the future
* result can be acquired by wrapping the remote call with {@link #getFutureResult(Object)} or by using {@link #getFutureResult()}).
* If an asynchronous view is passed in, the same view is returned.
*
* @param proxy the proxy interface instance
* @param <T> the proxy type
* @return the asynchronous view
* @throws IllegalArgumentException if the given object is not a valid proxy
*/
@SuppressWarnings("unchecked")
public static <T> T asynchronous(final T proxy) throws IllegalArgumentException {
final InvocationHandler invocationHandler = Proxy.getInvocationHandler(proxy);
if (invocationHandler instanceof EJBInvocationHandler) {
final EJBInvocationHandler<?> remoteInvocationHandler = (EJBInvocationHandler<?>) invocationHandler;
// determine proxy "type", return existing instance if it's already async
if (remoteInvocationHandler.isAsyncHandler()) {
return proxy;
} else {
return (T) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), proxy.getClass().getInterfaces(), remoteInvocationHandler.getAsyncHandler());
}
} else {
throw log.unknownProxy(proxy);
}
}
/**
* Get the future result of an operation. Should be called in conjunction with {@link #asynchronous(Object)}.
*
* @param operation the operation
* @param <T> the result type
* @return the future result
* @throws IllegalStateException if the operation is not appropriately given
*/
@SuppressWarnings("unchecked")
public static <T> Future<T> getFutureResult(final T operation) throws IllegalStateException {
if (operation != null) {
return new FinishedFuture<T>(operation);
}
final ThreadLocal<Future<?>> futureResult = FUTURE_RESULT;
try {
final Future<?> future = futureResult.get();
if (future == null) throw log.noAsyncInProgress();
return (Future<T>) future;
} finally {
futureResult.remove();
}
}
/**
* Get the future result of an operation. Should be called in conjunction with {@link #asynchronous(Object)}.
*
* @return the future result
* @throws IllegalStateException if the operation is not appropriately given
*/
public static Future<?> getFutureResult() throws IllegalStateException {
final ThreadLocal<Future<?>> futureResult = FUTURE_RESULT;
try {
final Future<?> future = futureResult.get();
if (future == null) throw log.noAsyncInProgress();
return future;
} finally {
futureResult.remove();
}
}
static void setFutureResult(final Future<?> future) {
FUTURE_RESULT.set(future);
}
/**
* Create a new proxy for the remote object identified by the given locator.
*
* @param locator the locator
* @param <T> the proxy type
* @return the new proxy
* @throws IllegalArgumentException if the locator parameter is {@code null} or is invalid
*/
public static <T> T createProxy(final EJBLocator<T> locator) throws IllegalArgumentException {
return createProxy(locator, null, null);
}
static <T> T createProxy(final EJBLocator<T> locator, final AuthenticationConfiguration authenticationConfiguration, final SSLContext sslContext) {
Assert.checkNotNullParam("locator", locator);
return locator.createProxyInstance(new EJBInvocationHandler<T>(locator, authenticationConfiguration, sslContext));
}
/**
* Determine whether an object is indeed a valid EJB proxy object created by this API.
*
* @param object the object to test
* @return {@code true} if it is an EJB proxy, {@code false} otherwise
*/
public static boolean isEJBProxy(final Object object) {
return object != null && Proxy.isProxyClass(object.getClass()) && Proxy.getInvocationHandler(object) instanceof EJBInvocationHandler;
}
/**
* Create a new EJB session.
*
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param distinctName the module distinct name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final Class<T> viewType, final String appName, final String moduleName, final String beanName, final String distinctName) throws Exception {
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, distinctName, Affinity.NONE));
}
/**
* Create a new EJB session.
*
* @param affinity the affinity specification for the session
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param distinctName the module distinct name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final Affinity affinity, final Class<T> viewType, final String appName, final String moduleName, final String beanName, final String distinctName) throws Exception {
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, distinctName, affinity == null ? Affinity.NONE : affinity));
}
/**
* Create a new EJB session.
*
* @param uri a URI at which EJBs may be obtained
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param distinctName the module distinct name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final URI uri, final Class<T> viewType, final String appName, final String moduleName, final String beanName, final String distinctName) throws Exception {
final Affinity affinity = uri == null ? Affinity.NONE : Affinity.forUri(uri);
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, distinctName, affinity));
}
/**
* Create a new EJB session.
*
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final Class<T> viewType, final String appName, final String moduleName, final String beanName) throws Exception {
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, Affinity.NONE));
}
/**
* Create a new EJB session.
*
* @param affinity the affinity specification for the session
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final Affinity affinity, final Class<T> viewType, final String appName, final String moduleName, final String beanName) throws Exception {
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, affinity == null ? Affinity.NONE : affinity));
}
/**
* Create a new EJB session.
*
* @param uri a URI at which EJBs may be obtained
* @param viewType the view type class
* @param appName the application name
* @param moduleName the module name
* @param beanName the EJB name
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(final URI uri, final Class<T> viewType, final String appName, final String moduleName, final String beanName) throws Exception {
final Affinity affinity = uri == null ? Affinity.NONE : Affinity.forUri(uri);
return createSession(new StatelessEJBLocator<T>(viewType, appName, moduleName, beanName, affinity));
}
/**
* Create a new EJB session.
*
* @param statelessLocator the stateless locator identifying the stateful EJB
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
public static <T> StatefulEJBLocator<T> createSession(StatelessEJBLocator<T> statelessLocator) throws Exception {
return createSession(statelessLocator, null, null);
}
/**
* Create a new EJB session.
*
* @param statelessLocator the stateless locator identifying the stateful EJB
* @param authenticationConfiguration the authentication configuration to use for the request
* @param sslContext the SSL context to use for the request
* @param <T> the view type
* @return the new EJB locator
* @throws CreateException if an error occurs
*/
static <T> StatefulEJBLocator<T> createSession(StatelessEJBLocator<T> statelessLocator, AuthenticationConfiguration authenticationConfiguration, SSLContext sslContext) throws Exception {
final EJBClientContext clientContext = EJBClientContext.getCurrent();
return clientContext.createSession(statelessLocator, authenticationConfiguration, sslContext);
}
/**
* Perform a one-way asynchronous invocation by method locator on a proxy. Any return value is ignored.
*
* @param proxy the EJB proxy
* @param methodLocator the method locator
* @param args the invocation arguments
* @param <T> the view type
* @throws Exception if the invocation failed for some reason
*/
public static <T> void invokeOneWay(T proxy, EJBMethodLocator methodLocator, Object... args) throws Exception {
final EJBInvocationHandler<? extends T> invocationHandler = EJBInvocationHandler.forProxy(proxy);
final EJBProxyInformation.ProxyMethodInfo proxyMethodInfo = invocationHandler.getProxyMethodInfo(methodLocator);
invocationHandler.invoke(proxy, proxyMethodInfo, args);
}
/**
* Perform an asynchronous invocation by method locator on a proxy, returning the future result.
*
* @param proxy the EJB proxy
* @param methodLocator the method locator
* @param args the invocation arguments
* @param <T> the view type
* @throws Exception if the invocation failed for some reason
*/
public static <T> Future<?> invokeAsync(T proxy, EJBMethodLocator methodLocator, Object... args) throws Exception {
final EJBInvocationHandler<? extends T> invocationHandler = EJBInvocationHandler.forProxy(proxy);
final EJBProxyInformation.ProxyMethodInfo proxyMethodInfo = invocationHandler.getProxyMethodInfo(methodLocator);
return (Future<?>) invocationHandler.invoke(proxy, proxyMethodInfo, args);
}
/**
* Perform an invocation by method locator on a proxy, returning the result.
*
* @param proxy the EJB proxy
* @param methodLocator the method locator
* @param args the invocation arguments
* @param <T> the view type
* @throws Exception if the invocation failed for some reason
*/
public static <T> Object invoke(T proxy, EJBMethodLocator methodLocator, Object... args) throws Exception {
final EJBInvocationHandler<? extends T> invocationHandler = EJBInvocationHandler.forProxy(proxy);
final EJBProxyInformation.ProxyMethodInfo proxyMethodInfo = invocationHandler.getProxyMethodInfo(methodLocator);
return invocationHandler.invoke(proxy, proxyMethodInfo, args);
}
/**
* Get the locator for a proxy, if it has one.
*
* @param proxy the proxy (may not be {@code null})
* @param <T> the proxy type
* @return the locator
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
*/
public static <T> EJBLocator<? extends T> getLocatorFor(T proxy) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
return EJBInvocationHandler.forProxy(proxy).getLocator();
}
/**
* Set a per-proxy invocation timeout. This overrides the globally configured timeout.
*
* @param proxy the proxy to change (must not be {@code null}, must be a valid EJB proxy)
* @param timeout the amount of time (must be greater than zero)
* @param timeUnit the time unit (must not be {@code null})
* @throws IllegalArgumentException if the timeout is less than or equal to zero or a required parameter is
* {@code null} or invalid
*/
public static void setInvocationTimeout(Object proxy, long timeout, TimeUnit timeUnit) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkMinimumParameter("timeout", 1L, timeout);
Assert.checkNotNullParam("timeUnit", timeUnit);
EJBInvocationHandler.forProxy(proxy).setInvocationTimeout(Math.max(1L, timeUnit.toMillis(timeout)));
}
/**
* Clear the per-proxy invocation timeout, causing it to use the globally configured timeout.
*
* @param proxy the proxy to change (must not be {@code null}, must be a valid EJB proxy)
* @throws IllegalArgumentException if the proxy is {@code null} or is not valid
*/
public static void clearInvocationTimeout(Object proxy) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
EJBInvocationHandler.forProxy(proxy).setInvocationTimeout(-1L);
}
/**
* Change the strong affinity of a proxy. All subsequent invocations against the proxy will use the new affinity.
* Subsequent calls to {@link #getLocatorFor(Object)} for the given proxy will return the updated locator.
*
* @param proxy the proxy (may not be {@code null})
* @param newAffinity the new affinity (may not be {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
* @throws SecurityException if a security manager is present and the caller does not have the {@code changeStrongAffinity} {@link EJBClientPermission}
*/
public static void setStrongAffinity(Object proxy, Affinity newAffinity) throws IllegalArgumentException, SecurityException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkNotNullParam("newAffinity", newAffinity);
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new EJBClientPermission(EJBClientPermission.Name.changeStrongAffinity));
}
EJBInvocationHandler.forProxy(proxy).setStrongAffinity(newAffinity);
}
/**
* Get the strong affinity of a proxy. This is a shortcut for {@code getLocatorFor(proxy).getAffinity()}.
*
* @param proxy the proxy (may not be {@code null})
* @return the proxy strong affinity
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
*/
public static Affinity getStrongAffinity(Object proxy) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
return getLocatorFor(proxy).getAffinity();
}
/**
* Compare and change the strong affinity of a proxy. All subsequent invocations against the proxy will use the new affinity.
* Subsequent calls to {@link #getLocatorFor(Object)} for the given proxy will return the updated locator. If the
* affinity is not equal to the expected value, {@code false} is returned and no change is made.
*
* @param proxy the proxy (may not be {@code null})
* @param newAffinity the new affinity (may not be {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
* @throws SecurityException if a security manager is present and the caller does not have the {@code changeStrongAffinity} {@link EJBClientPermission}
*/
public static boolean compareAndSetStrongAffinity(Object proxy, Affinity expectedAffinity, Affinity newAffinity) throws IllegalArgumentException, SecurityException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkNotNullParam("expectedAffinity", expectedAffinity);
Assert.checkNotNullParam("newAffinity", newAffinity);
final EJBInvocationHandler<?> invocationHandler = EJBInvocationHandler.forProxy(proxy);
final Affinity existing = invocationHandler.getLocator().getAffinity();
if (! expectedAffinity.equals(existing)) {
return false;
}
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new EJBClientPermission(EJBClientPermission.Name.changeStrongAffinity));
}
return invocationHandler.compareAndSetStrongAffinity(expectedAffinity, newAffinity);
}
/**
* Transform the strong affinity of a proxy. All subsequent invocations against the proxy will use the new affinity.
* Subsequent calls to {@link #getLocatorFor(Object)} for the given proxy will return the updated locator.
*
* @param proxy the proxy (may not be {@code null})
* @param transformOperator the operator to apply to acquire the new affinity from the old one (may not be {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
* @throws SecurityException if a security manager is present and the caller does not have the {@code changeStrongAffinity} {@link EJBClientPermission}
*/
public static void transformStrongAffinity(Object proxy, UnaryOperator<Affinity> transformOperator) throws IllegalArgumentException, SecurityException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkNotNullParam("transformOperator", transformOperator);
final EJBInvocationHandler<?> invocationHandler = EJBInvocationHandler.forProxy(proxy);
Affinity oldAffinity = invocationHandler.getLocator().getAffinity();
Affinity newAffinity = transformOperator.apply(oldAffinity);
Assert.assertNotNull(newAffinity);
if (oldAffinity.equals(newAffinity)) {
return;
}
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new EJBClientPermission(EJBClientPermission.Name.changeStrongAffinity));
}
while (! invocationHandler.compareAndSetStrongAffinity(oldAffinity, newAffinity)) {
oldAffinity = invocationHandler.getLocator().getAffinity();
newAffinity = transformOperator.apply(oldAffinity);
Assert.assertNotNull(newAffinity);
if (oldAffinity.equals(newAffinity)) {
return;
}
}
}
/**
* Change the weak affinity of a proxy. All subsequent invocations against the proxy will use the new affinity.
*
* @param proxy the proxy (may not be {@code null})
* @param newAffinity the new affinity (may not be {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
* @throws SecurityException if a security manager is present and the caller does not have the {@code changeWeakAffinity} {@link EJBClientPermission}
*/
public static void setWeakAffinity(Object proxy, Affinity newAffinity) throws IllegalArgumentException, SecurityException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkNotNullParam("newAffinity", newAffinity);
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new EJBClientPermission(EJBClientPermission.Name.changeWeakAffinity));
}
EJBInvocationHandler.forProxy(proxy).setWeakAffinity(newAffinity);
}
/**
* Get the current weak affinity of a proxy.
*
* @param proxy the proxy (must not be {@code null})
* @return the affinity (not {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance
*/
public static Affinity getWeakAffinity(Object proxy) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
return EJBInvocationHandler.forProxy(proxy).getWeakAffinity();
}
/**
* Convert a non-stateful proxy to be stateful. If the proxy was already stateful and the session ID matches, the
* proxy is unchanged. If the proxy was otherwise already stateful, an exception is thrown. Subsequent calls to
* {@link #getLocatorFor(Object)} for the given proxy will return the updated locator.
*
* @param proxy the proxy to convert (must not be {@code null})
* @param sessionID the session ID to use for the stateful locator (must not be {@code null})
* @throws IllegalArgumentException if the given proxy is not a valid client proxy instance, or the proxy is already
* stateful with a different session ID
*/
public static void convertToStateful(Object proxy, SessionID sessionID) throws IllegalArgumentException {
Assert.checkNotNullParam("proxy", proxy);
Assert.checkNotNullParam("sessionID", sessionID);
EJBInvocationHandler.forProxy(proxy).setSessionID(sessionID);
}
/**
* Get a {@code UserTransaction} object instance which can be used to control transactions on a specific node.
*
* @param targetNodeName the node name
* @return the {@code UserTransaction} instance
* @throws IllegalStateException if the transaction context isn't set or cannot provide a {@code UserTransaction} instance
*/
@Deprecated
@SuppressWarnings("unused")
public static UserTransaction getUserTransaction(String targetNodeName) {
final URI uri;
try (final ServicesQueue queue = EJBClientContext.getCurrent().getDiscovery().discover(
EJBClientContext.EJB_SERVICE_TYPE, FilterSpec.equal(EJBClientContext.FILTER_ATTR_NODE, targetNodeName)
)) {
uri = queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
if (uri == null) {
throw Logs.MAIN.userTxNotSupportedByTxContext();
}
return RemoteTransactionContext.getInstance().getUserTransaction(uri);
}
}