package edu.harvard.econcs.turkserver.client;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import edu.harvard.econcs.turkserver.QuizMaterials;
import edu.harvard.econcs.turkserver.api.*;
public class ClientAnnotationManager<C> {
final Logger logger = Logger.getLogger(this.getClass().getSimpleName());
C clientBean;
private Method startExperiment;
private Method startRound;
private Method timeLimit;
private Method finishExperiment;
private Method clientError;
private List<Method> broadcasts;
private List<Method> services;
public ClientAnnotationManager(ClientController client, Class<C> clientClass) throws Exception {
Constructor<C> cons = null;
try {
// TODO allow lobbyController as well
cons = clientClass.getConstructor(ClientController.class);
} catch (NoSuchMethodException e) {
logger.severe("Error: " + clientClass + " must have a public, one-argument constructor that accepts a ClientController\n");
throw e;
}
ExperimentClient e = clientClass.getAnnotation(ExperimentClient.class);
if( e == null )
logger.warning("Class " + clientClass.toString() +
" does not have @ExperimentClient annotation, but trying callbacks anyway");
broadcasts = new LinkedList<Method>();
services = new LinkedList<Method>();
boolean processed = false;
for (Class<?> c = clientClass; c != Object.class; c = c.getSuperclass())
{
Method[] methods = c.getDeclaredMethods();
for (Method method : methods)
{
processed |= processStartExperiment(method);
processed |= processStartRound(method);
processed |= processTimeLimit(method);
processed |= processFinishExperiment(method);
processed |= processClientError(method);
processed |= processBroadcast(method);
processed |= processService(method);
}
}
if( !processed )
logger.warning("Didn't find any methods in " + clientClass.toString());
clientBean = cons.newInstance(client);
}
public C getClientBean() {
return clientBean;
}
private boolean processStartExperiment(Method method) {
Class<? extends Annotation> annot = StartExperiment.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
if (method.getParameterTypes().length > 0)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have no parameters");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
if( startExperiment != null ) {
logger.warning("Already found a " + annot.toString() + " method, ignoring " + method.toString());
return false;
}
startExperiment = method;
return true;
}
private boolean processStartRound(Method method) {
Class<? extends Annotation> annot = StartRound.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || !int.class.isAssignableFrom(types[0]) )
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must accept an int");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
if( startRound != null ) {
logger.warning("Already found a " + annot.toString() + " method, ignoring " + method.toString());
return false;
}
startRound = method;
return true;
}
private boolean processTimeLimit(Method method) {
Class<? extends Annotation> annot = TimeLimit.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
if (method.getParameterTypes().length > 0)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have no parameters");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
if( timeLimit != null ) {
logger.warning("Already found a " + annot.toString() + " method, ignoring " + method.toString());
return false;
}
timeLimit = method;
return true;
}
private boolean processFinishExperiment(Method method) {
Class<? extends Annotation> annot = FinishExperiment.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
if (method.getParameterTypes().length > 0)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have no parameters");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
if( finishExperiment != null ) {
logger.warning("Already found a " + annot.toString() + " method, ignoring " + method.toString());
return false;
}
finishExperiment = method;
return true;
}
private boolean processClientError(Method method) {
Class<? extends Annotation> annot = ClientError.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || !String.class.isAssignableFrom(types[0]) )
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must accept a string");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
if( clientError != null ) {
logger.warning("Already found a " + annot.toString() + " method, ignoring " + method.toString());
return false;
}
clientError = method;
return true;
}
private boolean processBroadcast(Method method) {
Class<? extends Annotation> annot = BroadcastMessage.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || !Map.class.isAssignableFrom(types[0]) )
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must accept a Map<String, Object>");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
broadcasts.add(method);
return true;
}
private boolean processService(Method method) {
Class<? extends Annotation> annot = ServiceMessage.class;
if( method.getAnnotation(annot) == null ) return false;
if (method.getReturnType() != Void.TYPE)
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must have void return type");
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || !Map.class.isAssignableFrom(types[0]) )
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must accept a Map<String, Object>");
if (Modifier.isStatic(method.getModifiers()))
throw new RuntimeException("Invalid " + annot.toString() + " method " + method + ": it must not be static");
services.add(method);
return true;
}
private Object invokeMethod(Method m, Object... args) {
boolean accessible = m.isAccessible();
try {
// TODO robust-ify the accessibility issue here
m.setAccessible(true);
return m.invoke(clientBean, args);
} catch (Exception e) {
logger.warning("Exception invoking " + m + " on " + clientBean.getClass().toString() + ", ignoring");
e.printStackTrace();
return null;
} finally {
m.setAccessible(accessible);
}
}
public void triggerQuiz(QuizMaterials qm) {
// TODO Auto-generated method stub
}
public void triggerRequestUsername() {
// TODO Auto-generated method stub
}
public void triggerJoinLobby() {
// TODO Auto-generated method stub
}
public void triggerUpdateLobby(Map<String, Object> data) {
// TODO Auto-generated method stub
}
public void triggerStartExperiment() {
if( startExperiment != null ) invokeMethod(startExperiment);
}
public void triggerStartRound(int n) {
if( startRound != null ) invokeMethod(startRound, n);
}
public void triggerFinishExperiment() {
if( finishExperiment != null ) invokeMethod(finishExperiment);
}
public void triggerTimeLimit() {
if( timeLimit != null ) invokeMethod(timeLimit);
}
public void triggerClientError(String msg) {
if( clientError != null ) invokeMethod(clientError, msg);
}
public void deliverBroadcast(Map<String, Object> message) {
for( Method m : broadcasts ) {
BroadcastMessage ann = m.getAnnotation(BroadcastMessage.class);
if( ann.key().length > 0 ) {
String key = ann.key()[0];
if (!message.containsKey(key) ) continue;
if ( ann.value().length > 0 && !message.get(key).equals(ann.value()[0])) continue;
}
invokeMethod(m, message);
}
}
public void deliverService(Map<String, Object> message) {
for( Method m : services ) {
ServiceMessage ann = m.getAnnotation(ServiceMessage.class);
if( ann.key().length > 0 ) {
String key = ann.key()[0];
if (!message.containsKey(key) ) continue;
if ( ann.value().length > 0 && !message.get(key).equals(ann.value()[0])) continue;
}
invokeMethod(m, message);
}
}
}