/* * Copyright 2011 cruxframework.org * * 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.cruxframework.crux.core.server.rest.core.registry; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cruxframework.crux.core.server.rest.core.UriBuilder; import org.cruxframework.crux.core.server.rest.core.dispatch.CacheInfo; import org.cruxframework.crux.core.server.rest.core.dispatch.ResourceMethod; import org.cruxframework.crux.core.server.rest.spi.HttpRequest; import org.cruxframework.crux.core.server.rest.spi.InternalServerErrorException; import org.cruxframework.crux.core.server.rest.util.HttpMethodHelper; import org.cruxframework.crux.core.server.rest.util.InvalidRestMethod; import org.cruxframework.crux.core.shared.rest.annotation.Path; import org.cruxframework.crux.core.shared.rest.annotation.StateValidationModel; /** * * @author Thiago da Rosa de Bustamante * */ public class ResourceRegistry { private static final Log logger = LogFactory.getLog(ResourceRegistry.class); private static final ResourceRegistry instance = new ResourceRegistry(); private static final Lock lock = new ReentrantLock(); private static boolean initialized = false; protected int size; protected RootSegment rootSegment = new RootSegment(); /** * Singleton constructor */ private ResourceRegistry() {} /** * Singleton accessor * @return */ public static ResourceRegistry getInstance() { return instance; } public RootSegment getRoot() { if (!initialized) { initialize(); } return rootSegment; } /** * Number of endpoints registered * * @return */ public int getSize() { if (!initialized) { initialize(); } return size; } /** * Find a resource to invoke on * * @return */ public ResourceMethod getResourceMethod(HttpRequest request) { if (!initialized) { initialize(); } List<String> matchedUris = request.getUri().getMatchedURIs(false); if (matchedUris == null || matchedUris.size() == 0) { return rootSegment.matchRoot(request); } // resource location String currentUri = request.getUri().getMatchedURIs(false).get(0); return rootSegment.matchRoot(request, currentUri.length()); } /** * * @param clazz * @param base */ protected void addResource(Class<?> clazz, String base) { Set<String> restMethodNames = new HashSet<String>(); Map<String, List<RestMethodRegistrationInfo>> validRestMethods = new HashMap<String, List<RestMethodRegistrationInfo>>(); for (Method method : clazz.getMethods()) { if (!method.isSynthetic()) { RestMethodRegistrationInfo methodRegistrationInfo = null; try { methodRegistrationInfo = processMethod(base, clazz, method, restMethodNames); } catch (Exception e) { throw new InternalServerErrorException("Error to processMethod: " + method.toString(), "Can not execute requested service", e); } if (methodRegistrationInfo != null) { List<RestMethodRegistrationInfo> methodsForPath = validRestMethods.get(methodRegistrationInfo.pathExpression); if (methodsForPath == null) { methodsForPath = new ArrayList<RestMethodRegistrationInfo>(); validRestMethods.put(methodRegistrationInfo.pathExpression, methodsForPath); } methodsForPath.add(methodRegistrationInfo); } } } checkConditionalWriteMethods(validRestMethods); createCorsAllowedMethodsList(validRestMethods); } private void createCorsAllowedMethodsList(Map<String, List<RestMethodRegistrationInfo>> validRestMethods) { for (Entry<String, List<RestMethodRegistrationInfo>> entry : validRestMethods.entrySet()) { List<RestMethodRegistrationInfo> methods = entry.getValue(); if (methods.size() > 0) { List<String> allowedMethodsForPath = new ArrayList<String>(); for (RestMethodRegistrationInfo methodInfo : methods) { if (methodInfo.invoker.supportsCors()) { allowedMethodsForPath.add(methodInfo.invoker.getHttpMethod()); } } if (allowedMethodsForPath.size() > 0) { for (RestMethodRegistrationInfo methodInfo : methods) { if (methodInfo.invoker.supportsCors()) { methodInfo.invoker.setCorsAllowedMethods(allowedMethodsForPath); } } } } } } private void checkConditionalWriteMethods(Map<String, List<RestMethodRegistrationInfo>> validRestMethods) { for (Entry<String, List<RestMethodRegistrationInfo>> entry : validRestMethods.entrySet()) { List<RestMethodRegistrationInfo> methods = entry.getValue(); if (methods.size() > 0) { for (RestMethodRegistrationInfo methodInfo : methods) { StateValidationModel stateValidationModel = HttpMethodHelper.getStateValidationModel(methodInfo.invoker.getMethod()); if (stateValidationModel != null && !stateValidationModel.equals(StateValidationModel.NO_VALIDATE)) { if (!ensureReaderMethod(methods)) { logger.error(" Method: " + methodInfo.invoker.getResourceClass().getName() + "." + methodInfo.invoker.getMethod().getName() + "() " + "uses a stateValidationModel. It requires a valid GET method to provides the resource for validation."); } } } } } } private boolean ensureReaderMethod(List<RestMethodRegistrationInfo> methods) { for (RestMethodRegistrationInfo methodInfo : methods) { CacheInfo cacheInfo = HttpMethodHelper.getCacheInfoForGET(methodInfo.invoker.getMethod()); if (cacheInfo != null) { if (!cacheInfo.isCacheEnabled()) { //for cacheable resources, eTag generation is already enabled. methodInfo.invoker.forceEtagGeneration(); } return true; } } return false; } /** * * @param classes * @param base */ protected void addResource(Class<?>[] classes, String base) { for (Class<?> clazz : classes) { addResource(clazz, base); } } /** * * @param base * @param clazz * @param method * @param restMethodNames */ protected RestMethodRegistrationInfo processMethod(String base, Class<?> clazz, Method method, Set<String> restMethodNames) { if (method != null) { Path path = method.getAnnotation(Path.class); String httpMethod = null; try { httpMethod = HttpMethodHelper.getHttpMethod(method.getAnnotations()); } catch (InvalidRestMethod e) { logger.error("Invalid Method: " + method.getDeclaringClass().getName() + "." + method.getName() + "().", e); } boolean pathPresent = path != null; boolean restAnnotationPresent = pathPresent || (httpMethod != null); UriBuilder builder = new UriBuilder(); if (base != null) builder.path(base); if (clazz.isAnnotationPresent(Path.class)) { builder.path(clazz); } if (path != null) { builder.path(method); } String pathExpression = builder.getPath(); if (pathExpression == null) { pathExpression = ""; } if (restAnnotationPresent && !Modifier.isPublic(method.getModifiers())) { logger.error("Rest annotations found at non-public method: " + method.getDeclaringClass().getName() + "." + method.getName() + "(); Only public methods may be exposed as resource methods."); } else if (httpMethod != null) { if (restMethodNames.contains(method.getName())) { logger.error("Overloaded rest method: " + method.getDeclaringClass().getName() + "." + method.getName() + " found. It is not supported for Crux REST services."); } else { ResourceMethod invoker = new ResourceMethod(clazz, method, httpMethod); rootSegment.addPath(pathExpression, invoker); restMethodNames.add(method.getName()); size++; return new RestMethodRegistrationInfo(pathExpression, invoker); } } else { if (restAnnotationPresent) { logger.error("Method: " + method.getDeclaringClass().getName() + "." + method.getName() + "() declares rest annotations, but it does not inform the methods it must handle. Use one of @PUT, @POST, @GET or @DELETE."); } else if (logger.isDebugEnabled()) { logger.debug("Method: " + method.getDeclaringClass().getName() + "." + method.getName() + "() ignored. It is not a rest method."); } } } return null; } /** * */ public static void initialize() { if (initialized) { return; } try { lock.lock(); if (initialized) { return; } initializeRegistry(); } finally { lock.unlock(); } } private static void initializeRegistry() { RestServiceFactory serviceScanner = RestServiceFactoryInitializer.getServiceFactory(); Iterator<String> restServices = serviceScanner.iterateRestServices(); while (restServices.hasNext()) { String service = restServices.next(); try { Class<?> serviceClass = serviceScanner.getServiceClass(service); instance.addResource(serviceClass, ""); } catch (Exception e) { logger.error("Error initializing rest service class for service ["+service+"]", e); } } initialized = true; } private static class RestMethodRegistrationInfo { private String pathExpression; private ResourceMethod invoker; private RestMethodRegistrationInfo(String pathExpression, ResourceMethod invoker) { this.pathExpression = pathExpression; this.invoker = invoker; } } }