/* * 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; } } }