/** * Copyright (C) 2003-2008 eXo Platform SAS. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see<http://www.gnu.org/licenses/>. */ package org.etk.core.rest.impl.resource; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import javax.ws.rs.FormParam; import javax.ws.rs.core.MultivaluedMap; import org.etk.core.rest.FieldInjector; import org.etk.core.rest.FilterDescriptor; import org.etk.core.rest.impl.ConstructorDescriptor; import org.etk.core.rest.impl.ObjectModel; import org.etk.core.rest.impl.ResourceDescriptor; import org.etk.core.rest.method.MethodParameter; import org.etk.core.rest.provider.ProviderDescriptor; import org.etk.core.rest.resource.AbstractResourceDescriptor; import org.etk.core.rest.resource.ResourceDescriptorVisitor; import org.etk.core.rest.resource.ResourceMethodDescriptor; import org.etk.core.rest.resource.ResourceMethodMap; import org.etk.core.rest.resource.SubResourceLocatorDescriptor; import org.etk.core.rest.resource.SubResourceMethodDescriptor; /** * Validate ResourceDescriptors. @see * {@link ResourceDescriptor#accept(ResourceDescriptorVisitor)}. * <p> * Validation Goals: * <li>check number of method parameters without annotation, should be not more * then one at resource method or sub-resource method and no one at sub-resource * locator</li> * <li>if one of parameters at resource method or sub-resource method has * {@link FormParam} annotation then entity type can be only * MultivalueMap<String, String> and nothing other</li> * <li> {@link PathValue#getPath()} can't return empty string, it minds for root * resource classes, sub-resource methods and sub-resource locators can't have * annotation @Path("")</li> * <li>Resource class must contains at least one resource method, sub-resource * method or sub-resource locator</li> * <p> * Non-Goals: * <li>Check does any two resource methods has the same consume and produce * media type. This will be done later in binding cycle</li> * <li>Check does any two sub-resource methods has the same consume and produce * media type and HTTP request method designation. This will be done later in * binding cycle</li> * <li>Check does two sub-resource locators has the same UriPattern</li> * <p> * * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a> * @version $Id: $ */ public class ResourceDescriptorValidator implements ResourceDescriptorVisitor { /** * Visitor instance. */ private static AtomicReference<ResourceDescriptorValidator > instance = new AtomicReference<ResourceDescriptorValidator >(); /** * @return singleton instance of ResourceDescriptorVisitor */ public static ResourceDescriptorValidator getInstance() { ResourceDescriptorValidator t = instance.get(); if (t == null) { instance.compareAndSet(null, new ResourceDescriptorValidator()); t = instance.get(); } return t; } /** * Validate AbstractResourceDescriptor. AbstractResourceDescriptor is a class * which annotated with path annotation then it is root resource, or not * annotated with path then it is sub-resource. Can have also consumes and * produces annotation. Path annotation is required for root resource. * {@inheritDoc} */ public void visitAbstractResourceDescriptor(AbstractResourceDescriptor ard) { if (ard.isRootResource() && ard.getPathValue().getPath().length() == 0) { String msg = "Resource class " + ard.getObjectClass() + " is root resource but path value empty, see javax.ws.rs.Path#value()"; throw new RuntimeException(msg); } checkObjectModel(ard); // check all resource methods for (List<ResourceMethodDescriptor> l : ard.getResourceMethods().values()) { for (ResourceMethodDescriptor rmd : l) rmd.accept(this); } // check all sub-resource methods for (ResourceMethodMap<SubResourceMethodDescriptor> rmm : ard.getSubResourceMethods().values()) { for (List<SubResourceMethodDescriptor> l : rmm.values()) { for (SubResourceMethodDescriptor rmd : l) rmd.accept(this); } } // check all sub-resource locators for (SubResourceLocatorDescriptor loc : ard.getSubResourceLocators().values()) { loc.accept(this); } } /** * Validate ResourceMethodDescriptor. ResourceMethodDescriptor is method in * Resource class which has not path annotation. This method MUST have at * least one annotation (HTTP method, e.g. GET). {@inheritDoc} */ public void visitResourceMethodDescriptor(ResourceMethodDescriptor rmd) { checkMethodParameters(rmd); } /** * Validate SubResourceLocatorDescriptor. SubResourceLocatorDescriptor is a * method which annotated with path annotation and has not HTTP method * annotation. This method can not directly process the request but it can * produces object that will handle the request. {@inheritDoc} */ public void visitSubResourceLocatorDescriptor(SubResourceLocatorDescriptor srld) { if (srld.getPathValue().getPath().length() == 0) { String msg = "Path value is empty for method " + srld.getMethod().getName() + " in resource class " + srld.getParentResource().getObjectClass() + ", see javax.ws.rs.Path#value()"; throw new RuntimeException(msg); } checkMethodParameters(srld); } /** * Validate SubResourceMethodDescriptor. SubResourceMethodDescriptor is a * method which annotated with path annotation and has HTTP method annotation. * This method can process the request directly. {@inheritDoc} */ public void visitSubResourceMethodDescriptor(SubResourceMethodDescriptor srmd) { if (srmd.getPathValue().getPath().length() == 0) { String msg = "Path value is null or empty for method " + srmd.getMethod().getName() + " in resource class " + srmd.getParentResource().getObjectClass() + ", see javax.ws.rs.Path#value()"; throw new RuntimeException(msg); } checkMethodParameters(srmd); } /** * Check method parameter for valid annotations. NOTE If a any method * parameter is annotated with {@link FormParam} then type of entity parameter * must be MultivalueMap<String, String>. * * @param rmd See {@link ResourceMethodDescriptor} */ private static void checkMethodParameters(ResourceMethodDescriptor rmd) { List<MethodParameter> l = rmd.getMethodParameters(); boolean entity = false; boolean form = false; for (int i = 0; i < l.size(); i++) { // Must be only: MatrixParam, QueryParam, PathParam, HeaderParam, // FormParam, CookieParam, Context and only one of it at each parameter MethodParameter mp = l.get(i); if (mp.getAnnotation() == null) { if (!entity) { entity = true; if (form) // form already met then check type of entity checkFormParam(mp.getParameterClass(), mp.getGenericType()); } else { String msg = "Wrong or absent annotation at parameter with index " + i + " at " + rmd.getParentResource().getObjectClass() + "#" + rmd.getMethod().getName(); throw new RuntimeException(msg); } } else { if (mp.getAnnotation().annotationType() == FormParam.class) { form = true; if (entity) // entity already met then check type of entity checkFormParam(mp.getParameterClass(), mp.getGenericType()); } } } } /** * Check does sub-resource locator has required annotation at method * parameters. Sub-resource locator can't has not annotated parameter (entity * parameter). * * @param srld SubResourceLocatorDescriptor */ private static void checkMethodParameters(SubResourceLocatorDescriptor srld) { List<MethodParameter> l = srld.getMethodParameters(); for (int i = 0; i < l.size(); i++) { // Must be only: MatrixParam, QueryParam, PathParam, HeaderParam, // FormParam, CookieParam, Context and only one of it at each parameter MethodParameter mp = l.get(i); if (mp.getAnnotation() == null) { // not allowed to have not annotated parameters in resource locator String msg = "Wrong or absent annotation at parameter with index " + i + " at " + srld.getParentResource().getObjectClass() + "#" + srld.getMethod().getName(); throw new RuntimeException(msg); } } } /** * Check is supplied class MultivaluedMap<String, String>. * * @param clazz class to be checked * @param type generic type * @see #checkGenericType(Type) */ @SuppressWarnings("unchecked") private static void checkFormParam(Class clazz, Type type) { if (MultivaluedMap.class != clazz || !checkGenericType(type)) { String msg = "If a any method parameter is annotated with FormParam then type" + " of entity parameter MUST be MultivalueMap<String, String> or FormEntity"; throw new RuntimeException(msg); } } /** * Check is supplied class parameterized as <String, String>. * * @param type generic type * @return true if type is {@link ParameterizedType} and parameterized * <String, String>, false otherwise */ private static boolean checkGenericType(Type type) { if (type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; Type[] genericTypes = pt.getActualTypeArguments(); if (genericTypes.length == 2) { try { return (String.class == (Class<?>) genericTypes[0]) && (String.class == (Class<?>) genericTypes[1]); } catch (ClassCastException e) { return false; } } } // not parameterized type // TODO must be tolerant for not parameterized type and use string as // default ? return false; } /** * {@inheritDoc} */ public void visitConstructorInjector(ConstructorDescriptor ci) { // currently nothing to do, should be already valid } /** * {@inheritDoc} */ public void visitFieldInjector(FieldInjector fi) { // currently nothing to do, should be already valid } /** * {@inheritDoc} */ public void visitFilterDescriptor(FilterDescriptor fd) { checkObjectModel(fd); } /** * {@inheritDoc} */ public void visitProviderDescriptor(ProviderDescriptor pd) { checkObjectModel(pd); } protected void checkObjectModel(ObjectModel model) { for (ConstructorDescriptor c : model.getConstructorDescriptors()) c.accept(this); for (FieldInjector f : model.getFieldInjectors()) f.accept(this); } }