/*
* JBoss, Home of Professional Open Source.
* Copyright 2016, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.ejb3.remote;
import org.jboss.as.ee.component.Component;
import org.jboss.as.ee.component.ComponentIsStoppedException;
import org.jboss.as.ee.component.ComponentView;
import org.jboss.as.ee.component.interceptors.InvocationType;
import org.jboss.as.ejb3.component.EJBComponentUnavailableException;
import org.jboss.as.ejb3.component.interceptors.CancellationFlag;
import org.jboss.as.ejb3.component.session.SessionBeanComponent;
import org.jboss.as.ejb3.component.stateful.StatefulSessionComponent;
import org.jboss.as.ejb3.component.stateless.StatelessSessionComponent;
import org.jboss.as.ejb3.deployment.DeploymentModuleIdentifier;
import org.jboss.as.ejb3.deployment.DeploymentRepository;
import org.jboss.as.ejb3.deployment.DeploymentRepositoryListener;
import org.jboss.as.ejb3.deployment.EjbDeploymentInformation;
import org.jboss.as.ejb3.deployment.ModuleDeployment;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.as.network.ClientMapping;
import org.jboss.ejb.client.Affinity;
import org.jboss.ejb.client.EJBClientInvocationContext;
import org.jboss.ejb.client.EJBIdentifier;
import org.jboss.ejb.client.EJBLocator;
import org.jboss.ejb.client.EJBMethodLocator;
import org.jboss.ejb.client.SessionID;
import org.jboss.ejb.client.StatefulEJBLocator;
import org.jboss.ejb.server.Association;
import org.jboss.ejb.server.CancelHandle;
import org.jboss.ejb.server.ClusterTopologyListener;
import org.jboss.ejb.server.InvocationRequest;
import org.jboss.ejb.server.ListenerHandle;
import org.jboss.ejb.server.ModuleAvailabilityListener;
import org.jboss.ejb.server.Request;
import org.jboss.ejb.server.SessionOpenRequest;
import org.jboss.invocation.InterceptorContext;
import org.jboss.remoting3.Connection;
import org.wildfly.clustering.registry.Registry;
import org.wildfly.common.annotation.NotNull;
import org.wildfly.security.auth.server.SecurityIdentity;
import javax.ejb.EJBException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:tadamski@redhat.com">Tomasz Adamski</a>
*/
final class AssociationImpl implements Association {
private final DeploymentRepository deploymentRepository;
private final RegistryCollector<String, List<ClientMapping>> clientMappingRegistryCollector;
private volatile Executor executor;
AssociationImpl(final DeploymentRepository deploymentRepository, final RegistryCollector<String, List<ClientMapping>> registryCollector) {
this.deploymentRepository = deploymentRepository;
clientMappingRegistryCollector = registryCollector;
this.executor = executor;
}
@Override
public CancelHandle receiveInvocationRequest(@NotNull final InvocationRequest invocationRequest) {
final EJBIdentifier ejbIdentifier = invocationRequest.getEJBIdentifier();
final String appName = ejbIdentifier.getAppName();
final String moduleName = ejbIdentifier.getModuleName();
final String distinctName = ejbIdentifier.getDistinctName();
final String beanName = ejbIdentifier.getBeanName();
final EjbDeploymentInformation ejbDeploymentInformation = findEJB(appName, moduleName, distinctName, beanName);
if (ejbDeploymentInformation == null) {
invocationRequest.writeNoSuchEJB();
return CancelHandle.NULL;
}
final ClassLoader classLoader = ejbDeploymentInformation.getDeploymentClassLoader();
final InvocationRequest.Resolved requestContent;
try {
requestContent = invocationRequest.getRequestContent(classLoader);
} catch (IOException | ClassNotFoundException e) {
invocationRequest.writeException(new EJBException(e));
return CancelHandle.NULL;
}
final Map<String, Object> attachments = requestContent.getAttachments();
final EJBLocator<?> ejbLocator = requestContent.getEJBLocator();
final String viewClassName = ejbLocator.getViewType().getName();
if (!ejbDeploymentInformation.isRemoteView(viewClassName)) {
invocationRequest.writeWrongViewType();
return CancelHandle.NULL;
}
final ComponentView componentView = ejbDeploymentInformation.getView(viewClassName);
final Method invokedMethod = findMethod(componentView, invocationRequest.getMethodLocator());
if (invokedMethod == null) {
invocationRequest.writeNoSuchMethod();
return CancelHandle.NULL;
}
final boolean isAsync = componentView.isAsynchronous(invokedMethod);
final boolean oneWay = isAsync && invokedMethod.getReturnType() == void.class;
if (oneWay) {
// send immediate response
requestContent.writeInvocationResult(null);
} else if(isAsync) {
invocationRequest.writeProceedAsync();
}
final CancellationFlag cancellationFlag = new CancellationFlag();
Runnable runnable = () -> {
if (! cancellationFlag.runIfNotCancelled()) {
if (! oneWay) invocationRequest.writeCancelResponse();
return;
}
// invoke the method
final Object result;
// the Remoting connection that is set here is only used for legacy purposes
SecurityActions.remotingContextSetConnection(invocationRequest.getProviderInterface(Connection.class));
try {
result = invokeMethod(componentView, invokedMethod, invocationRequest, requestContent, cancellationFlag);
} catch (EJBComponentUnavailableException ex) {
// if the EJB is shutting down when the invocation was done, then it's as good as the EJB not being available. The client has to know about this as
// a "no such EJB" failure so that it can retry the invocation on a different node if possible.
EjbLogger.EJB3_INVOCATION_LOGGER.debugf("Cannot handle method invocation: %s on bean: %s due to EJB component unavailability exception. Returning a no such EJB available message back to client", invokedMethod, beanName);
if (! oneWay) invocationRequest.writeNoSuchEJB();
return;
} catch (ComponentIsStoppedException ex) {
EjbLogger.EJB3_INVOCATION_LOGGER.debugf("Cannot handle method invocation: %s on bean: %s due to EJB component stopped exception. Returning a no such EJB available message back to client", invokedMethod, beanName);
if (! oneWay) invocationRequest.writeNoSuchEJB();
return;
// TODO should we write a specifc response with a specific protocol letting client know that server is suspending?
} catch (CancellationException ex) {
if (! oneWay) invocationRequest.writeCancelResponse();
return;
} catch (Exception exception) {
if (oneWay) return;
// write out the failure
final Exception exceptionToWrite;
final Throwable cause = exception.getCause();
if (componentView.getComponent() instanceof StatefulSessionComponent && exception instanceof EJBException && cause != null) {
if (!(componentView.getComponent().isRemotable(cause))) {
// Avoid serializing the cause of the exception in case it is not remotable
// Client might not be able to deserialize and throw ClassNotFoundException
exceptionToWrite = new EJBException(exception.getLocalizedMessage());
} else {
exceptionToWrite = exception;
}
} else {
exceptionToWrite = exception;
}
invocationRequest.writeException(exceptionToWrite);
return;
}
// invocation was successful
if (! oneWay) try {
// attach any weak affinity if available
Affinity weakAffinity = null;
if (ejbLocator.isStateful() && componentView.getComponent() instanceof StatefulSessionComponent) {
final StatefulSessionComponent statefulSessionComponent = (StatefulSessionComponent) componentView.getComponent();
weakAffinity = getWeakAffinity(statefulSessionComponent, ejbLocator.asStateful());
} else if (componentView.getComponent() instanceof StatelessSessionComponent) {
final StatelessSessionComponent statelessSessionComponent = (StatelessSessionComponent) componentView.getComponent();
weakAffinity = statelessSessionComponent.getWeakAffinity();
}
if (weakAffinity != null && !weakAffinity.equals(Affinity.NONE)) {
attachments.put(Affinity.WEAK_AFFINITY_CONTEXT_KEY, weakAffinity);
}
requestContent.writeInvocationResult(result);
} catch (Throwable ioe) {
EjbLogger.REMOTE_LOGGER.couldNotWriteMethodInvocation(ioe, invokedMethod, beanName, appName, moduleName, distinctName);
}
};
// invoke the method and write out the response, possibly on a separate thread
execute(invocationRequest, runnable, isAsync);
return cancellationFlag::cancel;
}
private void execute(Request request, Runnable task, final boolean isAsync) {
if (request.getProtocol().equals("local") && ! isAsync) {
task.run();
} else {
if(executor != null) {
executor.execute(task);
} else {
request.getRequestExecutor().execute(task);
}
}
}
@Override
@NotNull
public CancelHandle receiveSessionOpenRequest(@NotNull final SessionOpenRequest sessionOpenRequest) {
final EJBIdentifier ejbIdentifier = sessionOpenRequest.getEJBIdentifier();
final String appName = ejbIdentifier.getAppName();
final String moduleName = ejbIdentifier.getModuleName();
final String beanName = ejbIdentifier.getBeanName();
final String distinctName = ejbIdentifier.getDistinctName();
final EjbDeploymentInformation ejbDeploymentInformation = findEJB(appName, moduleName, distinctName, beanName);
if (ejbDeploymentInformation == null) {
sessionOpenRequest.writeNoSuchEJB();
return CancelHandle.NULL;
}
final Component component = ejbDeploymentInformation.getEjbComponent();
if (!(component instanceof StatefulSessionComponent)) {
sessionOpenRequest.writeNotStateful();
return CancelHandle.NULL;
}
final StatefulSessionComponent statefulSessionComponent = (StatefulSessionComponent) component;
// generate the session id and write out the response, possibly on a separate thread
final AtomicBoolean cancelled = new AtomicBoolean();
Runnable runnable = () -> {
if (cancelled.get()) {
sessionOpenRequest.writeCancelResponse();
return;
}
final SessionID sessionID;
try {
sessionID = statefulSessionComponent.createSessionRemote();
} catch (Exception t) {
EjbLogger.REMOTE_LOGGER.exceptionGeneratingSessionId(t, statefulSessionComponent.getComponentName(), ejbIdentifier);
sessionOpenRequest.writeException(t);
return;
}
sessionOpenRequest.convertToStateful(sessionID);
};
execute(sessionOpenRequest, runnable, false);
return ignored -> cancelled.set(true);
}
@Override
public ListenerHandle registerClusterTopologyListener(@NotNull final ClusterTopologyListener clusterTopologyListener) {
RegistryCollector<String, List<ClientMapping>> clientMappingRegistryCollector = this.clientMappingRegistryCollector;
final RegistryCollector.Listener<String, List<ClientMapping>> listener = new RegistryCollector.Listener<String, List<ClientMapping>>() {
public void registryAdded(final Registry<String, List<ClientMapping>> registry) {
final String clusterName = registry.getGroup().getName();
registry.addListener(new ClusterTopologyUpdateListener(clusterName, clusterTopologyListener));
}
public void registryRemoved(final Registry<String, List<ClientMapping>> registry) {
// Only send the cluster removal message if the cluster node count reaches 0
final Map.Entry<String, List<ClientMapping>> localEntry = registry.getEntry(registry.getGroup().getLocalNode());
final Map<String, List<ClientMapping>> entries = registry.getEntries();
if ((localEntry != null) ? (entries.size() == 1) && entries.containsKey(localEntry.getKey()) : entries.isEmpty()) {
clusterTopologyListener.clusterRemoval(Collections.singletonList(registry.getGroup().getName()));
}
}
};
clientMappingRegistryCollector.addListener(listener);
// Ensure the cluster topology listener is also added for any registries that have already been added to clientMappingRegistryCollector
for (Registry<String, List<ClientMapping>> registry : clientMappingRegistryCollector.getRegistries()) {
registry.addListener(new ClusterTopologyUpdateListener(registry.getGroup().getName(), clusterTopologyListener));
}
clusterTopologyListener.clusterTopology(clientMappingRegistryCollector.getRegistries().parallelStream().map(r -> getClusterInfo(r.getEntries(), r.getGroup().getName())).collect(Collectors.toList()));
return () -> clientMappingRegistryCollector.removeListener(listener);
}
ClusterTopologyListener.ClusterInfo getClusterInfo(final Map<String, List<ClientMapping>> added, final String clusterName) {
final List<ClusterTopologyListener.NodeInfo> nodeInfoList = new ArrayList<>(added.size());
for (Map.Entry<String, List<ClientMapping>> entry : added.entrySet()) {
final String nodeName = entry.getKey();
final List<ClientMapping> clientMappingList = entry.getValue();
final List<ClusterTopologyListener.MappingInfo> mappingInfoList = new ArrayList<>();
for (ClientMapping clientMapping : clientMappingList) {
mappingInfoList.add(new ClusterTopologyListener.MappingInfo(
clientMapping.getDestinationAddress(),
clientMapping.getDestinationPort(),
clientMapping.getSourceNetworkAddress(),
clientMapping.getSourceNetworkMaskBits())
);
}
nodeInfoList.add(new ClusterTopologyListener.NodeInfo(nodeName, mappingInfoList));
}
return new ClusterTopologyListener.ClusterInfo(clusterName, nodeInfoList);
}
@Override
public ListenerHandle registerModuleAvailabilityListener(@NotNull final ModuleAvailabilityListener moduleAvailabilityListener) {
final DeploymentRepositoryListener listener = new DeploymentRepositoryListener() {
public void listenerAdded(final DeploymentRepository repository) {
List<ModuleAvailabilityListener.ModuleIdentifier> identifierList = new ArrayList<>();
for (DeploymentModuleIdentifier identifier : repository.getModules().keySet()) {
final ModuleAvailabilityListener.ModuleIdentifier moduleIdentifier = toModuleIdentifier(identifier);
identifierList.add(moduleIdentifier);
}
moduleAvailabilityListener.moduleAvailable(identifierList);
}
private ModuleAvailabilityListener.ModuleIdentifier toModuleIdentifier(final DeploymentModuleIdentifier identifier) {
return new ModuleAvailabilityListener.ModuleIdentifier(identifier.getApplicationName(), identifier.getModuleName(), identifier.getDistinctName());
}
public void deploymentAvailable(final DeploymentModuleIdentifier deployment, final ModuleDeployment moduleDeployment) {
moduleAvailabilityListener.moduleAvailable(Collections.singletonList(toModuleIdentifier(deployment)));
}
public void deploymentStarted(final DeploymentModuleIdentifier deployment, final ModuleDeployment moduleDeployment) {
}
public void deploymentRemoved(final DeploymentModuleIdentifier deployment) {
moduleAvailabilityListener.moduleUnavailable(Collections.singletonList(toModuleIdentifier(deployment)));
}
@Override public void deploymentSuspended(DeploymentModuleIdentifier deployment) {
moduleAvailabilityListener.moduleUnavailable(Collections.singletonList(toModuleIdentifier(deployment)));
}
@Override public void deploymentResumed(DeploymentModuleIdentifier deployment) {
moduleAvailabilityListener.moduleAvailable(Collections.singletonList(toModuleIdentifier(deployment)));
}
};
deploymentRepository.addListener(listener);
return () -> deploymentRepository.removeListener(listener);
}
private EjbDeploymentInformation findEJB(final String appName, final String moduleName, final String distinctName, final String beanName) {
final DeploymentModuleIdentifier ejbModule = new DeploymentModuleIdentifier(appName, moduleName, distinctName);
final Map<DeploymentModuleIdentifier, ModuleDeployment> modules = this.deploymentRepository.getStartedModules();
if (modules == null || modules.isEmpty()) {
return null;
}
final ModuleDeployment moduleDeployment = modules.get(ejbModule);
if (moduleDeployment == null) {
return null;
}
return moduleDeployment.getEjbs().get(beanName);
}
private class ClusterTopologyUpdateListener implements Registry.Listener<String, List<ClientMapping>> {
private final String clusterName;
private final ClusterTopologyListener delegate;
ClusterTopologyUpdateListener(final String clusterName, final ClusterTopologyListener delegate) {
this.clusterName = clusterName;
this.delegate = delegate;
}
public void addedEntries(final Map<String, List<ClientMapping>> added) {
delegate.clusterNewNodesAdded(getClusterInfo(added, clusterName));
}
public void updatedEntries(final Map<String, List<ClientMapping>> updated) {
delegate.clusterNewNodesAdded(getClusterInfo(updated, clusterName));
}
public void removedEntries(final Map<String, List<ClientMapping>> removed) {
final ArrayList<ClusterTopologyListener.ClusterRemovalInfo> list = new ArrayList<>();
list.add(new ClusterTopologyListener.ClusterRemovalInfo(clusterName, new ArrayList<>(removed.keySet())));
delegate.clusterNodesRemoved(list);
}
}
static Object invokeMethod(final ComponentView componentView, final Method method, final InvocationRequest incomingInvocation, final InvocationRequest.Resolved content, final CancellationFlag cancellationFlag) throws Exception {
final InterceptorContext interceptorContext = new InterceptorContext();
interceptorContext.setParameters(content.getParameters());
interceptorContext.setMethod(method);
interceptorContext.putPrivateData(Component.class, componentView.getComponent());
interceptorContext.putPrivateData(ComponentView.class, componentView);
interceptorContext.putPrivateData(InvocationType.class, InvocationType.REMOTE);
interceptorContext.setBlockingCaller(false);
// setup the contextData on the (spec specified) InvocationContext
final Map<String, Object> invocationContextData = new HashMap<String, Object>();
interceptorContext.setContextData(invocationContextData);
if (content.getAttachments() != null) {
// attach the attachments which were passed from the remote client
for (final Map.Entry<String, Object> attachment : content.getAttachments().entrySet()){
if (attachment == null) {
continue;
}
final String key = attachment.getKey();
final Object value = attachment.getValue();
// these are private to JBoss EJB implementation and not meant to be visible to the
// application, so add these attachments to the privateData of the InterceptorContext
if (EJBClientInvocationContext.PRIVATE_ATTACHMENTS_KEY.equals(key)) {
final Map<?, ?> privateAttachments = (Map<?, ?>) value;
for (final Map.Entry<?, ?> privateAttachment : privateAttachments.entrySet()) {
interceptorContext.putPrivateData(privateAttachment.getKey(), privateAttachment.getValue());
}
} else {
// add it to the InvocationContext which will be visible to the target bean and the
// application specific interceptors
invocationContextData.put(key, value);
}
}
}
// add the session id to the interceptor context, if it's a stateful ejb locator
final EJBLocator<?> ejbLocator = content.getEJBLocator();
if (ejbLocator.isStateful()) {
interceptorContext.putPrivateData(SessionID.class, ejbLocator.asStateful().getSessionId());
}
// add transaction
if (content.hasTransaction()) {
interceptorContext.setTransactionSupplier(content::getTransaction);
}
// add security identity
final SecurityIdentity securityIdentity = incomingInvocation.getSecurityIdentity();
final boolean isAsync = componentView.isAsynchronous(method);
final boolean oneWay = isAsync && method.getReturnType() == void.class;
final boolean isSessionBean = componentView.getComponent() instanceof SessionBeanComponent;
if (isAsync && isSessionBean) {
if (! oneWay) {
interceptorContext.putPrivateData(CancellationFlag.class, cancellationFlag);
}
final Object result = invokeWithIdentity(componentView, interceptorContext, securityIdentity);
return result == null ? null : ((Future<?>) result).get();
} else {
return invokeWithIdentity(componentView, interceptorContext, securityIdentity);
}
}
private static Object invokeWithIdentity(final ComponentView componentView, final InterceptorContext interceptorContext, final SecurityIdentity securityIdentity) throws Exception {
return securityIdentity == null ? componentView.invoke(interceptorContext) : securityIdentity.runAsFunctionEx(ComponentView::invoke, componentView, interceptorContext);
}
private static Method findMethod(final ComponentView componentView, final EJBMethodLocator ejbMethodLocator) {
final Set<Method> viewMethods = componentView.getViewMethods();
for (final Method method : viewMethods) {
if (method.getName().equals(ejbMethodLocator.getMethodName())) {
final Class<?>[] methodParamTypes = method.getParameterTypes();
if (methodParamTypes.length != ejbMethodLocator.getParameterCount()) {
continue;
}
boolean found = true;
for (int i = 0; i < methodParamTypes.length; i++) {
if (!methodParamTypes[i].getName().equals(ejbMethodLocator.getParameterTypeName(i))) {
found = false;
break;
}
}
if (found) {
return method;
}
}
}
return null;
}
private static Affinity getWeakAffinity(final StatefulSessionComponent statefulSessionComponent, final StatefulEJBLocator<?> statefulEJBLocator) {
final SessionID sessionID = statefulEJBLocator.getSessionId();
return statefulSessionComponent.getCache().getWeakAffinity(sessionID);
}
Executor getExecutor() {
return executor;
}
void setExecutor(Executor executor) {
this.executor = executor;
}
}