/*
* Copyright (C) 2009 Red Hat, Inc. and/or its affiliates.
*
* 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.errai.cdi.server;
import static java.util.ResourceBundle.getBundle;
import static org.jboss.errai.cdi.server.CDIServerUtil.lookupRPCBean;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Random;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.ProcessObserverMethod;
import javax.inject.Qualifier;
import org.jboss.errai.bus.client.api.builder.DefaultRemoteCallBuilder;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageBus;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.client.api.messaging.RequestDispatcher;
import org.jboss.errai.bus.server.AsyncDispatcher;
import org.jboss.errai.bus.server.ServerMessageBusImpl;
import org.jboss.errai.bus.server.SimpleDispatcher;
import org.jboss.errai.bus.server.annotations.Remote;
import org.jboss.errai.bus.server.annotations.Service;
import org.jboss.errai.bus.server.io.RPCEndpointFactory;
import org.jboss.errai.bus.server.io.RemoteServiceCallback;
import org.jboss.errai.bus.server.io.ServiceInstanceProvider;
import org.jboss.errai.bus.server.service.ErraiService;
import org.jboss.errai.bus.server.service.ErraiServiceSingleton;
import org.jboss.errai.bus.server.util.NotAService;
import org.jboss.errai.bus.server.util.SecureHashUtil;
import org.jboss.errai.bus.server.util.ServiceMethodParser;
import org.jboss.errai.bus.server.util.ServiceParser;
import org.jboss.errai.bus.server.util.ServiceTypeParser;
import org.jboss.errai.cdi.server.events.AnyEventObserver;
import org.jboss.errai.cdi.server.events.EventDispatcher;
import org.jboss.errai.cdi.server.events.EventRoutingTable;
import org.jboss.errai.cdi.server.events.ShutdownEventObserver;
import org.jboss.errai.codegen.util.ProxyUtil;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.framework.ProxyFactory;
import org.jboss.errai.common.server.api.ErraiBootstrapFailure;
import org.jboss.errai.config.rebind.EnvUtil;
import org.jboss.errai.config.util.ClassScanner;
import org.jboss.errai.enterprise.client.cdi.CDIProtocol;
import org.jboss.errai.enterprise.client.cdi.EventQualifierSerializer;
import org.jboss.errai.enterprise.client.cdi.api.CDI;
import org.jboss.errai.enterprise.rebind.NonGwtEventQualifierSerializerGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extension points to the CDI container. Makes Errai components available as CDI beans (i.e. the
* message bus) and registers CDI components as services with Errai.
*
* @author Heiko Braun <hbraun@redhat.com>
* @author Mike Brock <cbrock@redhat.com>
* @author Christian Sadilek <csadilek@redhat.com>
* @author Max Barkley <mbarkley@redhat.com>
*/
public class CDIExtensionPoints implements Extension {
public static final Logger log = LoggerFactory.getLogger(CDIExtensionPoints.class);
private final TypeRegistry managedTypes = new TypeRegistry();
private final Set<MessageSender> messageSenders = new LinkedHashSet<>();
private final Map<String, Annotation> eventQualifiers = new HashMap<>();
private final Map<String, Annotation> beanQualifiers = new HashMap<>();
private final Set<String> observableEvents = new HashSet<>();
private static final Set<String> vetoClasses;
private static final String ERRAI_CDI_STANDALONE = "errai.cdi.standalone";
private static final String ERRAI_VETO_PATTERN = "errai.cdi.veto.pattern";
private static final String DEFAULT_VETO_PATTERN = "(^|.*\\.)client(?!\\.shared)(\\..*)?";
static {
final Set<String> veto = new HashSet<>();
veto.add(ServerMessageBusImpl.class.getName());
veto.add(RequestDispatcher.class.getName());
veto.add(ErraiService.class.getName());
vetoClasses = Collections.unmodifiableSet(veto);
if (!EventQualifierSerializer.isSet()) {
try {
NonGwtEventQualifierSerializerGenerator.loadAndSetEventQualifierSerializer();
} catch (final Throwable t) {
log.warn("Failed to load static or create static " + EventQualifierSerializer.class.getSimpleName() + ". Falling back to dynamic serialization.");
EventQualifierSerializer.set(new DynamicEventQualifierSerializer());
}
}
}
public void beforeBeanDiscovery(@Observes final BeforeBeanDiscovery bbd) {
log.info("starting errai cdi ...");
final ResourceBundle erraiServiceConfig;
try {
erraiServiceConfig = getBundle("ErraiService");
} catch (final MissingResourceException e) {
// ErraiService is optional!
return;
}
if (erraiServiceConfig.containsKey(ERRAI_CDI_STANDALONE)) {
final boolean standalone = "true".equals(erraiServiceConfig.getString(ERRAI_CDI_STANDALONE).trim());
if (standalone) {
log.info("errai cdi running in standalone mode.");
}
else {
log.info("errai cdi running as regular extension.");
}
}
final String dispatchImplKey = "errai.dispatcher_implementation";
if (erraiServiceConfig.containsKey(dispatchImplKey)) {
if (AsyncDispatcher.class.getName().equals(erraiServiceConfig.getString(dispatchImplKey))) {
throw new ErraiBootstrapFailure("Cannot start Errai CDI. You have have configured the service to use the "
+ AsyncDispatcher.class.getName()
+ " dispatcher implementation. Due to limitations of Weld, you must use the "
+ SimpleDispatcher.class.getName() + " in order to use this module.");
}
}
}
/**
* Register managed beans as Errai services
*
* @param event
* -
* @param <T>
* -
*/
public <T> void observeResources(@Observes final ProcessAnnotatedType<T> event) {
final AnnotatedType<T> type = event.getAnnotatedType();
registerBeanQualifiers(type);
maybeRegisterServices(type);
maybeVetoClientClass(event, type);
}
private <T> void registerBeanQualifiers(final AnnotatedType<T> type) {
for (final Annotation a : type.getJavaClass().getAnnotations()) {
if (a.annotationType().isAnnotationPresent(Qualifier.class)) {
beanQualifiers.put(a.annotationType().getName(), a);
}
}
}
private <T> void maybeVetoClientClass(final ProcessAnnotatedType<T> event, final AnnotatedType<T> type) {
// veto on client side implementations that contain CDI annotations
// (i.e. @Observes) Otherwise Weld might try to invoke on them
final Class<?> javaClass = type.getJavaClass();
final Package pkg = javaClass.getPackage();
if (vetoClasses.contains(javaClass.getName())
|| (pkg != null && matchesVetoPattern(pkg.getName())
&& !javaClass.isInterface())) {
log.debug("Vetoing processed type: " + javaClass.getName());
event.veto();
}
}
private static boolean matchesVetoPattern(final String pkg) {
try {
final String pattern = System.getProperty(ERRAI_VETO_PATTERN, DEFAULT_VETO_PATTERN);
return Pattern.matches(pattern, pkg);
} catch (final PatternSyntaxException ex) {
throw new RuntimeException("There is a syntax error in the regex provided for " + ERRAI_VETO_PATTERN, ex);
}
}
private <T> void maybeRegisterServices(final AnnotatedType<T> type) {
if (type.isAnnotationPresent(Remote.class)) {
log.info("discovered errai remote interface: " + type);
managedTypes.addRemoteInterface(type.getJavaClass());
}
else if (type.isAnnotationPresent(Service.class)) {
log.info("discovered errai service: " + type);
boolean isRpc = false;
final Class<T> javaClass = type.getJavaClass();
for (final Class<?> intf : javaClass.getInterfaces()) {
isRpc = isRpc || intf.isAnnotationPresent(Remote.class);
if (isRpc) {
managedTypes.addRemoteServiceImplementation(intf, javaClass);
}
}
if (!isRpc) {
try {
managedTypes.addService(new ServiceTypeParser(type.getJavaClass()));
} catch (final NotAService e) {
e.printStackTrace();
}
}
}
for (@SuppressWarnings("rawtypes") final AnnotatedMethod method : type.getMethods()) {
if (method.isAnnotationPresent(Service.class)) {
try {
managedTypes.addService(new ServiceMethodParser(method.getJavaMember()));
} catch (final NotAService e) {
e.printStackTrace();
}
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void processObserverMethod(@Observes final ProcessObserverMethod processObserverMethod) {
final Type t = processObserverMethod.getObserverMethod().getObservedType();
Class type = null;
if (t instanceof Class) {
type = (Class) t;
}
ClassScanner.setReflectionsScanning(true);
if (type != null && EnvUtil.isPortableType(type) && !EnvUtil.isLocalEventType(type)) {
final Set<Annotation> annotations = processObserverMethod.getObserverMethod().getObservedQualifiers();
final Annotation[] methodQualifiers = annotations.toArray(new Annotation[annotations.size()]);
for (final Annotation qualifier : methodQualifiers) {
eventQualifiers.put(EventQualifierSerializer.get().serialize(qualifier), qualifier);
}
observableEvents.add(type.getName());
}
}
@SuppressWarnings("rawtypes")
public void afterBeanDiscovery(@Observes final AfterBeanDiscovery abd, final BeanManager bm) {
final ErraiService service = ErraiServiceSingleton.getService();
final MessageBus bus = service.getBus();
final EventRoutingTable eventRoutingTable = new EventRoutingTable();
if (bus.isSubscribed(CDI.SERVER_DISPATCHER_SUBJECT)) {
return;
}
final byte[] randBytes = new byte[32];
final Random random = new Random(System.currentTimeMillis());
random.nextBytes(randBytes);
abd.addBean(new ErraiServiceBean(bm, SecureHashUtil.hashToHexString(randBytes)));
for (final MessageSender ms : messageSenders) {
abd.addBean(new SenderBean(ms.getSenderType(), ms.getQualifiers(), bus));
}
// Errai bus injection
abd.addBean(new MessageBusBean(bus));
// Support to inject the request dispatcher.
abd.addBean(new RequestDispatcherMetaData(bm, service.getDispatcher()));
// Register observers
abd.addObserverMethod(new ShutdownEventObserver(managedTypes, bus));
// subscribe service and rpc endpoints
subscribeServices(bm, bus);
// initialize the CDI event bridge to the client
final EventDispatcher eventDispatcher =
new EventDispatcher(bm, eventRoutingTable, bus, observableEvents, eventQualifiers);
AnyEventObserver.init(eventDispatcher);
// subscribe event dispatcher
bus.subscribe(CDI.SERVER_DISPATCHER_SUBJECT, eventDispatcher);
}
/**
* Registers beans (type and method services) as they become available from the bean manager.
*/
private class StartupCallback implements Runnable {
private final Set<Object> toRegister = new HashSet<>();
private final BeanManager beanManager;
private final MessageBus bus;
private final ScheduledExecutorService scheduledExecutorService;
private final long expiryTime;
private StartupCallback(final BeanManager beanManager, final MessageBus bus,
final ScheduledExecutorService scheduledExecutorService, final int timeOutInSeconds) {
this.beanManager = beanManager;
this.bus = bus;
this.scheduledExecutorService = scheduledExecutorService;
toRegister.addAll(managedTypes.getDelegateClasses());
this.expiryTime = System.currentTimeMillis() + (timeOutInSeconds * 1000);
}
private Annotation[] getQualifiers(final Class<?> delegateClass) {
int length = 0;
for (final Annotation anno : delegateClass.getAnnotations()) {
if (anno.annotationType().isAnnotationPresent(Qualifier.class))
length += 1;
}
final Annotation[] ret = new Annotation[length];
int i = 0;
for (final Annotation anno : delegateClass.getAnnotations()) {
if (anno.annotationType().isAnnotationPresent(Qualifier.class))
ret[i++] = anno;
}
return ret;
}
@Override
public void run() {
if (System.currentTimeMillis() > expiryTime) {
scheduledExecutorService.shutdown();
throw new RuntimeException("failed to discover beans: " + managedTypes.getDelegateClasses());
}
if (toRegister.isEmpty()) {
scheduledExecutorService.shutdown();
return;
}
// As each delegate becomes available, register all the associated services (type and method)
for (final Class<?> delegateClass : managedTypes.getDelegateClasses()) {
try {
if (!toRegister.contains(delegateClass) || beanManager.getBeans(delegateClass, getQualifiers(delegateClass)).size() == 0) {
continue;
}
}
catch(final Throwable t) {
continue;
}
for (final ServiceParser svcParser : managedTypes.getDelegateServices(delegateClass)) {
final Object delegateInstance;
try {
delegateInstance = CDIServerUtil.lookupBean(beanManager, delegateClass, getQualifiers(delegateClass));
}
catch (final IllegalStateException t) {
// handle WELD-001332: BeanManager method getReference() is not available during application initialization
// try again later...
return;
}
final MessageCallback callback = svcParser.getCallback(delegateInstance);
if (callback != null) {
if (svcParser.isLocal()) {
bus.subscribeLocal(svcParser.getServiceName(), callback);
}
else {
bus.subscribe(svcParser.getServiceName(), callback);
}
}
}
toRegister.remove(delegateClass);
}
}
}
private void subscribeServices(final BeanManager beanManager, final MessageBus bus) {
/**
* Due to the lack of contract in CDI guaranteeing when beans will be available, we use an
* executor to search for the beans every 100ms until it finds them. Or, after a 25 seconds,
* blow up if they don't become available.
*/
final ScheduledExecutorService startupScheduler = Executors.newScheduledThreadPool(1);
startupScheduler.scheduleAtFixedRate(new StartupCallback(beanManager, bus, startupScheduler, 25), 0, 100,
TimeUnit.MILLISECONDS);
for (final Class<?> remoteInterfaceType : managedTypes.getRemoteInterfaces()) {
if (!managedTypes.isRemoteInterfaceImplemented(remoteInterfaceType)) {
log.warn("No @Service implementations found for " + remoteInterfaceType.getName());
}
createRPCScaffolding(remoteInterfaceType, bus, beanManager);
}
}
private void createRPCScaffolding(final Class<?> remoteIface, final MessageBus bus, final BeanManager beanManager) {
final Map<String, MessageCallback> epts = new HashMap<>();
final ServiceInstanceProvider genericSvc = new ServiceInstanceProvider() {
@SuppressWarnings("unchecked")
@Override
public Object get(final Message message) {
if (message.hasPart(CDIProtocol.Qualifiers)) {
final List<String> quals = message.get(List.class, CDIProtocol.Qualifiers);
final Annotation[] qualAnnos = new Annotation[quals.size()];
for (int i = 0; i < quals.size(); i++) {
qualAnnos[i] = beanQualifiers.get(quals.get(i));
}
return lookupRPCBean(beanManager, remoteIface, qualAnnos);
}
else {
return lookupRPCBean(beanManager, remoteIface, null);
}
}
};
// beware of classloading issues. better reflect on the actual instance
for (final Method method : remoteIface.getMethods()) {
if (ProxyUtil.isMethodInInterface(remoteIface, method)) {
epts.put(ProxyUtil.createCallSignature(remoteIface, method),
RPCEndpointFactory.createEndpointFor(genericSvc, method, bus));
}
}
final RemoteServiceCallback delegate = new RemoteServiceCallback(epts);
bus.subscribe(remoteIface.getName() + ":RPC", new MessageCallback() {
@Override
public void callback(final Message message) {
delegate.callback(message);
}
});
log.debug("registered RPC service for: " + remoteIface.getName());
// note: this method just exists because we want AbstractRemoteCallBuilder to be package
// private.
DefaultRemoteCallBuilder.setProxyFactory(Assert.notNull(new ProxyFactory() {
@Override
public <T> T getRemoteProxy(final Class<T> proxyType) {
throw new RuntimeException(
"There is not yet an available Errai RPC implementation for the server-side environment.");
}
}));
}
static class MessageSender {
private final Type senderType;
private final Set<Annotation> qualifiers;
MessageSender(final Type senderType, final Set<Annotation> qualifiers) {
this.senderType = senderType;
this.qualifiers = qualifiers;
}
public Type getSenderType() {
return senderType;
}
public Set<Annotation> getQualifiers() {
return qualifiers;
}
}
}