Java tutorial
/* * Aipo is a groupware program developed by TOWN, Inc. * Copyright (C) 2004-2015 TOWN, Inc. * http://www.aipo.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.apache.shindig.protocol; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.apache.shindig.auth.SecurityToken; import org.apache.shindig.common.util.ImmediateFuture; import org.apache.shindig.protocol.conversion.BeanConverter; import org.apache.shindig.protocol.conversion.BeanJsonConverter; import org.apache.shindig.protocol.multipart.FormDataItem; import org.json.JSONException; import org.json.JSONObject; import com.aipo.container.protocol.AipoErrorCode; import com.aipo.container.protocol.AipoProtocolException; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; /** * @see DefaultHandlerRegistry */ public class DefaultHandlerRegistry implements HandlerRegistry { private static final Logger LOG = Logger.getLogger(DefaultHandlerRegistry.class.getName()); // Map service - > method -> { handler, ...} private final Map<String, Map<String, SortedSet<RestPath>>> serviceMethodPathMap = Maps.newHashMap(); private final Map<String, RpcInvocationHandler> rpcOperations = Maps.newHashMap(); private final Injector injector; private final BeanJsonConverter beanJsonConverter; private final HandlerExecutionListener executionListener; /** * Creates a dispatcher with the specified handler classes * * @param injector * Used to create instance if handler is a Class */ @Inject public DefaultHandlerRegistry(Injector injector, BeanJsonConverter beanJsonConverter, HandlerExecutionListener executionListener) { this.injector = injector; this.beanJsonConverter = beanJsonConverter; this.executionListener = executionListener; } /** * Add handlers to the registry * * @param handlers */ @Override public void addHandlers(Set<Object> handlers) { for (final Object handler : handlers) { Class<?> handlerType; Provider<?> handlerProvider; if (handler instanceof Class<?>) { handlerType = (Class<?>) handler; handlerProvider = injector.getProvider(handlerType); } else { handlerType = handler.getClass(); handlerProvider = new Provider<Object>() { @Override public Object get() { return handler; } }; } Preconditions.checkState(handlerType.isAnnotationPresent(Service.class), "Attempt to bind unannotated service implementation %s", handlerType.getName()); Service service = handlerType.getAnnotation(Service.class); for (Method m : handlerType.getMethods()) { if (m.isAnnotationPresent(Operation.class)) { Operation op = m.getAnnotation(Operation.class); createRpcHandler(handlerProvider, service, op, m); createRestHandler(handlerProvider, service, op, m); } } } } /** * Get an RPC handler */ @Override public RpcHandler getRpcHandler(JSONObject rpc) { try { String key = rpc.getString("method"); RpcInvocationHandler rpcHandler = rpcOperations.get(key); if (rpcHandler == null) { return new ErrorRpcHandler(new AipoProtocolException( AipoErrorCode.NOT_IMPLEMENTED.customMessage("The method " + key + " is not implemented"))); } return new RpcInvocationWrapper(rpcHandler, rpc); } catch (JSONException je) { return new ErrorRpcHandler(new AipoProtocolException( AipoErrorCode.BAD_REQUEST.customMessage("No method requested in RPC"))); } } /** * Get a REST request handler */ @Override public RestHandler getRestHandler(String path, String method) { method = method.toUpperCase(); if (path != null) { if (path.startsWith("/")) { path = path.substring(1); } String[] pathParts = StringUtils.splitPreserveAllTokens(path, '/'); Map<String, SortedSet<RestPath>> methods = serviceMethodPathMap.get(pathParts[0]); if (methods != null) { SortedSet<RestPath> paths = methods.get(method); if (paths != null) { for (RestPath restPath : paths) { RestHandler handler = restPath.accept(pathParts); if (handler != null) { return handler; } } } } } return new ErrorRestHandler(new AipoProtocolException( AipoErrorCode.NOT_IMPLEMENTED.customMessage("No service defined for path " + path))); } @Override public Set<String> getSupportedRestServices() { Set<String> result = Sets.newTreeSet(); for (Map<String, SortedSet<RestPath>> methods : serviceMethodPathMap.values()) { for (Map.Entry<String, SortedSet<RestPath>> method : methods.entrySet()) { for (RestPath path : method.getValue()) { result.add(method.getKey() + ' ' + path.operationPath); } } } return Collections.unmodifiableSet(result); } @Override public Set<String> getSupportedRpcServices() { return Collections.unmodifiableSet(rpcOperations.keySet()); } private void createRestHandler(Provider<?> handlerProvider, Service service, Operation op, Method m) { try { MethodCaller methodCaller = new MethodCaller(m, true); String opName = m.getName(); if (!StringUtils.isEmpty(op.name())) { opName = op.name(); } RestInvocationHandler restHandler = new RestInvocationHandler(op, methodCaller, handlerProvider, beanJsonConverter, new ExecutionListenerWrapper(service.name(), opName, executionListener)); String serviceName = service.name(); Map<String, SortedSet<RestPath>> methods = serviceMethodPathMap.get(serviceName); if (methods == null) { methods = Maps.newHashMap(); serviceMethodPathMap.put(serviceName, methods); } for (String httpMethod : op.httpMethods()) { if (!StringUtils.isEmpty(httpMethod)) { httpMethod = httpMethod.toUpperCase(); SortedSet<RestPath> sortedSet = methods.get(httpMethod); if (sortedSet == null) { sortedSet = Sets.newTreeSet(); methods.put(httpMethod, sortedSet); } if (StringUtils.isEmpty(op.path())) { sortedSet.add(new RestPath('/' + serviceName + service.path(), restHandler)); } else { // Use the standard service name and constant prefix as the key sortedSet.add(new RestPath('/' + serviceName + op.path(), restHandler)); } } } } catch (NoSuchMethodException nme) { LOG.log(Level.INFO, "No REST binding for " + service.name() + '.' + m.getName()); } } private void createRpcHandler(Provider<?> handlerProvider, Service service, Operation op, Method m) { try { MethodCaller methodCaller = new MethodCaller(m, false); String opName = m.getName(); // Use the override if its defined if (op.name().length() > 0) { opName = op.name(); } RpcInvocationHandler rpcHandler = new RpcInvocationHandler(methodCaller, handlerProvider, beanJsonConverter, new ExecutionListenerWrapper(service.name(), opName, executionListener)); rpcOperations.put(service.name() + '.' + opName, rpcHandler); } catch (NoSuchMethodException nme) { LOG.log(Level.INFO, "No RPC binding for " + service.name() + '.' + m.getName()); } } /** * Utility wrapper for the HandlerExecutionListener */ private static class ExecutionListenerWrapper { final String service; final String operation; final HandlerExecutionListener listener; ExecutionListenerWrapper(String service, String operation, HandlerExecutionListener listener) { this.service = service; this.operation = operation; this.listener = listener; } private void executing(RequestItem req) { listener.executing(service, operation, req); } private void executed(RequestItem req) { listener.executed(service, operation, req); } } /** * Proxy binding for an RPC operation. We allow binding to methods that return * non-Future types by wrapping them in ImmediateFuture. */ static final class RpcInvocationHandler { private final Provider<?> handlerProvider; private final BeanJsonConverter beanJsonConverter; private final ExecutionListenerWrapper listener; private final MethodCaller methodCaller; private RpcInvocationHandler(MethodCaller methodCaller, Provider<?> handlerProvider, BeanJsonConverter beanJsonConverter, ExecutionListenerWrapper listener) { this.handlerProvider = handlerProvider; this.beanJsonConverter = beanJsonConverter; this.listener = listener; this.methodCaller = methodCaller; } public Future<?> execute(JSONObject rpc, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter) { RequestItem item; try { JSONObject params = rpc.has("params") ? (JSONObject) rpc.get("params") : new JSONObject(); item = methodCaller.getRpcRequestItem(params, formItems, token, beanJsonConverter); } catch (Exception e) { return ImmediateFuture.errorInstance(e); } try { listener.executing(item); return methodCaller.call(handlerProvider.get(), item); } catch (Exception e) { return ImmediateFuture.errorInstance(e); } finally { listener.executed(item); } } } /** * Encapsulate the dispatch of a single RPC */ static final class RpcInvocationWrapper implements RpcHandler { final RpcInvocationHandler handler; final JSONObject rpc; RpcInvocationWrapper(RpcInvocationHandler handler, JSONObject rpc) { this.handler = handler; this.rpc = rpc; } @Override public Future<?> execute(Map<String, FormDataItem> formItems, SecurityToken st, BeanConverter converter) { return handler.execute(rpc, formItems, st, converter); } } /** * Proxy binding for a REST operation. We allow binding to methods that return * non-Future types by wrapping them in ImmediateFuture. */ static final class RestInvocationHandler { final Provider<?> handlerProvider; final Operation operation; final BeanJsonConverter beanJsonConverter; final ExecutionListenerWrapper listener; final MethodCaller methodCaller; private RestInvocationHandler(Operation operation, MethodCaller methodCaller, Provider<?> handlerProvider, BeanJsonConverter beanJsonConverter, ExecutionListenerWrapper listener) { this.operation = operation; this.handlerProvider = handlerProvider; this.beanJsonConverter = beanJsonConverter; this.listener = listener; this.methodCaller = methodCaller; } public Future<?> execute(Map<String, String[]> parameters, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter) { RequestItem item; try { item = methodCaller.getRestRequestItem(parameters, formItems, token, converter, beanJsonConverter); } catch (Exception e) { return ImmediateFuture.errorInstance(e); } try { listener.executing(item); return methodCaller.call(handlerProvider.get(), item); } catch (Exception e) { return ImmediateFuture.errorInstance(e); } finally { listener.executed(item); } } } /** * Encapsulate the dispatch of a single REST call. Augment the executed * parameters with those extracted from the path */ static class RestInvocationWrapper implements RestHandler { RestInvocationHandler handler; Map<String, String[]> pathParams; RestInvocationWrapper(Map<String, String[]> pathParams, RestInvocationHandler handler) { this.pathParams = pathParams; this.handler = handler; } @Override public Future<?> execute(Map<String, String[]> parameters, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter) { pathParams.putAll(parameters); return handler.execute(pathParams, formItems, token, converter); } } /** * Calls methods annotated with {@link Operation} and appropriately translates * RequestItem to the actual input class of the method. */ private static class MethodCaller { /** Type of object to create for this method, or null if takes no args */ private Class<?> inputClass; /** Constructors for request item class that will be used */ private final Constructor<?> restRequestItemConstructor; private final Constructor<?> rpcRequestItemConstructor; /** The method */ private final Method method; /** * Create information needed to call a method * * @param method * The method * @param isRest * True if REST method (affects which RequestItem constructor to * return) * @throws NoSuchMethodException */ public MethodCaller(Method method, boolean isRest) throws NoSuchMethodException { this.method = method; inputClass = method.getParameterTypes().length > 0 ? method.getParameterTypes()[0] : null; // Methods that need RequestItem interface should automatically get a // BaseRequestItem if (RequestItem.class.equals(inputClass)) { inputClass = BaseRequestItem.class; } boolean inputIsRequestItem = (inputClass != null) && RequestItem.class.isAssignableFrom(inputClass); Class<?> requestItemType = inputIsRequestItem ? inputClass : BaseRequestItem.class; restRequestItemConstructor = requestItemType.getConstructor(Map.class, Map.class, SecurityToken.class, BeanConverter.class, BeanJsonConverter.class); rpcRequestItemConstructor = requestItemType.getConstructor(JSONObject.class, Map.class, SecurityToken.class, BeanConverter.class, BeanJsonConverter.class); } public RequestItem getRestRequestItem(Map<String, String[]> params, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter) { return getRequestItem(params, formItems, token, converter, jsonConverter, restRequestItemConstructor); } public RequestItem getRpcRequestItem(JSONObject params, Map<String, FormDataItem> formItems, SecurityToken token, BeanJsonConverter converter) { return getRequestItem(params, formItems, token, converter, converter, rpcRequestItemConstructor); } private RequestItem getRequestItem(Map<String, String[]> params, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter, Constructor<?> constructor) { try { return (BaseRequestItem) constructor.newInstance(params, formItems, token, converter, jsonConverter); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } private RequestItem getRequestItem(JSONObject params, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter, Constructor<?> constructor) { try { return (BaseRequestItem) constructor.newInstance(params, formItems, token, converter, jsonConverter); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } public Future<?> call(Object handler, RequestItem item) { try { Object result; if (inputClass == null) { result = method.invoke(handler); } else if (RequestItem.class.isAssignableFrom(inputClass)) { result = method.invoke(handler, item); } else { result = method.invoke(handler, item.getTypedRequest(inputClass)); } if (result instanceof Future<?>) { return (Future<?>) result; } return ImmediateFuture.newInstance(result); } catch (IllegalAccessException e) { return ImmediateFuture.errorInstance(e); } catch (InvocationTargetException e) { // Unwrap the internal exception return ImmediateFuture.errorInstance(e.getTargetException()); } } } /** * Standard REST handler to wrap errors */ private static final class ErrorRestHandler implements RestHandler { private final ProtocolException error; public ErrorRestHandler(ProtocolException error) { this.error = error; } @Override public Future<?> execute(Map<String, String[]> parameters, Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter) { return ImmediateFuture.errorInstance(error); } } /** * Standard RPC handler to wrap errors */ private static final class ErrorRpcHandler implements RpcHandler { private final ProtocolException error; public ErrorRpcHandler(ProtocolException error) { this.error = error; } @Override public Future<?> execute(Map<String, FormDataItem> formItems, SecurityToken token, BeanConverter converter) { return ImmediateFuture.errorInstance(error); } } /** * Path matching and parameter extraction for REST. */ static class RestPath implements Comparable<RestPath> { enum PartType { CONST, SINGULAR_PARAM, PLURAL_PARAM } static class Part { String partName; PartType type; Part(String partName, PartType type) { this.partName = partName; this.type = type; } } final String operationPath; final List<Part> parts; final RestInvocationHandler handler; final int constCount; final int lastConstIndex; public RestPath(String path, RestInvocationHandler handler) { int tmpConstCount = 0; int tmpConstIndex = -1; this.operationPath = path; String[] partArr = StringUtils.split(path.substring(1), '/'); parts = Lists.newArrayList(); for (int i = 0; i < partArr.length; i++) { String part = partArr[i]; if (part.startsWith("{")) { if (part.endsWith("}+")) { parts.add(new Part(part.substring(1, part.length() - 2), PartType.PLURAL_PARAM)); } else if (part.endsWith("}")) { parts.add(new Part(part.substring(1, part.length() - 1), PartType.SINGULAR_PARAM)); } else { throw new IllegalStateException("Invalid REST path part format " + part); } } else { parts.add(new Part(part, PartType.CONST)); tmpConstCount++; tmpConstIndex = i; } } constCount = tmpConstCount; lastConstIndex = tmpConstIndex; this.handler = handler; } /** * See if this Rest path is a match for the requested path Requested path is * offset by 1 as it includes service name * * @return A handler with the path parameters decoded, null if not a match * for the path */ public RestInvocationWrapper accept(String[] requestPathParts) { if (constCount > 0) { if (lastConstIndex >= requestPathParts.length) { // Last required constant match is not possible with // this request return null; } for (int i = 0; i <= lastConstIndex; i++) { if (parts.get(i).type == PartType.CONST && !parts.get(i).partName.equals(requestPathParts[i])) { // Constant part does not match request return null; } } } // All constant parts matched, extract the parameters Map<String, String[]> parsedParams = Maps.newHashMap(); for (int i = 0; i < Math.min(requestPathParts.length, parts.size()); i++) { if (parts.get(i).type == PartType.SINGULAR_PARAM) { if (requestPathParts[i].indexOf(',') != -1) { throw new AipoProtocolException(AipoErrorCode.BAD_REQUEST .customMessage("Cannot expect plural value " + requestPathParts[i] + " for singular field " + parts.get(i) + " for path " + operationPath)); } parsedParams.put(parts.get(i).partName, new String[] { requestPathParts[i] }); } else if (parts.get(i).type == PartType.PLURAL_PARAM) { parsedParams.put(parts.get(i).partName, StringUtils.splitPreserveAllTokens(requestPathParts[i], ',')); } } return new RestInvocationWrapper(parsedParams, handler); } @Override public boolean equals(Object other) { if (other instanceof RestPath) { RestPath that = (RestPath) other; return (this.constCount == that.constCount && this.lastConstIndex == that.lastConstIndex && Objects.equal(this.operationPath, that.operationPath)); } return false; } @Override public int hashCode() { return this.constCount ^ this.lastConstIndex ^ operationPath.hashCode(); } /** * Rank based on the number of consant parts they accept, where the constant * parts occur and lexical ordering. */ @Override public int compareTo(RestPath other) { // Rank first by number of constant elements in the path int result = other.constCount - this.constCount; if (result == 0) { // Rank second by the position of the last constant element // (lower index is better) result = this.lastConstIndex - other.lastConstIndex; } if (result == 0) { // Rank lastly by lexical order result = this.operationPath.compareTo(other.operationPath); } return result; } } }