/* * $Id$ * * Copyright 2008 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.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import ome.model.IObject; import ome.model.internal.Details; import ome.model.internal.Permissions; import ome.parameters.Filter; import ome.parameters.Parameters; import ome.system.EventContext; import ome.system.Principal; import ome.system.Roles; import omeis.providers.re.RGBBuffer; import omeis.providers.re.codomain.CodomainMapContext; import omeis.providers.re.data.PlaneDef; import omero.RInt; import omero.RList; import omero.RLong; import omero.RObject; import omero.RString; import omero.RTime; import omero.RType; import omero.ServerError; import omero.api._ServiceInterfaceOperations; import org.springframework.beans.BeansException; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * Checks all servant definitions (see: * ome/services/blitz-servantDefinitions.xml) to guarantee that the RMI and the * Blitz APIs match. * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/894">ticket:894</a> */ public class ApiConsistencyCheck implements BeanPostProcessor { /** * */ public Object postProcessAfterInitialization(Object arg0, String arg1) throws BeansException { if (arg0 instanceof BlitzOnly) { return arg0; // EARLY EXIT! } if (arg0 instanceof Ice.TieBase) { Ice.TieBase tie = (Ice.TieBase) arg0; if (tie.ice_delegate() instanceof BlitzOnly) { return arg0; // EARLY EXIT! } } if (arg0 instanceof _ServiceInterfaceOperations) { final _ServiceInterfaceOperations sio = (_ServiceInterfaceOperations) arg0; final Class ops = si(sio.getClass()); String opsName = ops.getName(); String apiName = opsName.replaceAll("omero", "ome").replaceFirst( "_", "").replace("Operations", ""); Class api; try { api = Class.forName(apiName); } catch (ClassNotFoundException e) { throw new FatalBeanException("No known API interface: " + apiName); } final List<String> differences = new ArrayList<String>(); final Method[] opsMethods = ops.getDeclaredMethods(); final Method[] apiMethods = api.getDeclaredMethods(); final Map<String, Method> opsMap = map(opsMethods); final Map<String, Method> apiMap = map(apiMethods); compareMethodNames(opsMap, apiMap, differences); for (final String name : apiMap.keySet()) { final Method apiMethod = apiMap.get(name); final Method opsMethod = opsMap.get(name); if (opsMethod == null) { differences.add("Missing method: " + name); continue; } final Class[] opsParams = opsMethod.getParameterTypes(); final Class[] apiParams = apiMethod.getParameterTypes(); // Blitz always has one more for the Ice.Current if (opsParams.length - 2 != apiParams.length) { differences.add(String.format( "Native Java method has %d parameters " + "while Blitz method has %d", apiParams.length, opsParams.length)); continue; } // Check actual values for (int i = 0; i < apiParams.length; i++) { Class apiType = apiParams[i]; Class opsType = opsParams[i + 1]; if (!matches(apiType, opsType)) { differences.add(String.format( "Parameter type mismatch in %s: %s <> %s", apiMethod, apiType, opsType)); continue; } } // Now check the return type Class opsReturn = opsMethod.getReturnType(); if (!void.class.equals(opsReturn)) { differences.add("Async calls must return void: " + opsMethod); } Class apiReturn = apiMethod.getReturnType(); Class amdReturn = amdResponse(opsMethod); if (!matches(apiReturn, amdReturn)) { differences.add(String.format( "Return type mismatch in %s: %s <> %s", apiMethod, apiReturn, amdReturn)); } } if (differences.size() > 0) { StringBuilder sb = new StringBuilder(); for (String difference : differences) { sb.append(difference); sb.append("\n"); } throw new ApiConsistencyException(sb.toString(), apiMap, opsMap); } } return arg0; } private void compareMethodNames(final Map<String, Method> opsMap, final Map<String, Method> apiMap, final List<String> differences) { for (String name : opsMap.keySet()) { if (!apiMap.containsKey(name)) { differences.add("Extra method: " + name); } Method opsMethod = opsMap.get(name); List<Class<?>> excs = Arrays.asList(opsMethod.getExceptionTypes()); if (!excs.contains(ServerError.class)) { differences.add("Missing ServerError: " + name); } } } /** * No-op */ public Object postProcessBeforeInitialization(Object arg0, String arg1) throws BeansException { return arg0; } /** * Defines what Class types match. * * @param apiType * @param opsType */ public static boolean matches(Class apiType, Class opsType) { // Check for equality if (apiType == opsType || apiType.equals(opsType)) { return true; } final ApiCheck check = new ApiCheck(apiType, opsType); // // Blacklist. If any of these match, we return false. // if (check.matches(Integer.class, int.class) || check.matches(Long.class, long.class) || check.matches(Double.class, double.class) || check.matches(Float.class, float.class)) { return false; } // // Whitelist. If any one these match, we return true. // if (apiType.isArray() && (opsType.isArray() || Collection.class .isAssignableFrom(opsType))) { return true; } if (check.matches(Collection.class, List.class) || check.matches(Map.class, Map.class) || check.matches(CodomainMapContext.class, omero.romio.CodomainMapContext.class) || check.matches(Date.class, RTime.class) || check.matches(Details.class, omero.model.Details.class) || check.matches(Class.class, String.class) || check.matches(EventContext.class, omero.sys.EventContext.class) || check.matches(Filter.class, omero.sys.Filter.class) || check.matches(Integer.class, RInt.class) || check.matches(IObject.class, omero.model.IObject.class) || check.matches(IObject.class, RObject.class) || check.matches(List.class, RList.class) || check.matches(Long.class, RLong.class) || check.matches(Parameters.class, omero.sys.Parameters.class) || check.matches(PlaneDef.class, omero.romio.PlaneDef.class) || check.matches(Permissions.class, omero.model.Permissions.class) || check.matches(Principal.class, omero.sys.Principal.class) || check.matches(RGBBuffer.class, omero.romio.RGBBuffer.class) || check.matches(Roles.class, omero.sys.Roles.class) || check.matches(String.class, RString.class)) { return true; } if (RType.class.isAssignableFrom(opsType)) { if (Object.class.equals(apiType) || Timestamp.class.isAssignableFrom(apiType)) { return true; } } return false; } /** * Throws a {@link RuntimeException} since if there are two methods with the * same name then there's really no way the comparison can continue. */ private Map<String, Method> map(Method[] methods) { Map<String, Method> map = new HashMap<String, Method>(); for (Method method : methods) { String name = method.getName(); name = name.replaceFirst("_async", ""); if (map.containsKey(name)) { throw new RuntimeException("Method " + name + " contained multiple times in API."); } map.put(name, method); } return map; } /** * Find the direct descendent of * {@link omero.api._ServiceInterfaceOperations} */ private Class si(Class k) { if (!_ServiceInterfaceOperations.class.isAssignableFrom(k)) { return null; } else { Class sc = k.getSuperclass(); if (sc != null) { sc = si(sc); if (sc != null) { return sc; } } for (Class iface : k.getInterfaces()) { if (iface.equals(_ServiceInterfaceOperations.class)) { return k; } else { Class rv = si(iface); if (rv != null) { return rv; } } } } return null; } /** * Checks the parameter type of the ice_response() method of the AMD * callback. */ Class amdResponse(Method m) { // The first parameter is always the AMD class Class amd = m.getParameterTypes()[0]; Method[] methods = amd.getMethods(); Method response = null; for (Method method : methods) { if (method.getName().equals("ice_response")) { if (response != null) { throw new RuntimeException( "2 ice_response() methods found: " + m); } else { response = method; } } } if (response == null) { throw new RuntimeException("No ice_response() method found: " + m); } Class[] responseTypes = response.getParameterTypes(); if (responseTypes.length > 1) { throw new RuntimeException("More than one response type for " + m); } else if (responseTypes.length == 1) { return responseTypes[0]; } else { return void.class; } } } class ApiConsistencyException extends RuntimeException { public ApiConsistencyException(String msg, Map<String, Method> api, Map<String, Method> ops) { super(string(msg, api.values(), ops.values())); } private static String string(String msg, Collection<Method> api, Collection<Method> ops) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append(msg); sb.append("\n"); sb.append("Method mismatch between:\n"); sb.append("native Java:"); sb.append(api.toString()); sb.append("\n"); sb.append("and Blitz:"); sb.append(ops.toString()); return sb.toString(); } } /** * Class to be used as a simple white or black list for checking consistency. To * perform a white list, create an {@link ApiCheck} with the start value of * false. Then * */ class ApiCheck { final Class apiType; final Class opsType; public ApiCheck(Class api, Class ops) { this.apiType = api; this.opsType = ops; } boolean matches(Class apiTest, Class opsTest) { return apiTest.isAssignableFrom(apiType) && opsTest.isAssignableFrom(opsType); } }