/** * 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.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; import javax.ws.rs.DefaultValue; import javax.ws.rs.Encoded; import javax.ws.rs.FormParam; import javax.ws.rs.HeaderParam; import javax.ws.rs.HttpMethod; import javax.ws.rs.MatrixParam; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import org.etk.common.logging.Logger; import org.etk.core.rest.ComponentLifecycleScope; import org.etk.core.rest.FieldInjector; import org.etk.core.rest.impl.ConstructorDescriptor; import org.etk.core.rest.impl.ConstructorDescriptorImpl; import org.etk.core.rest.impl.FieldInjectorImpl; import org.etk.core.rest.impl.header.MediaTypeHelper; import org.etk.core.rest.impl.method.DefaultMethodInvoker; import org.etk.core.rest.impl.method.MethodParameterImpl; import org.etk.core.rest.impl.method.OptionsRequestMethodInvoker; import org.etk.core.rest.impl.method.ParameterHelper; import org.etk.core.rest.impl.uri.UriPattern; import org.etk.core.rest.method.MethodParameter; 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.SubResourceLocatorMap; import org.etk.core.rest.resource.SubResourceMethodDescriptor; import org.etk.core.rest.resource.SubResourceMethodMap; public class AbstractResourceDescriptorImpl implements AbstractResourceDescriptor { /** * Logger. */ private static final Logger LOG = Logger.getLogger(AbstractResourceDescriptorImpl.class); /** * @see PathValue */ private final PathValue path; /** * @see UriPattern */ private final UriPattern uriPattern; /** * Resource class. */ private final Class<?> resourceClass; /** * Sub-resource methods. Sub-resource method has path annotation. * * @see SubResourceMethodDescriptor */ private final SubResourceMethodMap subResourceMethods; /** * Sub-resource locators. Sub-resource locator has path annotation. * * @see SubResourceLocatorDescriptor */ private final SubResourceLocatorMap subResourceLocators; /** * Resource methods. Resource method has not own path annotation. * * @see ResourceMethodDescriptor */ private final ResourceMethodMap<ResourceMethodDescriptor> resourceMethods; /** * Resource class constructors. * * @see ConstructorDescriptor */ private final List<ConstructorDescriptor> constructors; /** * Resource class fields. */ private final List<FieldInjector> fields; /** * Constructs new instance of AbstractResourceDescriptor without path * (sub-resource). * * @param resourceClass resource class */ public AbstractResourceDescriptorImpl(Class<?> resourceClass) { this(resourceClass.getAnnotation(Path.class), resourceClass, ComponentLifecycleScope.PER_REQUEST); } /** * Constructs new instance of AbstractResourceDescriptor without path * (sub-resource). * * @param resource resource instance */ public AbstractResourceDescriptorImpl(Object resource) { this(resource.getClass().getAnnotation(Path.class), resource.getClass(), ComponentLifecycleScope.SINGLETON); } /** * @param path the path value * @param resourceClass resource class * @param scope resource scope * @see ComponentLifecycleScope */ private AbstractResourceDescriptorImpl(Path path, Class<?> resourceClass, ComponentLifecycleScope scope) { if (path != null) { this.path = new PathValue(path.value()); uriPattern = new UriPattern(path.value()); } else { this.path = null; uriPattern = null; } this.resourceClass = resourceClass; this.constructors = new ArrayList<ConstructorDescriptor>(); this.fields = new ArrayList<FieldInjector>(); if (scope == ComponentLifecycleScope.PER_REQUEST) { for (Constructor<?> constructor : resourceClass.getConstructors()) { constructors.add(new ConstructorDescriptorImpl(resourceClass, constructor)); } if (constructors.size() == 0) { String msg = "Not found accepted constructors for resource class " + resourceClass.getName(); throw new RuntimeException(msg); } // Sort constructors in number parameters order if (constructors.size() > 1) { Collections.sort(constructors, ConstructorDescriptorImpl.CONSTRUCTOR_COMPARATOR); } // process field for (java.lang.reflect.Field jfield : resourceClass.getDeclaredFields()) { fields.add(new FieldInjectorImpl(resourceClass, jfield)); } } this.resourceMethods = new ResourceMethodMap<ResourceMethodDescriptor>(); this.subResourceMethods = new SubResourceMethodMap(); this.subResourceLocators = new SubResourceLocatorMap(); processMethods(); } /** * {@inheritDoc} */ public void accept(ResourceDescriptorVisitor visitor) { visitor.visitAbstractResourceDescriptor(this); } /** * {@inheritDoc} */ public List<ConstructorDescriptor> getConstructorDescriptors() { return constructors; } /** * {@inheritDoc} */ public List<FieldInjector> getFieldInjectors() { return fields; } /** * {@inheritDoc} */ public Class<?> getObjectClass() { return resourceClass; } /** * {@inheritDoc} */ public PathValue getPathValue() { return path; } /** * {@inheritDoc} */ public ResourceMethodMap<ResourceMethodDescriptor> getResourceMethods() { return resourceMethods; } /** * {@inheritDoc} */ public SubResourceLocatorMap getSubResourceLocators() { return subResourceLocators; } /** * {@inheritDoc} */ public SubResourceMethodMap getSubResourceMethods() { return subResourceMethods; } /** * {@inheritDoc} */ public UriPattern getUriPattern() { return uriPattern; } /** * {@inheritDoc} */ public boolean isRootResource() { return path != null; } /** * Process method of resource and separate them to three types Resource * Methods, Sub-Resource Methods and Sub-Resource Locators. */ protected void processMethods() { Class<?> resourceClass = getObjectClass(); for (Method method : resourceClass.getDeclaredMethods()) { for (Annotation a : method.getAnnotations()) { Class<?> ac = a.annotationType(); if (!Modifier.isPublic(method.getModifiers()) && (ac == CookieParam.class || ac == Consumes.class || ac == Context.class || ac == DefaultValue.class || ac == Encoded.class || ac == FormParam.class || ac == HeaderParam.class || ac == MatrixParam.class || ac == Path.class || ac == PathParam.class || ac == Produces.class || ac == QueryParam.class || ac.getAnnotation(HttpMethod.class) != null)) { LOG.warn("Non-public method at resource " + toString() + " annotated with JAX-RS annotation: " + a); } } } for (Method method : resourceClass.getMethods()) { Path subPath = getMethodAnnotation(method, resourceClass, Path.class, false); HttpMethod httpMethod = getMethodAnnotation(method, resourceClass, HttpMethod.class, true); if (subPath != null || httpMethod != null) { List<MethodParameter> params = createMethodParametersList(resourceClass, method); if (httpMethod != null) { Produces p = getMethodAnnotation(method, resourceClass, Produces.class, false); if (p == null) p = resourceClass.getAnnotation(Produces.class); // from resource // class List<MediaType> produces = MediaTypeHelper.createProducesList(p); Consumes c = getMethodAnnotation(method, resourceClass, Consumes.class, false); if (c == null) c = resourceClass.getAnnotation(Consumes.class); // from resource // class List<MediaType> consumes = MediaTypeHelper.createConsumesList(c); if (subPath == null) { // resource method ResourceMethodDescriptor res = new ResourceMethodDescriptorImpl(method, httpMethod.value(), params, this, consumes, produces, new DefaultMethodInvoker()); ResourceMethodDescriptor exist = findMethodResourceMediaType(resourceMethods.getList(httpMethod.value()), res.consumes(), res.produces()); if (exist == null) { resourceMethods.add(httpMethod.value(), res); } else { String msg = "Two resource method " + res + " and " + exist + " with the same HTTP method, consumes and produces found."; throw new RuntimeException(msg); } } else { // sub-resource method SubResourceMethodDescriptor subRes = new SubResourceMethodDescriptorImpl(new PathValue(subPath.value()), method, httpMethod.value(), params, this, consumes, produces, new DefaultMethodInvoker()); SubResourceMethodDescriptor exist = null; ResourceMethodMap<SubResourceMethodDescriptor> rmm = (ResourceMethodMap<SubResourceMethodDescriptor>) subResourceMethods.getMethodMap(subRes.getUriPattern()); // rmm is never null, empty map instead List<SubResourceMethodDescriptor> l = rmm.getList(httpMethod.value()); exist = (SubResourceMethodDescriptor) findMethodResourceMediaType(l, subRes.consumes(), subRes.produces()); if (exist == null) { rmm.add(httpMethod.value(), subRes); } else { String msg = "Two sub-resource method " + subRes + " and " + exist + " with the same HTTP method, path, consumes and produces found."; throw new RuntimeException(msg); } } } else { if (subPath != null) { // sub-resource locator SubResourceLocatorDescriptor loc = new SubResourceLocatorDescriptorImpl(new PathValue(subPath.value()), method, params, this, new DefaultMethodInvoker()); if (!subResourceLocators.containsKey(loc.getUriPattern())) { subResourceLocators.put(loc.getUriPattern(), loc); } else { String msg = "Two sub-resource locators " + loc + " and " + subResourceLocators.get(loc.getUriPattern()) + " with the same path found."; throw new RuntimeException(msg); } } } } } int resMethodCount = resourceMethods.size() + subResourceMethods.size() + subResourceLocators.size(); if (resMethodCount == 0) { String msg = "Not found any resource methods, sub-resource methods" + " or sub-resource locators in " + resourceClass.getName(); throw new RuntimeException(msg); } // End method processing. // Start HEAD and OPTIONS resolving, see JAX-RS (JSR-311) specification // section 3.3.5 resolveHeadRequest(); resolveOptionsRequest(); resourceMethods.sort(); subResourceMethods.sort(); // sub-resource locators already sorted } /** * Create list of {@link MethodParameter} . * * @param resourceClass class * @param method See {@link Method} * @return list of {@link MethodParameter} */ protected List<MethodParameter> createMethodParametersList(Class<?> resourceClass, Method method) { Class<?>[] parameterClasses = method.getParameterTypes(); if (parameterClasses.length == 0) return java.util.Collections.emptyList(); Type[] parameterGenTypes = method.getGenericParameterTypes(); Annotation[][] annotations = method.getParameterAnnotations(); List<MethodParameter> params = new ArrayList<MethodParameter>(parameterClasses.length); for (int i = 0; i < parameterClasses.length; i++) { String defaultValue = null; Annotation annotation = null; boolean encoded = false; List<String> allowedAnnotation = ParameterHelper.RESOURCE_METHOD_PARAMETER_ANNOTATIONS; for (Annotation a : annotations[i]) { Class<?> ac = a.annotationType(); if (allowedAnnotation.contains(ac.getName())) { if (annotation == null) { annotation = a; } else { String msg = "JAX-RS annotations on one of method parameters of resource " + toString() + "are equivocality. " + "Annotations: " + annotation + " and " + a + " can't be applied to one parameter."; throw new RuntimeException(msg); } } else if (ac == Encoded.class) { encoded = true; } else if (ac == DefaultValue.class) { defaultValue = ((DefaultValue) a).value(); } else { LOG.warn("Method parameter contains unknown or not valid JAX-RS annotation " + a.toString() + ". It will be ignored."); } } encoded = encoded || resourceClass.getAnnotation(Encoded.class) != null; MethodParameter mp = new MethodParameterImpl(annotation, annotations[i], parameterClasses[i], parameterGenTypes[i], defaultValue, encoded); params.add(mp); } return params; } /** * According to JSR-311: * <p> * On receipt of a HEAD request an implementation MUST either: 1. Call method * annotated with request method designation for HEAD or, if none present, 2. * Call method annotated with a request method designation GET and discard any * returned entity. * </p> */ protected void resolveHeadRequest() { List<ResourceMethodDescriptor> getRes = resourceMethods.get(HttpMethod.GET); if (getRes == null || getRes.size() == 0) return; // nothing to do, there is not 'GET' methods // If there is no methods for 'HEAD' anyway never return null. // Instead null empty List will be returned. List<ResourceMethodDescriptor> headRes = resourceMethods.getList(HttpMethod.HEAD); for (ResourceMethodDescriptor rmd : getRes) { if (findMethodResourceMediaType(headRes, rmd.consumes(), rmd.produces()) == null) headRes.add(new ResourceMethodDescriptorImpl(rmd.getMethod(), HttpMethod.HEAD, rmd.getMethodParameters(), this, rmd.consumes(), rmd.produces(), rmd.getMethodInvoker())); } for (ResourceMethodMap<SubResourceMethodDescriptor> rmm : subResourceMethods.values()) { List<SubResourceMethodDescriptor> getSubres = rmm.get(HttpMethod.GET); if (getSubres == null || getSubres.size() == 0) continue; // nothing to do, there is not 'GET' methods // If there is no methods for 'HEAD' anyway never return null. // Instead null empty List will be returned. List<SubResourceMethodDescriptor> headSubres = rmm.getList(HttpMethod.HEAD); Iterator<SubResourceMethodDescriptor> i = getSubres.iterator(); while (i.hasNext()) { SubResourceMethodDescriptor srmd = (SubResourceMethodDescriptor) i.next(); if (findMethodResourceMediaType(headSubres, srmd.consumes(), srmd.produces()) == null) { headSubres.add(new SubResourceMethodDescriptorImpl(srmd.getPathValue(), srmd.getMethod(), HttpMethod.HEAD, srmd.getMethodParameters(), this, srmd.consumes(), srmd.produces(), new DefaultMethodInvoker())); } } } } /** * According to JSR-311: * <p> * On receipt of a OPTIONS request an implementation MUST either: 1. Call * method annotated with request method designation for OPTIONS or, if none * present, 2. Generate an automatic response using the metadata provided by * the JAX-RS annotations on the matching class and its methods. * </p> */ protected void resolveOptionsRequest() { List<ResourceMethodDescriptor> o = resourceMethods.getList("OPTIONS"); if (o.size() == 0) { List<MethodParameter> mps = Collections.emptyList(); List<MediaType> consumes = MediaTypeHelper.DEFAULT_TYPE_LIST; List<MediaType> produces = new ArrayList<MediaType>(1); produces.add(MediaTypeHelper.WADL_TYPE); o.add(new OptionsRequestResourceMethodDescriptorImpl(null, "OPTIONS", mps, this, consumes, produces, new OptionsRequestMethodInvoker())); } // TODO need process sub-resources ? } /** * Get all method with at least one annotation which has annotation * <i>annotation</i>. It is useful for annotation {@link javax.ws.rs.GET}, * etc. All HTTP method annotations has annotation {@link HttpMethod}. * * @param <T> annotation type * @param m method * @param annotation annotation class * @return list of annotation */ protected <T extends Annotation> T getMetaAnnotation(Method m, Class<T> annotation) { for (Annotation a : m.getAnnotations()) { T endPoint = null; if ((endPoint = a.annotationType().getAnnotation(annotation)) != null) return endPoint; } return null; } /** * Tries to get JAX-RS annotation on method from the root resource class's * superclass or implemented interfaces. * * @param <T> annotation type * @param method method for discovering * @param resourceClass class that contains discovered method * @param annotationClass annotation type what we are looking for * @param metaAnnotation false if annotation should be on method and true in * method should contain annotations that has supplied annotation * @return annotation from class or its ancestor or null if nothing found */ protected <T extends Annotation> T getMethodAnnotation(Method method, Class<?> resourceClass, Class<T> annotationClass, boolean metaAnnotation) { T annotation = null; if (metaAnnotation) annotation = getMetaAnnotation(method, annotationClass); else annotation = method.getAnnotation(annotationClass); if (annotation == null) { Method inhMethod = null; try { inhMethod = resourceClass.getSuperclass().getMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { for (Class<?> intf : resourceClass.getInterfaces()) { try { Method tmp = intf.getMethod(method.getName(), method.getParameterTypes()); if (inhMethod == null) { inhMethod = tmp; } else { String msg = "JAX-RS annotation on method " + inhMethod.getName() + " of resource " + toString() + " is equivocality."; throw new RuntimeException(msg); } } catch (NoSuchMethodException exc) { } } } if (inhMethod != null) { if (metaAnnotation) annotation = getMetaAnnotation(inhMethod, annotationClass); else annotation = inhMethod.getAnnotation(annotationClass); } } return annotation; } /** * Check is collection of {@link ResourceMethodDescriptor} already contains * ResourceMethodDescriptor with the same media types. * * @param rmds {@link Set} of {@link ResourceMethodDescriptor} * @param consumes resource method consumed media type * @param produces resource method produced media type * @return ResourceMethodDescriptor or null if nothing found */ protected <T extends ResourceMethodDescriptor> ResourceMethodDescriptor findMethodResourceMediaType(List<T> rmds, List<MediaType> consumes, List<MediaType> produces) { ResourceMethodDescriptor matched = null; for (T rmd : rmds) { if (rmd.consumes().size() != consumes.size()) return null; if (rmd.produces().size() != produces.size()) return null; for (MediaType c1 : rmd.consumes()) { boolean eq = false; for (MediaType c2 : consumes) { if (c1.equals(c2)) { eq = true; break; } } if (!eq) return null; } for (MediaType p1 : rmd.produces()) { boolean eq = false; for (MediaType p2 : produces) { if (p1.equals(p2)) { eq = true; break; } } if (!eq) return null; } matched = rmd; // matched resource method break; } return matched; } /** * {@inheritDoc} */ @Override public String toString() { StringBuffer sb = new StringBuffer("[ AbstractResourceDescriptorImpl: "); sb.append("path: " + getPathValue()) .append("; isRootResource: " + isRootResource()) .append("; class: " + getObjectClass()) .append(getConstructorDescriptors() + "; ") .append(getFieldInjectors()) .append(" ]"); return sb.toString(); } }