/*
* Copyright 2013 Jeanfrancois Arcand
*
* 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.atmosphere.cpr.packages;
import org.atmosphere.annotation.Processor;
import org.atmosphere.config.AtmosphereAnnotation;
import org.atmosphere.config.managed.Decoder;
import org.atmosphere.config.managed.Encoder;
import org.atmosphere.config.service.Heartbeat;
import org.atmosphere.config.service.Message;
import org.atmosphere.cpr.AtmosphereFramework;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.Broadcaster;
import org.atmosphere.stomp.annotation.StompEndpoint;
import org.atmosphere.stomp.annotation.StompService;
import org.atmosphere.stomp.handler.StompSendActionAtmosphereHandler;
import org.atmosphere.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* <p>
* This processor handles classes annotated with {@link org.atmosphere.stomp.annotation.StompEndpoint}. Any annotated class should provides several
* methods annotated with {@link org.atmosphere.stomp.annotation.StompService}. Each annotated method should point to a different
* {@link org.atmosphere.stomp.annotation.StompService#destination()}.
* </p>
*
* <p>
* When a method is discovered, an {@link org.atmosphere.stomp.handler.StompSendActionAtmosphereHandler} is associated to it. Moreover, this processor
* creates a mapping for a {@link org.atmosphere.cpr.Broadcaster} which corresponds to the value returned by
* {@link org.atmosphere.stomp.annotation.StompService#destination()}.
* </p>
*
* <p>
* By adding those new {@link org.atmosphere.cpr.AtmosphereHandler handlers} to the {@link AtmosphereFramework},
* the {@link org.atmosphere.stomp.interceptor.FrameInterceptor} will be able to find the appropriate method to invoke when
* reading the {@link org.atmosphere.stomp.protocol.Header#DESTINATION destination} in frames.
* </p>
*
* @author Guillaume DROUET
* @since 0.1
* @version 1.0
*/
@AtmosphereAnnotation(StompEndpoint.class)
public class StompEndpointProcessor implements Processor<Object> {
/**
* The logger.
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* The selected heartbeat method.
*/
private static Method onHeartbeatMethod;
/**
* The object the method belongs to.
*/
private static Object heartbeatHandler;
/**
* <p>
* Invokes the heartbeat method if not {@code null} which has been detected during endpoint detection.
* </p>
*
* @param event the event
*/
public static void invokeOnHeartbeat(final AtmosphereResourceEvent event) {
if (onHeartbeatMethod != null) {
Utils.invoke(heartbeatHandler, onHeartbeatMethod, event);
}
}
/**
* {@inheritDoc}
*/
@Override
public void handle(final AtmosphereFramework framework, final Class<Object> annotatedClass) {
logger.info("Handling {}", annotatedClass.getName());
final Object instance;
// Try to retrieve an instance for the given class
try {
instance = framework.newClassInstance(Object.class, annotatedClass);
} catch (Exception iae) {
logger.warn("Failed to process class annotated with {}", StompEndpoint.class.getName(), iae);
return;
}
final List<Method> list = Arrays.asList(annotatedClass.getDeclaredMethods());
heartbeatHandler = instance;
// Look for heartbeat listener first
// Only one method can be notified
for (final Iterator<Method> it = list.iterator(); it.hasNext() && onHeartbeatMethod == null;) {
onHeartbeatMethod = detectHeartbeat(it.next());
}
// Look for service
for (final Method m : annotatedClass.getDeclaredMethods()) {
detectStompService(framework, m, instance);
}
}
/**
* <p>
* Detects if the given method is annotated with {@link Heartbeat}.
* </p>
*
* @param method the method to inspect
* @return the given method if it can receive heartbeat event, {@code null} otherwise
*/
private Method detectHeartbeat(final Method method) {
logger.debug("Detecting heartbeat annotation on method {}", method.getName());
// Heartbeat listener detected
return (method.isAnnotationPresent(Heartbeat.class)) ? method : null;
}
/**
* <p>
* Detects if the given method is annotated with {@link StompService} and creates the appropriate handler.
* </p>
*
* @param framework the framework instance
* @param method the method to inspect
* @param instance the annotated class instance
*/
private void detectStompService(final AtmosphereFramework framework,
final Method method,
final Object instance) {
logger.debug("Detecting annotation on method {}", method.getName());
// Stomp service detected
if (method.isAnnotationPresent(StompService.class)) {
// The destination will be the broadcaster mapping
final String destination = method.getAnnotation(StompService.class).destination();
if (destination == null || destination.isEmpty()) {
logger.warn("The destination in {} must not be empty", StompService.class.getName(), new IllegalStateException());
} else {
// Optional message annotation with encoders and decoders
final Decoder<String, Object> decoder;
final Encoder<Object, String> encoder;
if (method.isAnnotationPresent(Message.class)) {
try {
// TODO: support many encoders/decoders ?
final Message message = method.getAnnotation(Message.class);
decoder = framework.newClassInstance(Decoder.class, message.decoders()[0]);
encoder = framework.newClassInstance(Encoder.class, message.encoders()[0]);
} catch (Exception iae) {
logger.warn("Failed to process annotation {}", Message.class.getName(), iae);
return;
}
} else {
decoder = null;
encoder = null;
}
// Now add to the framework the handler for the declared destination
try {
final Broadcaster b = framework.getBroadcasterFactory().get(destination);
final AtmosphereHandler ah = new StompSendActionAtmosphereHandler(instance, method, encoder, decoder, b, onHeartbeatMethod);
framework.addAtmosphereHandler(destination, ah);
} catch (IllegalArgumentException iae) {
logger.warn("Method {} has not the required signature to be a {}", method.getName(), iae);
}
}
}
}
}