/*
* Copyright 2007 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.blitz.util;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import ome.api.ServiceInterface;
import ome.system.OmeroContext;
import omero.ServerError;
import omero.util.IceMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
/**
* {@link Method}-cache primed either with an {@link ServiceInterface} instance
* or with a {@link Class} with generic type {@link ServiceInterface}. Actual
* invocation happens via
* {@link #invoke(Object, Ice.Current, IceMapper, Object[])}
*
* No reference is held to the initial priming argument in
* {@link IceMethodInvoker#IceMethodInvoker(ServiceInterface, OmeroContext)}
* just the class.
*
* MAPPING RULES:
* <ul>
* <li>Method names exact</li>
* <li>Collections of the same type only (no arrays)</li>
* <li>Primitives use Ice primitives (long, int, bool,...)</li>
* <li>Primitive wrappers all use RTypes (RLong, RInt, RBool,...)</li>
* </ul>
*
* It is also possible to have this class not handle mapping arguments and
* return values by passing a return value mapper.
*
* Future:
* <ul>
* <li>Currently ignoring
* {@link ome.annotations.NotNull} annotations
* </li>
* </ul>
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta2
*/
public class IceMethodInvoker {
private static Logger log = LoggerFactory.getLogger(IceMethodInvoker.class);
static class Info {
Method method;
Class<?>[] params;
Class<?> retType;
int[] switches;
}
private final static Map<Class<?>, Map<String, Info>> staticmap = new HashMap<Class<?>, Map<String, Info>>();
private final Class<?> serviceClass;
private OmeroContext ctx;
/**
* Create an {@link IceMethodInvoker} instance using the {@link Class} of
* the passed argument to call
* {@link IceMethodInvoker#IceMethodInvoker(Class, OmeroContext)}.
*
* @param srv
* A Non-null {@link ServiceInterface} instance.
* @param context
* The active {@link OmeroContext} instance.
*/
public IceMethodInvoker(ServiceInterface srv, OmeroContext context) {
this(srv.getClass(), context);
}
/**
* Creates an {@link IceMethodInvoker} instance by using reflection on the
* {@link Class} argument. All information is cached internally in a static
* {@link #staticmap map} if the given service class argument has not
* already been cached.
*
* @param <S>
* A type which subclasses {@link ServiceInterface}
* @param context
* A non-null {@link ServiceInterface} {@link Class}
*/
public <S extends ServiceInterface> IceMethodInvoker(Class<S> k,
OmeroContext context) {
this.serviceClass = k;
this.ctx = context;
if (!staticmap.containsKey(this.serviceClass)) {
synchronized (staticmap) {
// Re-check in case already added
if (!staticmap.containsKey(this.serviceClass)) {
Map<String, Info> map = new HashMap<String, Info>();
Method[] ms = this.serviceClass.getMethods();
for (Method m : ms) {
Info i = new Info();
i.method = m;
i.params = m.getParameterTypes();
i.retType = m.getReturnType();
map.put(m.getName(), i);
}
staticmap.put(this.serviceClass, map);
}
}
}
}
Map<String, Info> map() {
return staticmap.get(serviceClass);
}
/**
* Checks for a void return type, which is needed to know what type of
* ice_response() method to invoke.
*/
public boolean isVoid(Ice.Current current) {
Info info = map().get(current.operation);
return info.retType.equals(void.class);
}
/**
* Calls the method named in {@link Ice.Current#operation} with the
* arguments provided mapped via the {@link IceMapper} instance. The return
* value or any method which is thrown is equally mapped and returned.
* Exceptions are handled by
* {@link IceMapper#handleException(Throwable, OmeroContext)}.
*
* @param obj
* Instance for the call to
* {@link Method#invoke(Object, Object[])}. Can be null if this
* is a static call.
* @param current
* The current Ice operation. Non-null.
* @param mapper
* A non-null mapper.
* @param args
* The proper number of arguments for the method specified in
* current.
* @return Either the return value of the invocation, or the exception if
* one was thrown.
*/
public Object invoke(Object obj, Ice.Current current, IceMapper mapper,
Object... args) throws Ice.UserException {
Assert.notNull(mapper, "IceMapper cannot be null");
Assert.notNull(current, "Ice.Current cannot be null");
final Info info = map().get(current.operation);
if (info == null) {
throw new IllegalArgumentException("Unknown method:"
+ current.operation);
}
final Object[] objs = arguments(current, mapper, info, args);
Object retVal = null;
try {
retVal = info.method.invoke(obj, objs);
} catch (Throwable t) {
throw mapper.handleException(t, ctx);
}
// Handling case of generics (e.g. Search.next())
// in which case we cannot properly handle the mapping.
Class<?> retType = info.retType;
if (retType == Object.class && retVal != null) {
retType = retVal.getClass();
}
// If we have a returnValueMapper then it's that objects responsibility
// to convert the return value, otherwise this class must do it.
if (mapper.canMapReturnValue()) {
return mapper.mapReturnValue(retVal);
} else {
return mapper.handleOutput(retType, retVal);
}
}
/**
* Maps input parameters from omero.* to ome.* types if the
* returnValueMapper is non-null. If it is null, then it is assumed that the
* responsibility falls to this class.
*/
private Object[] arguments(Ice.Current current, IceMapper mapper,
Info info, Object... args) throws ServerError {
if (mapper.canMapReturnValue()) {
return args; // EARLY EXIT!
}
// Alias
Class<?>[] params = info.params;
if (params.length != args.length) {
throw new IllegalArgumentException("Must provide " + params.length
+ " arguments for " + current.operation + " not "
+ args.length);
}
// The Mapped argument parameters to be passed to the
// ServiceInterface instance.
Object[] objs = new Object[params.length];
// be sure to use our own types
for (int i = 0; i < params.length; i++) {
Class<?> p = params[i];
Object arg = args[i];
objs[i] = mapper.handleInput(p, arg);
// This check duplicates what should be in handleInput
// if (null != objs[i] && !isPrimitive(p) && // FIXME need way
// to check autoboxing.
// !p.isAssignableFrom(objs[i].getClass())) {
// throw new IllegalStateException(String.format(
// "Cannot assign %s to %s",objs[i],p));
// }
}
return objs;
}
/** For testing the cached method. */
public Method getMethod(String name) {
return map().get(name).method;
}
}