All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.java.annotation.ServerAnnotationProcessor Maven / Gradle / Ivy

There is a newer version: 4.0.9
Show newest version
/*
 * Copyright (c) 2010 the original author or authors.
 *
 * 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.cometd.java.annotation;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;

import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ConfigurableServerChannel.Initializer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;

/**
 * 

Processes annotations in server-side service objects.

*

Service objects must be annotated with {@link Service} at class level to be processed by this processor, * for example:

*
 * @Service
 * public class MyService
 * {
 *     @Session
 *     private ServerSession session;
 *
 *     @Configure("/foo")
 *     public void configureFoo(ConfigurableServerChannel channel)
 *     {
 *         channel.setPersistent(...);
 *         channel.addListener(...);
 *         channel.addAuthorizer(...);
 *     }
 *
 *     @Listener("/foo")
 *     public void handleFooMessages(ServerSession remote, ServerMessage.Mutable message)
 *     {
 *         // Do something
 *     }
 * }
 * 
*

The processor is used in this way:

*
 * BayeuxServer bayeux = ...;
 * ServerAnnotationProcessor processor = ServerAnnotationProcessor.get(bayeux);
 * MyService s = new MyService();
 * processor.process(s);
 * 
* * @see ClientAnnotationProcessor */ public class ServerAnnotationProcessor extends AnnotationProcessor { private final ConcurrentMap sessions = new ConcurrentHashMap(); private final ConcurrentMap> listeners = new ConcurrentHashMap>(); private final ConcurrentMap> subscribers = new ConcurrentHashMap>(); private final BayeuxServer bayeuxServer; private final Object[] injectables; public ServerAnnotationProcessor(BayeuxServer bayeuxServer) { this(bayeuxServer, new Object[0]); } public ServerAnnotationProcessor(BayeuxServer bayeuxServer, Object... injectables) { this.bayeuxServer = bayeuxServer; this.injectables = injectables; } /** * Processes dependencies annotated with {@link Inject} and {@link Session}, lifecycle methods * annotated with {@link PostConstruct}, and callback methods annotated with {@link Listener} * and {@link Subscription}. * * @param bean the annotated service instance * @return true if the bean contains at least one annotation that has been processed, false otherwise */ public boolean process(Object bean) { boolean result = processDependencies(bean); result |= processConfigurations(bean); result |= processCallbacks(bean); result |= processPostConstruct(bean); return result; } /** * Processes the methods annotated with {@link Configure} * * @param bean the annotated service instance * @return true if at least one annotated configure has been processed, false otherwise */ public boolean processConfigurations(final Object bean) { if (bean == null) return false; Class klass = bean.getClass(); Service serviceAnnotation = klass.getAnnotation(Service.class); if (serviceAnnotation == null) return false; boolean result = false; for (Class c = bean.getClass(); c != null; c = c.getSuperclass()) { Method[] methods = c.getDeclaredMethods(); for (final Method method : methods) { final Configure configure = method.getAnnotation(Configure.class); if (configure != null) { result = true; String[] channels = configure.value(); for (String channel : channels) { final Initializer init = new Initializer() { public void configureChannel(ConfigurableServerChannel channel) { boolean flip = false; try { logger.debug("Configure channel {} with method {} on bean {}", new Object[]{channel, method, bean}); if (!method.isAccessible()) { flip = true; method.setAccessible(true); } method.invoke(bean, channel); } catch (Exception x) { throw new RuntimeException(x); } finally { if (flip) method.setAccessible(false); } } }; boolean initialized = bayeuxServer.createIfAbsent(channel, init); if (initialized) { logger.debug("Channel {} already initialized. Not called method {} on bean {}", new Object[]{channel, method, bean}); } else { if (configure.configureIfExists()) { logger.debug("Configure channel {} with method {} on bean {}", new Object[]{channel, method, bean}); init.configureChannel(bayeuxServer.getChannel(channel)); } else if (configure.errorIfExists()) throw new IllegalStateException("Channel already configured: " + channel); } } } } } return result; } /** * Processes the dependencies annotated with {@link Inject} and {@link Session}. * * @param bean the annotated service instance * @return true if at least one annotated dependency has been processed, false otherwise */ public boolean processDependencies(Object bean) { if (bean == null) return false; Class klass = bean.getClass(); Service serviceAnnotation = klass.getAnnotation(Service.class); if (serviceAnnotation == null) return false; List injectables = new ArrayList(Arrays.asList(this.injectables)); injectables.add(0, bayeuxServer); boolean result = processInjectables(bean, injectables); LocalSession session = findOrCreateLocalSession(bean, serviceAnnotation.value()); result |= processSession(bean, session); return result; } /** * Processes lifecycle methods annotated with {@link PostConstruct}. * * @param bean the annotated service instance * @return true if at least one lifecycle method has been invoked, false otherwise */ @Override public boolean processPostConstruct(Object bean) { return super.processPostConstruct(bean); } /** * Processes the callbacks annotated with {@link Listener} and {@link Subscription}. * * @param bean the annotated service instance * @return true if at least one annotated callback has been processed, false otherwise */ public boolean processCallbacks(Object bean) { if (bean == null) return false; Class klass = bean.getClass(); Service serviceAnnotation = klass.getAnnotation(Service.class); if (serviceAnnotation == null) return false; LocalSession session = findOrCreateLocalSession(bean, serviceAnnotation.value()); boolean result = processListener(bean, session); result |= processSubscription(bean, session); return result; } /** * Performs the opposite processing done by {@link #process(Object)} on callbacks methods * annotated with {@link Listener} and {@link Subscription}, and on lifecycle methods annotated * with {@link PreDestroy}. * * @param bean the annotated service instance * @return true if at least one deprocessing has been performed, false otherwise * @see #process(Object) */ public boolean deprocess(Object bean) { boolean result = deprocessCallbacks(bean); result |= processPreDestroy(bean); return result; } /** * Performs the opposite processing done by {@link #processCallbacks(Object)} on callback methods * annotated with {@link Listener} and {@link Subscription}. * * @param bean the annotated service instance * @return true if the at least one callback has been deprocessed */ public boolean deprocessCallbacks(Object bean) { if (bean == null) return false; Class klass = bean.getClass(); Service serviceAnnotation = klass.getAnnotation(Service.class); if (serviceAnnotation == null) return false; boolean result = deprocessListener(bean); result |= deprocessSubscription(bean); return result; } /** * Processes lifecycle methods annotated with {@link PreDestroy}. * * @param bean the annotated service instance * @return true if at least one lifecycle method has been invoked, false otherwise */ @Override public boolean processPreDestroy(Object bean) { return super.processPreDestroy(bean); } private LocalSession findOrCreateLocalSession(Object bean, String name) { LocalSession session = sessions.get(bean); if (session == null) { session = bayeuxServer.newLocalSession(name); LocalSession existing = sessions.putIfAbsent(bean, session); if (existing != null) session = existing; else session.handshake(); } return session; } private boolean processSession(Object bean, LocalSession localSession) { ServerSession serverSession = localSession.getServerSession(); boolean result = false; for (Class c = bean.getClass(); c != Object.class; c = c.getSuperclass()) { Field[] fields = c.getDeclaredFields(); for (Field field : fields) { if (field.getAnnotation(Session.class) != null) { Object value = null; if (field.getType().isAssignableFrom(localSession.getClass())) value = localSession; else if (field.getType().isAssignableFrom(serverSession.getClass())) value = serverSession; if (value != null) { setField(bean, field, value); result = true; logger.debug("Injected {} to field {} on bean {}", new Object[]{value, field, bean}); } } } Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { if (method.getAnnotation(Session.class) != null) { Class[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) { Object value = null; if (parameterTypes[0].isAssignableFrom(localSession.getClass())) value = localSession; else if (parameterTypes[0].isAssignableFrom(serverSession.getClass())) value = serverSession; if (value != null) { invokeMethod(bean, method, value); result = true; logger.debug("Injected {} to method {} on bean {}", new Object[]{value, method, bean}); } } } } } return result; } private boolean processListener(Object bean, LocalSession localSession) { boolean result = false; for (Class c = bean.getClass(); c != Object.class; c = c.getSuperclass()) { Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { Listener listener = method.getAnnotation(Listener.class); if (listener != null) { String[] channels = listener.value(); for (String channel : channels) { bayeuxServer.createIfAbsent(channel); ListenerCallback listenerCallback = new ListenerCallback(localSession, bean, method, channel, listener.receiveOwnPublishes()); bayeuxServer.getChannel(channel).addListener(listenerCallback); List callbacks = listeners.get(bean); if (callbacks == null) { callbacks = new CopyOnWriteArrayList(); List existing = listeners.putIfAbsent(bean, callbacks); if (existing != null) callbacks = existing; } callbacks.add(listenerCallback); result = true; logger.debug("Registered listener for channel {} to method {} on bean {}", new Object[]{channel, method, bean}); } } } } return result; } private boolean deprocessListener(Object bean) { boolean result = false; List callbacks = listeners.get(bean); if (callbacks != null) { for (ListenerCallback callback : callbacks) { ServerChannel channel = bayeuxServer.getChannel(callback.channel); if (channel != null) { channel.removeListener(callback); result = true; } } } return result; } private boolean processSubscription(Object bean, LocalSession localSession) { boolean result = false; for (Class c = bean.getClass(); c != Object.class; c = c.getSuperclass()) { Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { Subscription subscription = method.getAnnotation(Subscription.class); if (subscription != null) { String[] channels = subscription.value(); for (String channel : channels) { SubscriptionCallback subscriptionCallback = new SubscriptionCallback(localSession, bean, method, channel); localSession.getChannel(channel).subscribe(subscriptionCallback); List callbacks = subscribers.get(bean); if (callbacks == null) { callbacks = new CopyOnWriteArrayList(); List existing = subscribers.putIfAbsent(bean, callbacks); if (existing != null) callbacks = existing; } callbacks.add(subscriptionCallback); result = true; logger.debug("Registered subscriber for channel {} to method {} on bean {}", new Object[]{channel, method, bean}); } } } } return result; } private boolean deprocessSubscription(Object bean) { boolean result = false; List callbacks = subscribers.get(bean); if (callbacks != null) { for (SubscriptionCallback callback : callbacks) { callback.localSession.getChannel(callback.channel).unsubscribe(callback); result = true; } } return result; } private static class ListenerCallback implements ServerChannel.MessageListener { private static final Class[] signature = new Class[]{ServerSession.class, ServerMessage.Mutable.class}; private final LocalSession localSession; private final Object target; private final Method method; private final String channel; private final boolean receiveOwnPublishes; private ListenerCallback(LocalSession localSession, Object target, Method method, String channel, boolean receiveOwnPublishes) { Class[] parameters = method.getParameterTypes(); if (!signaturesMatch(parameters, signature)) throw new IllegalArgumentException("Wrong method signature for method " + method); this.localSession = localSession; this.target = target; this.method = method; this.channel = channel; this.receiveOwnPublishes = receiveOwnPublishes; } public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message) { if (from == localSession.getServerSession() && !receiveOwnPublishes) return true; boolean accessible = method.isAccessible(); try { method.setAccessible(true); Object result = method.invoke(target, from, message); return result != Boolean.FALSE; } catch (InvocationTargetException x) { throw new RuntimeException(x.getCause()); } catch (IllegalAccessException x) { throw new RuntimeException(x); } finally { method.setAccessible(accessible); } } } private static class SubscriptionCallback implements ClientSessionChannel.MessageListener { private static final Class[] signature = new Class[]{Message.class}; private final LocalSession localSession; private final Object target; private final Method method; private final String channel; public SubscriptionCallback(LocalSession localSession, Object target, Method method, String channel) { Class[] parameters = method.getParameterTypes(); if (!signaturesMatch(parameters, signature)) throw new IllegalArgumentException("Wrong method signature for method " + method); this.localSession = localSession; this.target = target; this.method = method; this.channel = channel; } public void onMessage(ClientSessionChannel channel, Message message) { boolean accessible = method.isAccessible(); try { method.setAccessible(true); method.invoke(target, message); } catch (InvocationTargetException x) { throw new RuntimeException(x.getCause()); } catch (IllegalAccessException x) { throw new RuntimeException(x); } finally { method.setAccessible(accessible); } } } }