http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/244cc21a/org.apache.juneau.server/src/main/java/org/apache/juneau/server/RestServlet.java ---------------------------------------------------------------------- diff --git a/org.apache.juneau.server/src/main/java/org/apache/juneau/server/RestServlet.java b/org.apache.juneau.server/src/main/java/org/apache/juneau/server/RestServlet.java new file mode 100755 index 0000000..9b5bbb8 --- /dev/null +++ b/org.apache.juneau.server/src/main/java/org/apache/juneau/server/RestServlet.java @@ -0,0 +1,2795 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you 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.apache.juneau.server; + +import static java.lang.String.*; +import static java.util.logging.Level.*; +import static javax.servlet.http.HttpServletResponse.*; +import static org.apache.juneau.internal.ArrayUtils.*; +import static org.apache.juneau.internal.ClassUtils.*; +import static org.apache.juneau.serializer.SerializerContext.*; +import static org.apache.juneau.server.RestServlet.ParamType.*; +import static org.apache.juneau.server.RestServletContext.*; +import static org.apache.juneau.server.annotation.Inherit.*; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.lang.reflect.Method; +import java.nio.charset.*; +import java.text.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.*; + +import javax.activation.*; +import javax.servlet.*; +import javax.servlet.http.*; + +import org.apache.juneau.*; +import org.apache.juneau.encoders.*; +import org.apache.juneau.encoders.Encoder; +import org.apache.juneau.ini.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.parser.ParseException; +import org.apache.juneau.serializer.*; +import org.apache.juneau.server.annotation.*; +import org.apache.juneau.server.annotation.Properties; +import org.apache.juneau.server.annotation.Var; +import org.apache.juneau.server.labels.*; +import org.apache.juneau.server.response.*; +import org.apache.juneau.server.vars.*; +import org.apache.juneau.svl.*; +import org.apache.juneau.svl.vars.*; +import org.apache.juneau.urlencoding.*; +import org.apache.juneau.utils.*; + +/** + * Servlet implementation of a REST resource. + * <p> + * Refer to <a class='doclink' href='package-summary.html#TOC'>REST Servlet API</a> for information about using this class. + * </p> + * + * @author jbognar + */ +@SuppressWarnings({"rawtypes","hiding"}) +public abstract class RestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + static final SortedMap<String,Charset> availableCharsets = new TreeMap<String,Charset>(String.CASE_INSENSITIVE_ORDER); + static { + availableCharsets.putAll(Charset.availableCharsets()); + } + // Map of HTTP method names (e.g. GET/PUT/...) to ResourceMethod implementations for it. Populated during resource initialization. + private final Map<String,ResourceMethod> restMethods = new LinkedHashMap<String,ResourceMethod>(); + + // The list of all @RestMethod annotated methods in the order they appear in the class. + private final Map<String,MethodMeta> javaRestMethods = new LinkedHashMap<String,MethodMeta>(); + + // Child resources of this resource defined through getX() methods on this class. + private final Map<String,RestServlet> childResources = new LinkedHashMap<String,RestServlet>(); + + private RestServlet parentResource; + + private ServletConfig servletConfig; + private volatile boolean isInitialized = false; + private Exception initException; // Exception thrown by init() method (cached so it can be thrown on all subsequent requests). + private JuneauLogger logger; + private MessageBundle msgs; // NLS messages. + + private Map<Integer,Integer> stackTraceHashes = new HashMap<Integer,Integer>(); + private String path; + + private LinkedHashMap<Class<?>,RestResource> restResourceAnnotationsChildFirst, restResourceAnnotationsParentFirst; + + private UrlEncodingSerializer urlEncodingSerializer; + private UrlEncodingParser urlEncodingParser; + private ObjectMap properties; + private RestGuard[] guards; + private Class<?>[] transforms; + private RestConverter[] converters; + private TreeMap<String,String> defaultRequestHeaders; + private Map<String,Object> defaultResponseHeaders; + private EncoderGroup encoders; + private SerializerGroup serializers; + private ParserGroup parsers; + private MimetypesFileTypeMap mimetypesFileTypeMap; + private BeanContext beanContext; + private VarResolver varResolver; + private String label="", description=""; + private Map<String,byte[]> resourceStreams = new ConcurrentHashMap<String,byte[]>(); + private Map<String,String> resourceStrings = new ConcurrentHashMap<String,String>(); + private ConfigFile configFile, resolvingConfigFile; + private String configPath; + private StreamResource styleSheet, favIcon; + private Map<String,String> staticFilesMap; + private String[] staticFilesPrefixes; + private ResponseHandler[] responseHandlers; + private String clientVersionHeader = ""; + + RestServletContext context; + + // In-memory cache of images and stylesheets in the org.apache.juneau.server.htdocs package. + private Map<String,StreamResource> staticFilesCache = new ConcurrentHashMap<String,StreamResource>(); + + // The following code block is executed before the constructor is called to + // allow the config file to be accessed during object creation. + // e.g. private String myConfig = getConfig().getString("myConfig"); + { + varResolver = createVarResolver(); + + // @RestResource annotations from bottom to top. + restResourceAnnotationsChildFirst = ReflectionUtils.findAnnotationsMap(RestResource.class, getClass()); + + // @RestResource annotations from top to bottom. + restResourceAnnotationsParentFirst = CollectionUtils.reverse(restResourceAnnotationsChildFirst); + + for (RestResource r : restResourceAnnotationsParentFirst.values()) { + if (! r.config().isEmpty()) + configPath = r.config(); + } + + try { + configFile = createConfigFile(); + varResolver.setContextObject(ConfigFileVar.SESSION_config, configFile); + } catch (IOException e) { + this.initException = e; + } + } + + @Override /* Servlet */ + public synchronized void init(ServletConfig servletConfig) throws ServletException { + try { + log(FINE, "Servlet {0} init called.", getClass().getName()); + this.servletConfig = servletConfig; + + if (isInitialized) + return; + + super.init(servletConfig); + + // Find resource resource bundle location. + for (Map.Entry<Class<?>,RestResource> e : restResourceAnnotationsChildFirst.entrySet()) { + Class<?> c = e.getKey(); + RestResource r = e.getValue(); + if (! r.messages().isEmpty()) { + if (msgs == null) + msgs = new MessageBundle(c, r.messages()); + else + msgs.addSearchPath(c, r.messages()); + } + if (label.isEmpty()) + label = r.label(); + if (description.isEmpty()) + description = r.description(); + if (clientVersionHeader.isEmpty()) + clientVersionHeader = r.clientVersionHeader(); + } + if (msgs == null) + msgs = new MessageBundle(this.getClass(), ""); + if (clientVersionHeader.isEmpty()) + clientVersionHeader = "X-Client-Version"; + + styleSheet = createStyleSheet(); + favIcon = createFavIcon(); + staticFilesMap = Collections.unmodifiableMap(createStaticFilesMap()); + staticFilesPrefixes = staticFilesMap.keySet().toArray(new String[0]); + + properties = createProperties(); + transforms = createTransforms(); + context = ContextFactory.create().setProperties(properties).getContext(RestServletContext.class); + beanContext = createBeanContext(properties, transforms); + urlEncodingSerializer = createUrlEncodingSerializer(properties, transforms).lock(); + urlEncodingParser = createUrlEncodingParser(properties, transforms).lock(); + serializers = createSerializers(properties, transforms).lock(); + parsers = createParsers(properties, transforms).lock(); + converters = createConverters(properties); + encoders = createEncoders(properties); + guards = createGuards(properties); + mimetypesFileTypeMap = createMimetypesFileTypeMap(properties); + defaultRequestHeaders = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER); + defaultRequestHeaders.putAll(createDefaultRequestHeaders(properties)); + defaultResponseHeaders = createDefaultResponseHeaders(properties); + responseHandlers = createResponseHandlers(properties); + + // Discover the @RestMethod methods available on the resource. + List<String> methodsFound = new LinkedList<String>(); // Temporary to help debug transient duplicate method issue. + for (java.lang.reflect.Method method : this.getClass().getMethods()) { + if (method.isAnnotationPresent(RestMethod.class)) { + RestMethod a = method.getAnnotation(RestMethod.class); + methodsFound.add(method.getName() + "," + a.name() + "," + a.path()); + try { + if (! Modifier.isPublic(method.getModifiers())) + throw new RestServletException("@RestMethod method {0}.{1} must be defined as public.", this.getClass().getName(), method.getName()); + + MethodMeta sm = new MethodMeta(method); + javaRestMethods.put(method.getName(), sm); + ResourceMethod rm = restMethods.get(sm.httpMethod); + if (rm == null) + restMethods.put(sm.httpMethod, sm); + else if (rm instanceof MultiMethod) + ((MultiMethod)rm).addSimpleMethod(sm); + else + restMethods.put(sm.httpMethod, new MultiMethod((MethodMeta)rm, sm)); + } catch (RestServletException e) { + throw new RestServletException("Problem occurred trying to serialize methods on class {0}, methods={1}", this.getClass().getName(), JsonSerializer.DEFAULT_LAX.serialize(methodsFound)).initCause(e); + } + } + } + + for (ResourceMethod m : restMethods.values()) + m.complete(); + + // Discover the child resources. + childResources.putAll(createChildrenMap()); + + for (RestServlet child : childResources.values()) + child.init(servletConfig); + + varResolver.addVars( + LocalizationVar.class, + RequestAttrVar.class, + RequestParamVar.class, + RequestVar.class, + SerializedRequestAttrVar.class, + SerializedRequestParamVar.class, + ServletInitParamVar.class, + UrlEncodeVar.class + ); + + } catch (RestException e) { + // Thrown RestExceptions are simply caught and rethrown on subsequent calls to service(). + initException = e; + log(SEVERE, e, "Servlet init error on class ''{0}''", getClass().getName()); + label = String.valueOf(initException.getLocalizedMessage()); + } catch (ServletException e) { + initException = e; + log(SEVERE, e, "Servlet init error on class ''{0}''", getClass().getName()); + label = String.valueOf(initException.getLocalizedMessage()); + throw e; + } catch (Exception e) { + initException = e; + log(SEVERE, e, "Servlet init error on class ''{0}''", getClass().getName()); + label = String.valueOf(initException.getLocalizedMessage()); + throw new ServletException(e); + } catch (Throwable e) { + initException = new Exception(e); + log(SEVERE, e, "Servlet init error on class ''{0}''", getClass().getName()); + label = String.valueOf(initException.getLocalizedMessage()); + throw new ServletException(e); + } finally { + isInitialized = true; + } + } + + //-------------------------------------------------------------------------------- + // Initialization methods + //-------------------------------------------------------------------------------- + + /** + * Creates the child resources of this resource. + * <p> + * Default implementation calls {@link #createChildren()} and uses the {@link RestResource#path() @RestResource.path()} annotation + * on each child to identify the subpath for the resource which become the keys in this map. + * It then calls the {@link #setParent(RestServlet)} method on the child resource. + * </p> + * <p> + * Subclasses can override this method to programatically create child resources + * without using the {@link RestResource#children() @RestResource.children()} annotation. + * When overridding this method, you are responsible for calling {@link #setParent(RestServlet)} on the + * child resources. + * </p> + * + * @return The new mutable list of child resource instances. + * @throws Exception If an error occurred during servlet instantiation. + */ + protected Map<String,RestServlet> createChildrenMap() throws Exception { + Map<String,RestServlet> m = new LinkedHashMap<String,RestServlet>(); + for (RestServlet r : createChildren()) { + r.setParent(this); + String p = r.findPath(); + if (p == null) + throw new RestServletException("Child resource ''{0}'' does not define a ''@RestResource.path'' attribute.", r.getClass().getName()); + m.put(p, r); + } + return m; + } + + /** + * Creates instances of child resources for this servlet. + * <p> + * Default implementation uses the {@link RestResource#children() @RestResource.children()} annotation to identify and + * instantiate children. + * </p> + * <p> + * Subclasses can override this method to programatically create child resources + * without using the {@link RestResource#children() @RestResource.children()} annotation. + * </p> + * + * @return The new mutable list of child resource instances. + * @throws Exception If an error occurred during servlet instantiation. + */ + protected List<RestServlet> createChildren() throws Exception { + List<RestServlet> l = new LinkedList<RestServlet>(); + for (Class<?> c : getChildClasses()) { + if (isParentClass(RestServlet.class, c)) + l.add((RestServlet)c.newInstance()); + else + l.add(resolveChild(c)); + } + return l; + } + + /** + * Programmatic equivalent to the {@link RestResource#children() @RestResource.children()} annotation. + * <p> + * Subclasses can override this method to provide customized list of child resources. + * (e.g. different children based on values specified in the config file). + * </p> + * <p> + * Default implementation simply returns the value from the {@link RestResource#children() @RestResource.children()} annotation. + * </p> + * + * @return The new mutable list of child resource instances. + * @throws Exception If an error occurred during servlet instantiation. + */ + protected Class<?>[] getChildClasses() throws Exception { + List<Class<?>> l = new ArrayList<Class<?>>(); + List<RestResource> rr = ReflectionUtils.findAnnotations(RestResource.class, getClass()); + for (RestResource r : rr) + l.addAll(Arrays.asList(r.children())); + return l.toArray(new Class<?>[l.size()]); + } + + /** + * Creates the class-level properties associated with this servlet. + * <p> + * Subclasses can override this method to provide their own class-level properties for this servlet, typically + * by calling <code><jk>super</jk>.createProperties()</code> and appending to the map. + * However, in most cases, the existing set of properties can be added to by overridding {@link #getProperties()} + * and appending to the map returned by <code><jk>super</jk>.getProperties()</code> + * </p> + * <p> + * By default, the map returned by this method contains the following: + * </p> + * <ul class='spaced-list'> + * <li>Servlet-init parameters. + * <li>{@link RestResource#properties()} annotations in parent-to-child order. + * <li>{@link SerializerContext#SERIALIZER_relativeUriBase} from {@link ServletConfig#getServletContext()}. + * </ul> + * + * @return The resource properties as an {@link ObjectMap}. + */ + protected ObjectMap createProperties() { + ObjectMap m = new ObjectMap(); + + ServletContext ctx = servletConfig.getServletContext(); + + // Workaround for bug in Jetty that causes context path to always end in "null". + String ctxPath = ctx.getContextPath(); + if (ctxPath.endsWith("null")) + ctxPath = ctxPath.substring(0, ctxPath.length()-4); + m.put(SERIALIZER_relativeUriBase, ctxPath); + + // Get the initialization parameters. + for (Enumeration ep = servletConfig.getInitParameterNames(); ep.hasMoreElements();) { + String p = (String)ep.nextElement(); + String initParam = servletConfig.getInitParameter(p); + m.put(p, initParam); + } + + // Properties are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) + for (Property p : r.properties()) + m.append(getVarResolver().resolve(p.name()), getVarResolver().resolve(p.value())); + + return m; + } + + /** + * Creates the class-level POJO transforms associated with this servlet. + * <p> + * Subclasses can override this method to provide their own class-level POJO transforms for this servlet. + * <p> + * By default, returns the transforms specified through the {@link RestResource#transforms() @RestResource.transforms()} annotation in child-to-parent order. + * (i.e. transforms will be applied in child-to-parent order with child annotations overriding parent annotations when + * the same transforms are applied). + * + * @return The new set of transforms associated with this servet. + */ + protected Class<?>[] createTransforms() { + List<Class<?>> l = new LinkedList<Class<?>>(); + + // Transforms are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsChildFirst.values()) + for (Class c : r.transforms()) + l.add(c); + + return l.toArray(new Class<?>[l.size()]); + } + + /** + * Creates the {@link BeanContext} object used for parsing path variables and header values. + * <p> + * Subclasses can override this method to provide their own specialized bean context. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @param transforms Servlet-level transforms returned by {@link #createTransforms()}. + * @return The new bean context. + * @throws Exception If bean context not be constructed for any reason. + */ + protected BeanContext createBeanContext(ObjectMap properties, Class<?>[] transforms) throws Exception { + return ContextFactory.create().addTransforms(transforms).setProperties(properties).getBeanContext(); + } + + /** + * Creates the URL-encoding serializer used for serializing object passed to {@link Redirect}. + * <p> + * Subclasses can override this method to provide their own specialized serializer. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @param transforms Servlet-level transforms returned by {@link #createTransforms()}. + * @return The new URL-Encoding serializer. + * @throws Exception If the serializer could not be constructed for any reason. + */ + protected UrlEncodingSerializer createUrlEncodingSerializer(ObjectMap properties, Class<?>[] transforms) throws Exception { + return new UrlEncodingSerializer().setProperties(properties).addTransforms(transforms); + } + + /** + * Creates the URL-encoding parser used for parsing URL query parameters. + * <p> + * Subclasses can override this method to provide their own specialized parser. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @param transforms Servlet-level transforms returned by {@link #createTransforms()}. + * @return The new URL-Encoding parser. + * @throws Exception If the parser could not be constructed for any reason. + */ + protected UrlEncodingParser createUrlEncodingParser(ObjectMap properties, Class<?>[] transforms) throws Exception { + return new UrlEncodingParser().setProperties(properties).addTransforms(transforms); + } + + /** + * Creates the serializer group containing serializers used for serializing output POJOs in HTTP responses. + * <p> + * Subclasses can override this method to provide their own set of serializers for this servlet. + * They can do this by either creating a new {@link SerializerGroup} from scratch, or appending to the + * group returned by <code><jk>super</jk>.createSerializers()</code>. + * <p> + * By default, returns the serializers defined through {@link RestResource#serializers() @RestResource.serializers()} on this class + * and all parent classes. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @param transforms Servlet-level transforms returned by {@link #createTransforms()}. + * @return The group of serializers. + * @throws Exception If serializer group could not be constructed for any reason. + */ + protected SerializerGroup createSerializers(ObjectMap properties, Class<?>[] transforms) throws Exception { + SerializerGroup g = new SerializerGroup(); + + // Serializers are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) + for (Class<? extends Serializer> c : reverse(r.serializers())) + try { + g.append(c); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate Serializer ''{0}''", c.getSimpleName()).initCause(e); + } + + g.setProperties(properties); + g.addTransforms(transforms); + return g; + } + + /** + * Creates the parser group containing parsers used for parsing input into POJOs from HTTP requests. + * <p> + * Subclasses can override this method to provide their own set of parsers for this servlet. + * They can do this by either creating a new {@link ParserGroup} from scratch, or appending to the + * group returned by <code><jk>super</jk>.createParsers()</code>. + * <p> + * By default, returns the parsers defined through {@link RestResource#parsers() @RestResource.parsers()} on this class + * and all parent classes. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @param transforms Servlet-level transforms returned by {@link #createTransforms()}. + * @return The group of parsers. + * @throws Exception If parser group could not be constructed for any reason. + */ + protected ParserGroup createParsers(ObjectMap properties, Class<?>[] transforms) throws Exception { + ParserGroup g = new ParserGroup(); + + // Parsers are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) + for (Class<? extends Parser> p : reverse(r.parsers())) + try { + g.append(p); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate Parser ''{0}''", p.getSimpleName()).initCause(e); + } + + g.setProperties(properties); + g.addTransforms(transforms); + return g; + } + + /** + * Creates the class-level converters associated with this servlet. + * <p> + * Subclasses can override this method to provide their own class-level converters for this servlet. + * <p> + * By default, returns the converters specified through the {@link RestResource#converters() @RestResource.converters()} annotation in child-to-parent order. + * (e.g. converters on children will be called before converters on parents). + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new set of transforms associated with this servet. + * @throws RestServletException + */ + protected RestConverter[] createConverters(ObjectMap properties) throws RestServletException { + List<RestConverter> l = new LinkedList<RestConverter>(); + + // Converters are loaded in child-to-parent order. + for (RestResource r : restResourceAnnotationsChildFirst.values()) + for (Class<? extends RestConverter> c : r.converters()) + try { + l.add(c.newInstance()); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate RestConverter ''{0}''", c.getSimpleName()).initCause(e); + } + + return l.toArray(new RestConverter[l.size()]); + } + + /** + * Creates the {@link EncoderGroup} for this servlet for handling various encoding schemes. + * <p> + * Subclasses can override this method to provide their own encoder group, typically by + * appending to the group returned by <code><jk>super</jk>.createEncoders()</code>. + * <p> + * By default, returns a group containing {@link IdentityEncoder#INSTANCE} and all encoders + * specified through {@link RestResource#encoders() @RestResource.encoders()} annotations in parent-to-child order. + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new encoder group associated with this servet. + * @throws RestServletException + */ + protected EncoderGroup createEncoders(ObjectMap properties) throws RestServletException { + EncoderGroup g = new EncoderGroup().append(IdentityEncoder.INSTANCE); + + // Encoders are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) + for (Class<? extends Encoder> c : reverse(r.encoders())) + try { + g.append(c); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate Encoder ''{0}''", c.getSimpleName()).initCause(e); + } + + return g; + } + + /** + * Creates the class-level guards associated with this servlet. + * <p> + * Subclasses can override this method to provide their own class-level guards for this servlet. + * <p> + * By default, returns the guards specified through the {@link RestResource#guards() @RestResource.guards()} annotation in child-to-parent order. + * (i.e. guards on children will be called before guards on parents). + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new set of guards associated with this servet. + * @throws RestServletException + */ + protected RestGuard[] createGuards(ObjectMap properties) throws RestServletException { + List<RestGuard> l = new LinkedList<RestGuard>(); + + // Guards are loaded in child-to-parent order. + for (RestResource r : restResourceAnnotationsChildFirst.values()) + for (Class<? extends RestGuard> c : reverse(r.guards())) + try { + l.add(c.newInstance()); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate RestGuard ''{0}''", c.getSimpleName()).initCause(e); + } + + return l.toArray(new RestGuard[l.size()]); + } + + /** + * Creates an instance of {@link MimetypesFileTypeMap} that is used to determine + * the media types of static files. + * <p> + * Subclasses can override this method to provide their own mappings, or augment the existing + * map by appending to <code><jk>super</jk>.createMimetypesFileTypeMap()</code> + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return A new reusable MIME-types map. + */ + protected MimetypesFileTypeMap createMimetypesFileTypeMap(ObjectMap properties) { + MimetypesFileTypeMap m = new MimetypesFileTypeMap(); + m.addMimeTypes("text/css css CSS"); + m.addMimeTypes("text/html html htm HTML"); + m.addMimeTypes("text/plain txt text TXT"); + m.addMimeTypes("application/javascript js"); + m.addMimeTypes("image/png png"); + m.addMimeTypes("image/gif gif"); + m.addMimeTypes("application/xml xml XML"); + m.addMimeTypes("application/json json JSON"); + return m; + } + + /** + * Creates the set of default request headers for this servlet. + * <p> + * Default request headers are default values for when HTTP requests do not specify a header value. + * For example, you can specify a default value for <code>Accept</code> if a request does not specify that header value. + * <p> + * Subclasses can override this method to provide their own class-level default request headers for this servlet. + * <p> + * By default, returns the default request headers specified through the {@link RestResource#defaultRequestHeaders() @RestResource.defaultRequestHeaders()} + * annotation in parent-to-child order. + * (e.g. headers defined on children will override the same headers defined on parents). + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new set of default request headers associated with this servet. + * @throws RestServletException + */ + protected Map<String,String> createDefaultRequestHeaders(ObjectMap properties) throws RestServletException { + Map<String,String> m = new HashMap<String,String>(); + + // Headers are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) { + for (String s : r.defaultRequestHeaders()) { + String[] h = parseHeader(s); + if (h == null) + throw new RestServletException("Invalid default request header specified: ''{0}''. Must be in the format: ''Header-Name: header-value''", s); + m.put(h[0], h[1]); + } + } + + return m; + } + + /** + * Creates the set of default response headers for this servlet. + * <p> + * Default response headers are headers that will be appended to all responses if those headers have not already been + * set on the response object. + * <p> + * Subclasses can override this method to provide their own class-level default response headers for this servlet. + * <p> + * By default, returns the default response headers specified through the {@link RestResource#defaultResponseHeaders() @RestResource.defaultResponseHeaders()} + * annotation in parent-to-child order. + * (e.g. headers defined on children will override the same headers defined on parents). + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new set of default response headers associated with this servet. + * @throws RestServletException + */ + protected Map<String,Object> createDefaultResponseHeaders(ObjectMap properties) throws RestServletException { + Map<String,Object> m = new LinkedHashMap<String,Object>(); + + // Headers are loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) { + for (String s : r.defaultResponseHeaders()) { + String[] h = parseHeader(s); + if (h == null) + throw new RestServletException("Invalid default response header specified: ''{0}''. Must be in the format: ''Header-Name: header-value''", s); + m.put(h[0], h[1]); + } + } + + return m; + } + + /** + * Creates the class-level response handlers associated with this servlet. + * <p> + * Subclasses can override this method to provide their own class-level response handlers for this servlet. + * <p> + * By default, returns the handlers specified through the {@link RestResource#responseHandlers() @RestResource.responseHandlers()} + * annotation in parent-to-child order. + * (e.g. handlers on children will be called before handlers on parents). + * + * @param properties Servlet-level properties returned by {@link #createProperties()}. + * @return The new set of response handlers associated with this servet. + * @throws RestException + */ + protected ResponseHandler[] createResponseHandlers(ObjectMap properties) throws RestException { + List<ResponseHandler> l = new LinkedList<ResponseHandler>(); + + // Loaded in parent-to-child order to allow overrides. + for (RestResource r : restResourceAnnotationsParentFirst.values()) + for (Class<? extends ResponseHandler> c : r.responseHandlers()) + try { + l.add(c.newInstance()); + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + + // Add the default handlers. + l.add(new StreamableHandler()); + l.add(new WritableHandler()); + l.add(new ReaderHandler()); + l.add(new InputStreamHandler()); + l.add(new RedirectHandler()); + l.add(new DefaultHandler()); + + return l.toArray(new ResponseHandler[l.size()]); + } + + + //-------------------------------------------------------------------------------- + // Other methods + //-------------------------------------------------------------------------------- + + /** + * Sets the parent of this resource. + * + * @param parent The parent of this resource. + */ + protected void setParent(RestServlet parent) { + this.parentResource = parent; + } + + /** + * Returns the parent of this resource. + * + * @return The parent of this resource, or <jk>null</jk> if resource has no parent. + */ + public RestServlet getParent() { + return this.parentResource; + } + + private String[] parseHeader(String s) { + int i = s.indexOf(':'); + if (i == -1) + return null; + String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH); + String val = s.substring(i+1).trim(); + return new String[]{name,val}; + } + + /** + * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object. + * <p> + * Subclasses may choose to override this method to provide a specialized request object. + * </p> + * + * @param req The request object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. + * @return The wrapped request object. + * @throws ServletException If any errors occur trying to interpret the request. + */ + protected RestRequest createRequest(HttpServletRequest req) throws ServletException { + return new RestRequest(this, req); + } + + /** + * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object + * and the request returned by {@link #createRequest(HttpServletRequest)}. + * <p> + * Subclasses may choose to override this method to provide a specialized response object. + * </p> + * + * @param req The request object returned by {@link #createRequest(HttpServletRequest)}. + * @param res The response object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. + * @return The wrapped response object. + * @throws ServletException If any erros occur trying to interpret the request or response. + */ + protected RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException { + return new RestResponse(this, req, res); + } + + /** + * Returns whether this resource class can provide an OPTIONS page. + * <p> + * By default, returns <jk>false</jk>. + * </p> + * <p> + * Subclasses can override this method to cause the <code>options</code> link to show up in the HTML serialized output. + * </p> + * + * @return <jk>true</jk> if this resource has implemented a {@code getOptions()} method. + */ + public boolean hasOptionsPage() { + return false; + } + + /** + * Specify a class-level property. + * <p> + * Typically, properties in {@link RestServletContext} can be set in the {@link Servlet#init(ServletConfig)} method. + * </p> + * + * @param key The property name. + * @param value The property value. + * @return This object (for method chaining). + */ + public synchronized RestServlet setProperty(String key, Object value) { + getProperties().put(key, value); + return this; + } + + /** + * The main service method. + * <p> + * Subclasses can optionally override this method if they want to tailor the behavior of requests. + * </p> + */ + @Override /* Servlet */ + public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException { + + log(FINE, "HTTP: {0} {1}", r1.getMethod(), r1.getRequestURI()); + long startTime = System.currentTimeMillis(); + + try { + + if (initException != null) { + if (initException instanceof RestException) + throw (RestException)initException; + throw new RestException(SC_INTERNAL_SERVER_ERROR, initException); + } + + if (! isInitialized) + throw new RestException(SC_INTERNAL_SERVER_ERROR, "Servlet has not been initialized"); + + String pathInfo = RestUtils.getPathInfoUndecoded(r1); // Can't use r1.getPathInfo() because we don't want '%2F' resolved. + + // If this resource has child resources, try to recursively call them. + if (pathInfo != null && (! childResources.isEmpty()) && (! pathInfo.equals("/"))) { + int i = pathInfo.indexOf('/', 1); + String pathInfoPart = i == -1 ? pathInfo.substring(1) : pathInfo.substring(1, i); + RestServlet childResource = childResources.get(pathInfoPart); + if (childResource != null) { + final String pathInfoRemainder = (i == -1 ? null : pathInfo.substring(i)); + final String servletPath = r1.getServletPath() + "/" + pathInfoPart; + final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) { + @Override /* ServletRequest */ + public String getPathInfo() { + return RestUtils.decode(pathInfoRemainder); + } + @Override /* ServletRequest */ + public String getServletPath() { + return servletPath; + } + }; + childResource.service(childRequest, r2); + return; + } + } + + RestRequest req = createRequest(r1); + RestResponse res = createResponse(req, r2); + String method = req.getMethod(); + String methodUC = method.toUpperCase(Locale.ENGLISH); + + StreamResource r = null; + if (pathInfo != null) { + String p = pathInfo.substring(1); + if (p.equals("favicon.ico")) + r = favIcon; + else if (p.equals("style.css")) + r = styleSheet; + else if (StringUtils.pathStartsWith(p, staticFilesPrefixes)) + r = resolveStaticFile(p); + } + + if (r != null) { + res.setStatus(SC_OK); + res.setOutput(r); + } else { + // If the specified method has been defined in a subclass, invoke it. + int rc = SC_METHOD_NOT_ALLOWED; + if (restMethods.containsKey(methodUC)) { + rc = restMethods.get(methodUC).invoke(method, pathInfo, this, req, res); + } else if (restMethods.containsKey("*")) { + rc = restMethods.get("*").invoke(method, pathInfo, this, req, res); + } + + // If not invoked above, see if it's an OPTIONs request + if (rc != SC_OK) + handleNotFound(rc, req, res); + } + + if (res.hasOutput()) { + Object output = res.getOutput(); + + // Do any class-level transforming. + for (RestConverter converter : getConverters()) + output = converter.convert(req, output, getBeanContext().getClassMetaForObject(output)); + + res.setOutput(output); + + // Now serialize the output if there was any. + // Some subclasses may write to the OutputStream or Writer directly. + handleResponse(req, res, output); + } + + onSuccess(req, res, System.currentTimeMillis() - startTime); + + } catch (RestException e) { + handleError(r1, r2, e); + } catch (Throwable e) { + handleError(r1, r2, new RestException(SC_INTERNAL_SERVER_ERROR, e)); + } + log(FINE, "HTTP: [{0} {1}] finished in {2}ms", r1.getMethod(), r1.getRequestURI(), System.currentTimeMillis()-startTime); + } + + /** + * Handle the case where a matching method was not found. + * <p> + * Subclasses can override this method to provide a 2nd-chance for specifying a response. + * The default implementation will simply throw an exception with an appropriate message. + * </p> + * + * @param rc The HTTP response code. + * @param req The HTTP request. + * @param res The HTTP response. + * @throws Exception + */ + protected void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception { + String pathInfo = req.getPathInfo(); + String methodUC = req.getMethod(); + String onPath = pathInfo == null ? " on no pathInfo" : format(" on path '%s'", pathInfo); + if (rc == SC_NOT_FOUND) + throw new RestException(rc, "Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath); + else if (rc == SC_PRECONDITION_FAILED) + throw new RestException(rc, "Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath); + else if (rc == SC_METHOD_NOT_ALLOWED) + throw new RestException(rc, "Method ''{0}'' not found on resource.", methodUC); + else + throw new ServletException("Invalid method response: " + rc); + } + + private synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { + Integer c = 1; + if (context.useStackTraceHashes) { + int h = e.hashCode(); + c = stackTraceHashes.get(h); + if (c == null) + c = 1; + else + c++; + stackTraceHashes.put(h, c); + e.setOccurrence(c); + } + onError(req, res, e); + renderError(req, res, e); + } + + /** + * Method for rendering response errors. + * <p> + * The default implementation renders a plain text English message, optionally with a stack trace + * if {@link RestServletContext#REST_renderResponseStackTraces} is enabled. + * </p> + * <p> + * Subclasses can override this method to provide their own custom error response handling. + * </p> + * + * @param req The servlet request. + * @param res The servlet response. + * @param e The exception that occurred. + * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. + */ + protected void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { + + int status = e.getStatus(); + res.setStatus(status); + res.setContentType("text/plain"); + res.setHeader("Content-Encoding", "identity"); + PrintWriter w = null; + try { + w = res.getWriter(); + } catch (IllegalStateException e2) { + w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), IOUtils.UTF8)); + } + String httpMessage = RestUtils.getHttpResponseText(status); + if (httpMessage != null) + w.append("HTTP ").append(String.valueOf(status)).append(": ").append(httpMessage).append("\n\n"); + if (context.renderResponseStackTraces) + e.printStackTrace(w); + else + w.append(e.getFullStackMessage(true)); + w.flush(); + w.close(); + } + + /** + * Callback method for logging errors during HTTP requests. + * <p> + * Typically, subclasses will override this method and log errors themselves. + * <p> + * </p> + * The default implementation simply logs errors to the <code>RestServlet</code> logger. + * </p> + * <p> + * Here's a typical implementation showing how stack trace hashing can be used to reduce log file sizes... + * </p> + * <p class='bcode'> + * <jk>protected void</jk> onError(HttpServletRequest req, HttpServletResponse res, RestException e, <jk>boolean</jk> noTrace) { + * String qs = req.getQueryString(); + * String msg = <js>"HTTP "</js> + req.getMethod() + <js>" "</js> + e.getStatus() + <js>" "</js> + req.getRequestURI() + (qs == <jk>null</jk> ? <js>""</js> : <js>"?"</js> + qs); + * <jk>int</jk> c = e.getOccurrence(); + * + * <jc>// REST_useStackTraceHashes is disabled, so we have to log the exception every time.</jc> + * <jk>if</jk> (c == 0) + * myLogger.log(Level.<jsf>WARNING</jsf>, <jsm>format</jsm>(<js>"[%s] %s"</js>, e.getStatus(), msg), e); + * + * <jc>// This is the first time we've countered this error, so log a stack trace + * // unless ?noTrace was passed in as a URL parameter.</jc> + * <jk>else if</jk> (c == 1 && ! noTrace) + * myLogger.log(Level.<jsf>WARNING</jsf>, <jsm>format</jsm>(<js>"[%h.%s.%s] %s"</js>, e.hashCode(), e.getStatus(), c, msg), e); + * + * <jc>// This error occurred before. + * // Only log the message, not the stack trace.</jc> + * <jk>else</jk> + * myLogger.log(Level.<jsf>WARNING</jsf>, <jsm>format</jsm>(<js>"[%h.%s.%s] %s, %s"</js>, e.hashCode(), e.getStatus(), c, msg, e.getLocalizedMessage())); + * } + * </p> + * + * @param req The servlet request object. + * @param res The servlet response object. + * @param e Exception indicating what error occurred. + */ + protected void onError(HttpServletRequest req, HttpServletResponse res, RestException e) { + if (shouldLog(req, res, e)) { + String qs = req.getQueryString(); + String msg = "HTTP " + req.getMethod() + " " + e.getStatus() + " " + req.getRequestURI() + (qs == null ? "" : "?" + qs); + int c = e.getOccurrence(); + if (shouldLogStackTrace(req, res, e)) { + msg = '[' + Integer.toHexString(e.hashCode()) + '.' + e.getStatus() + '.' + c + "] " + msg; + log(Level.WARNING, e, msg); + } else { + msg = '[' + Integer.toHexString(e.hashCode()) + '.' + e.getStatus() + '.' + c + "] " + msg + ", " + e.getLocalizedMessage(); + log(Level.WARNING, msg); + } + } + } + + /** + * Returns <jk>true</jk> if the specified exception should be logged. + * <p> + * Subclasses can override this method to provide their own logic for determining when exceptions are logged. + * </p> + * <p> + * The default implementation will return <jk>false</jk> if <js>"noTrace=true"</js> is passed in the query string. + * </p> + * + * @param req The HTTP request. + * @param res The HTTP response. + * @param e The exception. + * @return <jk>true</jk> if exception should be logged. + */ + protected boolean shouldLog(HttpServletRequest req, HttpServletResponse res, RestException e) { + String q = req.getQueryString(); + return (q == null ? true : q.indexOf("noTrace=true") == -1); + } + + /** + * Returns <jk>true</jk> if a stack trace should be logged for this exception. + * <p> + * Subclasses can override this method to provide their own logic for determining when stack traces are logged. + * </p> + * <p> + * The default implementation will only log a stack trace if {@link RestException#getOccurrence()} returns <code>1</code> + * and the exception is not one of the following: + * </p> + * <ul> + * <li>{@link HttpServletResponse#SC_UNAUTHORIZED} + * <li>{@link HttpServletResponse#SC_FORBIDDEN} + * <li>{@link HttpServletResponse#SC_NOT_FOUND} + * </ul> + * + * @param req The HTTP request. + * @param res The HTTP response. + * @param e The exception. + * @return <jk>true</jk> if stack trace should be logged. + */ + protected boolean shouldLogStackTrace(HttpServletRequest req, HttpServletResponse res, RestException e) { + if (e.getOccurrence() == 1) { + switch (e.getStatus()) { + case SC_UNAUTHORIZED: + case SC_FORBIDDEN: + case SC_NOT_FOUND: return false; + default: return true; + } + } + return false; + } + + /** + * Log a message. + * <p> + * Equivalent to calling <code>log(level, <jk>null</jk>, msg, args);</code> + * </p> + * + * @param level The log level. + * @param msg The message to log. + * @param args {@link MessageFormat} style arguments in the message. + */ + protected void log(Level level, String msg, Object...args) { + log(level, null, msg, args); + } + + /** + * Same as {@link #log(Level, String, Object...)} excepts runs the + * arguments through {@link JsonSerializer#DEFAULT_LAX_READABLE}. + * <p> + * Serialization of arguments do not occur if message is not logged, so + * it's safe to use this method from within debug log statements. + * </p> + * + * <dl> + * <dt>Example:</dt> + * <dd> + * <p class='bcode'> + * logObjects(<jsf>DEBUG</jsf>, <js>"Pojo contents:\n{0}"</js>, myPojo); + * </p> + * </dd> + * </dl> + * + * @param level The log level. + * @param msg The message to log. + * @param args {@link MessageFormat} style arguments in the message. + */ + protected void logObjects(Level level, String msg, Object...args) { + for (int i = 0; i < args.length; i++) + args[i] = JsonSerializer.DEFAULT_LAX_READABLE.toStringObject(args[i]); + log(level, null, msg, args); + } + + /** + * Log a message to the logger returned by {@link #getLogger()}. + * <p> + * Subclasses can override this method if they wish to log messages using a library other than + * Java Logging (e.g. Apache Commons Logging). + * </p> + * + * @param level The log level. + * @param cause The cause. + * @param msg The message to log. + * @param args {@link MessageFormat} style arguments in the message. + */ + protected void log(Level level, Throwable cause, String msg, Object...args) { + JuneauLogger log = getLogger(); + if (args.length > 0) + msg = MessageFormat.format(msg, args); + log.log(level, msg, cause); + } + + /** + * Callback method for listening for successful completion of requests. + * <p> + * Subclasses can override this method for gathering performance statistics. + * </p> + * <p> + * The default implementation does nothing. + * </p> + * + * @param req The HTTP request. + * @param res The HTTP response. + * @param time The time in milliseconds it took to process the request. + */ + protected void onSuccess(RestRequest req, RestResponse res, long time) {} + + /** + * Callback method that gets invoked right before the REST Java method is invoked. + * <p> + * Subclasses can override this method to override request headers or set request-duration properties + * before the Java method is invoked. + * </p> + * + * @param req The HTTP servlet request object. + * @throws RestException If any error occurs. + */ + protected void onPreCall(RestRequest req) throws RestException {} + + /** + * Callback method that gets invoked right after the REST Java method is invoked, but before + * the serializer is invoked. + * <p> + * Subclasses can override this method to override request and response headers, or + * set/override properties used by the serializer. + * </p> + * + * @param req The HTTP servlet request object. + * @param res The HTTP servlet response object. + * @throws RestException If any error occurs. + */ + protected void onPostCall(RestRequest req, RestResponse res) throws RestException {} + + /** + * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method. + * <p> + * Subclasses may override this method if they wish to modify the way the output is rendered, or support + * other output formats. + * </p> + * + * @param req The HTTP request. + * @param res The HTTP response. + * @param output The output to serialize in the response. + * @throws IOException + * @throws RestException + */ + protected void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException { + // Loop until we find the correct handler for the POJO. + for (ResponseHandler h : getResponseHandlers()) + if (h.handle(req, res, output)) + return; + throw new RestException(SC_NOT_IMPLEMENTED, "No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'"); + } + + @Override /* GenericServlet */ + public ServletConfig getServletConfig() { + return servletConfig; + } + + @Override /* GenericServlet */ + public void destroy() { + for (RestServlet r : childResources.values()) + r.destroy(); + super.destroy(); + } + + /** + * Resolve a static resource file. + * <p> + * Subclasses can override this method to provide their own way to resolve files. + * </p> + * + * @param pathInfo The unencoded path info. + * @return The resource, or <jk>null</jk> if the resource could not be resolved. + * @throws IOException + */ + protected StreamResource resolveStaticFile(String pathInfo) throws IOException { + if (! staticFilesCache.containsKey(pathInfo)) { + String p = RestUtils.decode(RestUtils.trimSlashes(pathInfo)); + if (p.indexOf("..") != -1) + throw new RestException(SC_NOT_FOUND, "Invalid path"); + for (Map.Entry<String,String> e : staticFilesMap.entrySet()) { + String key = RestUtils.trimSlashes(e.getKey()); + if (p.startsWith(key)) { + String remainder = (p.equals(key) ? "" : p.substring(key.length())); + if (remainder.isEmpty() || remainder.startsWith("/")) { + String p2 = RestUtils.trimSlashes(e.getValue()) + remainder; + InputStream is = getResource(p2); + if (is != null) { + try { + int i = p2.lastIndexOf('/'); + String name = (i == -1 ? p2 : p2.substring(i+1)); + String mediaType = getMimetypesFileTypeMap().getContentType(name); + staticFilesCache.put(pathInfo, new StreamResource(is, mediaType).setHeader("Cache-Control", "max-age=86400, public")); + return staticFilesCache.get(pathInfo); + } finally { + is.close(); + } + } + } + } + } + } + return staticFilesCache.get(pathInfo); + } + + /** + * Returns a list of valid {@code Accept} content types for this resource. + * <p> + * Typically used by subclasses during {@code OPTIONS} requests. + * </p> + * <p> + * The default implementation resturns the list from {@link ParserGroup#getSupportedMediaTypes()} + * from the parser group returned by {@link #getParsers()}. + * </p> + * <p> + * Subclasses can override or expand this list as they see fit. + * </p> + * + * @return The list of valid {@code Accept} content types for this resource. + * @throws RestServletException + */ + public Collection<String> getSupportedAcceptTypes() throws RestServletException { + return getParsers().getSupportedMediaTypes(); + } + + /** + * Returns a list of valid {@code Content-Types} for input for this resource. + * <p> + * Typically used by subclasses during {@code OPTIONS} requests. + * </p> + * <p> + * The default implementation resturns the list from {@link SerializerGroup#getSupportedMediaTypes()} + * from the parser group returned by {@link #getSerializers()}. + * </p> + * <p> + * Subclasses can override or expand this list as they see fit. + * </p> + * + * @return The list of valid {@code Content-Type} header values for this resource. + * @throws RestServletException + */ + public Collection<String> getSupportedContentTypes() throws RestServletException { + return getSerializers().getSupportedMediaTypes(); + } + + /** + * Returns localized descriptions of all REST methods defined on this class that the user of the current + * request is allowed to access. + * <p> + * Useful for OPTIONS pages. + * </p> + * <p> + * This method does not cache results, since it's expected to be called infrequently. + * </p> + * + * @param req The current request. + * @return Localized descriptions of all REST methods defined on this class. + * @throws RestServletException + */ + public Collection<MethodDescription> getMethodDescriptions(RestRequest req) throws RestServletException { + List<MethodDescription> l = new LinkedList<MethodDescription>(); + for (MethodMeta sm : javaRestMethods.values()) + if (sm.isRequestAllowed(req)) + l.add(getMethodDescription(sm.method, sm, req)); + return l; + } + + /** + * Returns the localized description of this REST resource. + * <p> + * Subclasses can override this method to provide their own description. + * </p> + * <p> + * The default implementation returns the description from the following locations (whichever matches first): + * </p> + * <ol> + * <li>{@link RestResource#description() @RestResource.description()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].description</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>description</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * </ol> + * + * @param req The current request. + * @return The localized description of this REST resource, or a blank string if no resource description was found. + */ + public String getDescription(RestRequest req) { + if (! description.isEmpty()) + return req.getVarResolverSession().resolve(description); + String description = msgs.findFirstString(req.getLocale(), "description"); + return (description == null ? "" : req.getVarResolverSession().resolve(description)); + } + + /** + * Returns the localized description of the specified java method on this servlet. + * <p> + * Subclasses can override this method to provide their own description. + * </p> + * <p> + * The default implementation returns the description from the following locations (whichever matches first): + * </p> + * <ol> + * <li>{@link RestMethod#description() @RestMethod.description()} annotation on the method. + * <li><ck>[ClassName].[javaMethodName]</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>[javaMethodName]</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * </ol> + * + * @param javaMethodName The name of the Java method whose description we're retrieving. + * @param req The current request. + * @return The localized description of the method, or a blank string if no description was found. + */ + public String getMethodDescription(String javaMethodName, RestRequest req) { + MethodMeta m = javaRestMethods.get(javaMethodName); + if (m != null) + return m.getDescription(req); + return ""; + } + + /** + * Returns the localized label of this REST resource. + * <p> + * Subclasses can override this method to provide their own description. + * </p> + * <p> + * The default implementation returns the description from the following locations (whichever matches first): + * </p> + * <ol> + * <li>{@link RestResource#label() @RestResourcel.label()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].label</ck> property in resource bundle identified by {@link RestResource#messages() @ResourceBundle.messages()} + * annotation for this class, then any parent classes. + * <li><ck>label</ck> in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * </ol> + * + * @param req The current request. + * @return The localized description of this REST resource, or a blank string if no resource description was found. + */ + public String getLabel(RestRequest req) { + if (! label.isEmpty()) + return req.getVarResolverSession().resolve(label); + String label = msgs.findFirstString(req.getLocale(), "label"); + return (label == null ? "" : req.getVarResolverSession().resolve(label)); + } + + /** + * Returns the resource bundle identified by the {@link RestResource#messages() @RestResource.messages()} annotation for the default locale. + * + * @return The resource bundle. Never <jk>null</jk>. + */ + public MessageBundle getMessages() { + return msgs; + } + + /** + * Returns the resource bundle identified by the {@link RestResource#messages() @RestResource.messages()} annotation for the specified locale. + * + * @param locale The resource bundle locale. + * @return The resource bundle. Never <jk>null</jk>. + */ + public MessageBundle getMessages(Locale locale) { + return msgs.getBundle(locale); + } + + /** + * Gets a localized message from the resource bundle identified by the {@link RestResource#messages() @RestResource.messages()} annotation. + * <p> + * If resource bundle location was not specified, or the resource bundle was not found, + * returns the string <js>"{!!key}"</js>. + * </p> + * <p> + * If message was not found in the resource bundle, returns the string <js>"{!key}"</js>. + * </p> + * + * @param locale The client locale. + * @param key The resource bundle key. + * @param args Optional {@link java.text.MessageFormat} variable values to replace. + * @return The localized message. + */ + public String getMessage(Locale locale, String key, Object...args) { + return msgs.getString(locale, key, args); + } + + /** + * Programmatically adds the specified resource as a child to this resource. + * <p> + * This method can be used in a resources {@link #init()} method to define child resources + * accessible through a child URL. + * </p> + * <p> + * Typically, child methods are defined via {@link RestResource#children() @RestResource.children()}. However, this + * method is provided to handle child resources determined at runtime. + * </p> + * + * @param name The sub-URL under which this resource is accessible.<br> + * For example, if the parent resource URL is <js>"/foo"</js>, and this name is <js>"bar"</js>, then + * the child resource will be accessible via the URL <js>"/foo/bar"</js>. + * @param resource The child resource. + * @throws ServletException Thrown by the child init() method. + */ + protected void addChildResource(String name, RestServlet resource) throws ServletException { + resource.init(getServletConfig()); + childResources.put(name, resource); + } + + /** + * Returns the child resources associated with this servlet. + * + * @return An unmodifiable map of child resources. + * Keys are the {@link RestResource#path() @RestResource.path()} annotation defined on the child resource. + */ + public Map<String,RestServlet> getChildResources() { + return Collections.unmodifiableMap(childResources); + } + + /** + * Returns the path for this servlet as defined by the {@link RestResource#path()} annotation + * on this class concatenated with those on all parent classes. + * <p> + * If path is not specified, returns <js>"/"</js>. + * </p> + * <p> + * Path always starts with <js>"/"</js>. + * </p> + * + * @return The servlet path. + */ + public String getPath() { + if (path == null) { + LinkedList<String> l = new LinkedList<String>(); + RestServlet r = this; + while (r != null) { + String p = r.findPath(); + if (p == null) + break; + l.addFirst(p); + r = r.parentResource; + } + StringBuilder sb = new StringBuilder(); + for (String p : l) + sb.append('/').append(p); + path = sb.toString(); + } + return path; + } + + private String findPath() { + List<RestResource> rrc = ReflectionUtils.findAnnotations(RestResource.class, getClass()); + for (RestResource rc : rrc) { + String p = rc.path(); + if (StringUtils.startsWith(p, '/')) + p = p.substring(1); + if (! p.isEmpty()) + return p; + } + return null; + } + + /** + * Returns the config file for this servlet. + * <p> + * Subclasses can override this method to provide their own config file. + * </p> + * <p> + * The default implementation uses the path defined by the {@link RestResource#config() @RestResource.config()} property resolved + * by {@link ConfigMgr#DEFAULT}. + * </p> + * + * @return The config file for this servlet. + * @throws IOException + */ + protected ConfigFile createConfigFile() throws IOException { + String cf = varResolver.resolve(configPath); + if (cf.isEmpty()) + return getConfigMgr().create(); + return getConfigMgr().get(cf); + } + + /** + * Creates the stylesheet for this servlet. + * <p> + * The stylesheet is made available on the path <js>"/servlet-path/style.css"</js>. + * </p> + * <p> + * Subclasses can override this method to provide their own stylesheet. + * </p> + * <p> + * The default implementation uses the {@link RestResource#stylesheet() @RestResource.stylesheet()} annotation + * to determine the stylesheet name and then searches the classpath then working directory + * for that stylesheet. + * </p> + * + * @return The stylesheet to use for this servlet, or <jk>null</jk> if the stylesheet could not be found. + * @throws IOException If stylesheet could not be loaded. + */ + protected StreamResource createStyleSheet() throws IOException { + for (RestResource r : restResourceAnnotationsChildFirst.values()) { + if (! r.stylesheet().isEmpty()) { + String path = getVarResolver().resolve(r.stylesheet()); + InputStream is = getResource(path); + if (is != null) { + try { + return new StreamResource(is, "text/css"); + } finally { + is.close(); + } + } + } + } + return null; + } + + /** + * Creates the favicon for this servlet. + * <p> + * The favicon is made available on the path <js>"/servlet-path/favicon.ico"</js>. + * </p> + * <p> + * Subclasses can override this method to provide their own favorites icon. + * </p> + * <p> + * The default implementation uses the {@link RestResource#favicon() @RestResource.favicon()} annotation + * to determine the file name and then searches the classpath then working directory + * for that file. + * </p> + * + * @return The icon file to use for this servlet. + * @throws IOException If icon file could not be loaded. + */ + protected StreamResource createFavIcon() throws IOException { + for (RestResource r : restResourceAnnotationsChildFirst.values()) { + if (! r.favicon().isEmpty()) { + String path = getVarResolver().resolve(r.favicon()); + InputStream is = getResource(path); + if (is != null) { + try { + return new StreamResource(is, "image/x-icon"); + } finally { + is.close(); + } + } + } + } + return null; + } + + /** + * Creates the static files map for this servlet. + * <p> + * This map defines static files that can be served up through subpaths on this servlet. + * The map keys are subpaths (e.g. <js>"htdocs"</js>) and the values are locations to look in + * the classpath and working directory for those files. + * </p> + * <p> + * Subclasses can override this method to provide their own mappings. + * </p> + * <p> + * The default implementation uses the {@link RestResource#staticFiles() @RestResource.staticFiles()} annotation + * to determine the mappings. + * </p> + * + * @return The list of static file mappings. + * @throws ParseException + */ + @SuppressWarnings("unchecked") + protected Map<String,String> createStaticFilesMap() throws ParseException { + Map<String,String> m = new LinkedHashMap<String,String>(); + for (RestResource r : restResourceAnnotationsParentFirst.values()) + if (! r.staticFiles().isEmpty()) + m.putAll(JsonParser.DEFAULT.parseMap(getVarResolver().resolve(r.staticFiles()), LinkedHashMap.class, String.class, String.class)); + return m; + } + + /** + * Returns the config manager used to create the config file in {@link #createConfigFile()}. + * <p> + * The default implementation return {@link ConfigMgr#DEFAULT}, but subclasses can override + * this if they want to provide their own customized config manager. + * </p> + * + * @return The config file manager. + */ + protected ConfigMgr getConfigMgr() { + return ConfigMgr.DEFAULT; + } + + /** + * Returns the logger associated with this servlet. + * <p> + * Subclasses can override this method to provide their own Java Logging logger. + * </p> + * <p> + * Subclasses that use other logging libraries such as Apache Commons Logging should + * override the {@link #log(Level, Throwable, String, Object...)} method instead. + * </p> + * + * @return The logger associated with this servlet. + */ + protected JuneauLogger getLogger() { + if (logger == null) + logger = JuneauLogger.getLogger(getClass()); + return logger; + } + + private abstract class ResourceMethod { + abstract int invoke(String methodName, String pathInfo, RestServlet resource, RestRequest req, RestResponse res) throws RestException; + + void complete() { + // Do nothing by default. + } + } + + static enum ParamType { + REQ, RES, ATTR, CONTENT, HEADER, METHOD, PARAM, QPARAM, HASPARAM, HASQPARAM, PATHREMAINDER, PROPS, MESSAGES; + + boolean isOneOf(ParamType...pt) { + for (ParamType ptt : pt) + if (this == ptt) + return true; + return false; + } + } + + static class MethodParam { + + ParamType paramType; + Type type; + String name = ""; + boolean multiPart, plainParams; + + MethodParam(MethodMeta mm, Type type, Method method, Annotation[] annotations) throws ServletException { + this.type = type; + boolean isClass = type instanceof Class; + if (isClass && isParentClass(HttpServletRequest.class, (Class)type)) + paramType = REQ; + else if (isClass && isParentClass(HttpServletResponse.class, (Class)type)) + paramType = RES; + else for (Annotation a : annotations) { + if (a instanceof Attr) { + Attr a2 = (Attr)a; + paramType = ATTR; + name = a2.value(); + } else if (a instanceof Header) { + Header h = (Header)a; + paramType = HEADER; + name = h.value(); + } else if (a instanceof Param) { + Param p = (Param)a; + if (p.multipart()) + assertCollection(type, method); + paramType = PARAM; + multiPart = p.multipart(); + plainParams = p.format().equals("INHERIT") ? mm.mPlainParams : p.format().equals("PLAIN"); + name = p.value(); + } else if (a instanceof QParam) { + QParam p = (QParam)a; + if (p.multipart()) + assertCollection(type, method); + paramType = QPARAM; + multiPart = p.multipart(); + plainParams = p.format().equals("INHERIT") ? mm.mPlainParams : p.format().equals("PLAIN"); + name = p.value(); + } else if (a instanceof HasParam) { + HasParam p = (HasParam)a; + paramType = HASPARAM; + name = p.value(); + } else if (a instanceof HasQParam) { + HasQParam p = (HasQParam)a; + paramType = HASQPARAM; + name = p.value(); + } else if (a instanceof Content) { + paramType = CONTENT; + } else if (a instanceof org.apache.juneau.server.annotation.Method) { + paramType = METHOD; + if (type != String.class) + throw new ServletException("@Method parameters must be of type String"); + } else if (a instanceof PathRemainder) { + paramType = PATHREMAINDER; + if (type != String.class) + throw new ServletException("@PathRemainder parameters must be of type String"); + } else if (a instanceof Properties) { + paramType = PROPS; + name = "PROPERTIES"; + } else if (a instanceof Messages) { + paramType = MESSAGES; + name = "MESSAGES"; + } + } + if (paramType == null) + paramType = ATTR; + } + + /** + * Throws an exception if the specified type isn't an array or collection. + */ + private void assertCollection(Type t, Method m) throws ServletException { + ClassMeta<?> cm = BeanContext.DEFAULT.getClassMeta(t); + if (! (cm.isArray() || cm.isCollection())) + throw new ServletException("Use of multipart flag on parameter that's not an array or Collection on method" + m); + } + + @SuppressWarnings("unchecked") + private Object getValue(RestRequest req, RestResponse res) throws Exception { + BeanContext bc = req.getServlet().getBeanContext(); + switch(paramType) { + case REQ: return req; + case RES: return res; + case ATTR: return req.getAttribute(name, type); + case CONTENT: return req.getInput(type); + case HEADER: return req.getHeader(name, type); + case METHOD: return req.getMethod(); + case PARAM: { + if (multiPart) + return req.getParameters(name, type); + if (plainParams) + return bc.convertToType(req.getParameter(name), bc.getClassMeta(type)); + return req.getParameter(name, type); + } + case QPARAM: { + if (multiPart) + return req.getQueryParameters(name, type); + if (plainParams) + return bc.convertToType(req.getQueryParameter(name), bc.getClassMeta(type)); + return req.getQueryParameter(name, type); + } + case HASPARAM: return bc.convertToType(req.hasParameter(name), bc.getClassMeta(type)); + case HASQPARAM: return bc.convertToType(req.hasQueryParameter(name), bc.getClassMeta(type)); + case PATHREMAINDER: return req.getPathRemainder(); + case PROPS: return res.getProperties(); + case MESSAGES: return req.getResourceBundle(); + } + return null; + } + } + + /* + * Represents a single Java servlet method annotated with @RestMethod. + */ + private class MethodMeta extends ResourceMethod implements Comparable<MethodMeta> { + private String httpMethod; + private java.lang.reflect.Method method; + private UrlPathPattern pathPattern; + private MethodParam[] params; + private RestGuard[] guards; + private RestMatcher[] optionalMatchers, requiredMatchers; + private RestConverter[] mConverters; + private SerializerGroup mSerializers; // Method-level serializers + private ParserGroup mParsers; // Method-level parsers + private EncoderGroup mEncoders; // Method-level encoders + private UrlEncodingParser mUrlEncodingParser; // Method-level URL parameter parser. + private UrlEncodingSerializer mUrlEncodingSerializer; // Method-level URL parameter serializer. + private ObjectMap mProperties; // Method-level properties + private Map<String,String> mDefaultRequestHeaders; // Method-level default request headers + private String mDefaultEncoding; + private boolean mPlainParams; + private String description; + private Integer priority; + + private MethodMeta(java.lang.reflect.Method method) throws RestServletException { + try { + this.method = method; + + RestMethod m = method.getAnnotation(RestMethod.class); + if (m == null) + throw new RestServletException("@RestMethod annotation not found on method ''{0}.{1}''", method.getDeclaringClass().getName(), method.getName()); + + this.description = m.description(); + this.mSerializers = getSerializers(); + this.mParsers = getParsers(); + this.mUrlEncodingParser = getUrlEncodingParser(); + this.mUrlEncodingSerializer = getUrlEncodingSerializer(); + this.mProperties = getProperties(); + this.mEncoders = getEncoders(); + + ArrayList<Inherit> si = new ArrayList<Inherit>(Arrays.asList(m.serializersInherit())); + ArrayList<Inherit> pi = new ArrayList<Inherit>(Arrays.asList(m.parsersInherit())); + + if (m.serializers().length > 0 || m.parsers().length > 0 || m.properties().length > 0 || m.transforms().length > 0) { + mSerializers = (si.contains(SERIALIZERS) || m.serializers().length == 0 ? mSerializers.clone() : new SerializerGroup()); + mParsers = (pi.contains(PARSERS) || m.parsers().length == 0 ? mParsers.clone() : new ParserGroup()); + mUrlEncodingParser = mUrlEncodingParser.clone(); + } + + httpMethod = m.name().toUpperCase(Locale.ENGLISH); + if (httpMethod.equals("") && method.getName().startsWith("do")) + httpMethod = method.getName().substring(2).toUpperCase(Locale.ENGLISH); + if (httpMethod.equals("")) + throw new RestServletException("@RestMethod name not specified on method ''{0}.{1}''", method.getDeclaringClass().getName(), method.getName()); + if (httpMethod.equals("METHOD")) + httpMethod = "*"; + + priority = m.priority(); + + String p = m.path(); + mConverters = new RestConverter[m.converters().length]; + for (int i = 0; i < mConverters.length; i++) + mConverters[i] = m.converters()[i].newInstance(); + + guards = new RestGuard[m.guards().length]; + for (int i = 0; i < guards.length; i++) + guards[i] = m.guards()[i].newInstance(); + + List<RestMatcher> optionalMatchers = new LinkedList<RestMatcher>(), requiredMatchers = new LinkedList<RestMatcher>(); + for (int i = 0; i < m.matchers().length; i++) { + Class<? extends RestMatcher> c = m.matchers()[i]; + RestMatcher matcher = null; + if (ClassUtils.isParentClass(RestMatcherReflecting.class, c)) + matcher = c.getConstructor(RestServlet.class, Method.class).newInstance(RestServlet.this, method); + else + matcher = c.newInstance(); + if (matcher.mustMatch()) + requiredMatchers.add(matcher); + else + optionalMatchers.add(matcher); + } + if (! m.clientVersion().isEmpty()) + requiredMatchers.add(new ClientVersionMatcher(RestServlet.this, method)); + + this.requiredMatchers = requiredMatchers.toArray(new RestMatcher[requiredMatchers.size()]); + this.optionalMatchers = optionalMatchers.toArray(new RestMatcher[optionalMatchers.size()]); + + if (m.serializers().length > 0) { + mSerializers.append(m.serializers()); + if (si.contains(TRANSFORMS)) + mSerializers.addTransforms(getTransforms()); + if (si.contains(PROPERTIES)) + mSerializers.setProperties(getProperties()); + } + + if (m.parsers().length > 0) { + mParsers.append(m.parsers()); + if (pi.contains(TRANSFORMS)) + mParsers.addTransforms(getTransforms()); + if (pi.contains(PROPERTIES)) + mParsers.setProperties(getProperties()); + } + + if (m.properties().length > 0) { + mProperties = new ObjectMap().setInner(getProperties()); + for (Property p1 : m.properties()) { + String n = p1.name(), v = p1.value(); + mProperties.put(n, v); + mSerializers.setProperty(n, v); + mParsers.setProperty(n, v); + mUrlEncodingParser.setProperty(n, v); + } + } + + if (m.transforms().length > 0) { + mSerializers.addTransforms(m.transforms()); + mParsers.addTransforms(m.transforms()); + mUrlEncodingParser.addTransforms(m.transforms()); + } + + if (m.encoders().length > 0 || ! m.inheritEncoders()) { + EncoderGroup g = new EncoderGroup(); + if (m.inheritEncoders()) + g.append(mEncoders); + else + g.append(IdentityEncoder.INSTANCE); + + for (Class<? extends Encoder> c : m.encoders()) { + try { + g.append(c); + } catch (Exception e) { + throw new RestServletException("Exception occurred while trying to instantiate Encoder ''{0}''", c.getSimpleName()).initCause(e); + } + } + mEncoders = g; + } + + mDefaultRequestHeaders = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER); + for (String s : m.defaultRequestHeaders()) { + String[] h = parseHeader(s); + if (h == null) + throw new RestServletException("Invalid default request header specified: ''{0}''. Must be in the format: ''Header-Name: header-value''", s); + mDefaultRequestHeaders.put(h[0], h[1]); + } + + mDefaultEncoding = mProperties.getString(REST_defaultCharset, RestServlet.this.context.defaultCharset); + String paramFormat = mProperties.getString(REST_paramFormat, RestServlet.this.context.paramFormat); + mPlainParams = paramFormat.equals("PLAIN"); + + pathPattern = new UrlPathPattern(p); + + int attrIdx = 0; + Type[] pt = method.getGenericParameterTypes(); + Annotation[][] pa = method.getParameterAnnotations(); + params = new MethodParam[pt.length]; + for (int i = 0; i < params.length; i++) { + params[i] = new MethodParam(this, pt[i], method, pa[i]); + if (params[i].paramType == ATTR && params[i].name.isEmpty()) { + if (pathPattern.vars.length <= attrIdx) + throw new RestServletException("Number of attribute parameters in method ''{0}'' exceeds the number of URL pattern variables.", method.getName()); + params[i].name = pathPattern.vars[attrIdx++]; + } + } + + mSerializers.lock(); + mParsers.lock(); + mUrlEncodingParser.lock(); + + // Need this to access methods in anonymous inner classes. + method.setAccessible(true); + } catch (Exception e) { + throw new RestServletException("Exception occurred while initializing method ''{0}''", method.getName()).initCause(e); + } + } + + private String getDescription(RestRequest req) { + if (! description.isEmpty()) + return req.getVarResolverSession().resolve(description); + String description = msgs.findFirstString(req.getLocale(), method.getName()); + return (description == null ? "" : req.getVarResolverSession().resolve(description)); + } + + private boolean isRequestAllowed(RestRequest req) { + for (RestGuard guard : guards) { + req.javaMethod = method; + if (! guard.isRequestAllowed(req)) + return false; + } + return true; + } + + @Override /* ResourceMethod */ + int invoke(String methodName, String pathInfo, RestServlet resource, RestRequest req, RestResponse res) throws RestException { + + String[] patternVals = pathPattern.match(pathInfo); + if (patternVals == null) + return SC_NOT_FOUND; + + String remainder = null; + if (patternVals.length > pathPattern.vars.length) + remainder = patternVals[pathPattern.vars.length]; + for (int i = 0; i < pathPattern.vars.length; i++) + req.setAttribute(pathPattern.vars[i], patternVals[i]); + + req.init(method, remainder, createRequestProperties(mProperties, req), mDefaultRequestHeaders, mDefaultEncoding, mSerializers, mParsers, mUrlEncodingParser); + res.init(req.getProperties(), mDefaultEncoding, mSerializers, mUrlEncodingSerializer, mEncoders); + + // Class-level guards + for (RestGuard guard : getGuards()) + if (! guard.guard(req, res)) + return SC_UNAUTHORIZED; + + // If the method implements matchers, test them. + for (RestMatcher m : requiredMatchers) + if (! m.matches(req)) + return SC_PRECONDITION_FAILED; + if (optionalMatchers.length > 0) { + boolean matches = false; + for (RestMatcher m : optionalMatchers) + matches |= m.matches(req); + if (! matches) + return SC_PRECONDITION_FAILED; + } + + onPreCall(req); + + Object[] args = new Object[params.length]; + for (int i = 0; i < params.length; i++) { + try { + args[i] = params[i].getValue(req, res); + } catch (RestException e) { + throw e; + } catch (Exception e) { + throw new RestException(SC_BAD_REQUEST, + "Invalid data conversion. Could not convert {0} ''{1}'' to type ''{2}'' on method ''{3}.{4}''.", + params[i].paramType.name(), params[i].name, params[i].type, method.getDeclaringClass().getName(), method.getName() + ).initCause(e); + } + } + + try { + + for (RestGuard guard : guards) + if (! guard.guard(req, res)) + return SC_OK; + + Object output = method.invoke(resource, args); + if (! method.getReturnType().equals(Void.TYPE)) + if (output != null || ! res.getOutputStreamCalled()) + res.setOutput(output); + + onPostCall(req, res); + + if (res.hasOutput()) { + output = res.getOutput(); + for (RestConverter converter : mConverters) + output = converter.convert(req, output, getBeanContext().getClassMetaForObject(output)); + res.setOutput(output); + } + } catch (IllegalArgumentException e) { + throw new RestException(SC_BAD_REQUEST, + "Invalid argument type passed to the following method: ''{0}''.\n\tArgument types: {1}", + method.toString(), ClassUtils.getReadableClassNames(args) + ); + } catch (InvocationTargetException e) { + Throwable e2 = e.getTargetException(); // Get the throwable thrown from the doX() method. + if (e2 instanceof RestException) + throw (RestException)e2; + if (e2 instanceof ParseException) + throw new RestException(SC_BAD_REQUEST, e2); + if (e2 instanceof InvalidDataConversionException) + throw new RestException(SC_BAD_REQUEST, e2); + throw new RestException(SC_INTERNAL_SERVER_ERROR, e2); + } catch (RestException e) { + throw e; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + return SC_OK; + } + + @Override /* Object */ + public String toString() { + return "SimpleMethod: name=" + httpMethod + ", path=" + pathPattern.patternString; + } + + /* + * compareTo() method is used to keep SimpleMethods ordered in the MultiMethod.tempCache list. + * It maintains the order in which matches are made during requests. + */ + @Override /* Comparable */ + public int compareTo(MethodMeta o) { + int c; + + c = priority.compareTo(o.priority); + if (c != 0) + return c; + + c = pathPattern.compareTo(o.pathPattern); + if (c != 0) + return c; + + c = Utils.compare(o.requiredMatchers.length, requiredMatchers.length); + if (c != 0) + return c; + + c = Utils.compare(o.optionalMatchers.length, optionalMatchers.length); + if (c != 0) + return c; + + c = Utils.compare(o.guards.length, guards.length); + if (c != 0) + return c; + + return 0; + } + + @Override /* Object */ + public boolean equals(Object o) { + if (! (o instanceof MethodMeta)) + return false; + return (compareTo((MethodMeta)o) == 0); + } + + @Override /* Object */ + public int hashCode() { + return super.hashCode(); + } + } + + /* + * Represents a group of SimpleMethods that all belong to the same HTTP method (e.g. "GET"). + */ + private class MultiMethod extends ResourceMethod { + MethodMeta[] childMethods; + List<MethodMeta> tempCache = new LinkedList<MethodMeta>(); + Set<String> collisions = new HashSet<String>(); + + private MultiMethod(MethodMeta... simpleMethods) throws RestServletException { + for (MethodMeta m : simpleMethods) + addSimpleMethod(m); + } + + private void addSimpleMethod(MethodMeta m) throws RestServletException { + if (m.guards.length == 0 && m.requiredMatchers.length == 0 && m.optionalMatchers.length == 0) { + String p = m.httpMethod + ":" + m.pathPattern.toRegEx(); + if (collisions.contains(p)) + throw new RestServletException("Duplicate Java methods assigned to the same method/pattern: method=''{0}'', path=''{1}''", m.httpMethod, m.pathPattern); + collisions.add(p); + } + tempCache.add(m); + } + + @Override /* ResourceMethod */ + void complete() { + Collections.sort(tempCache); + collisions = null; + childMethods = tempCache.toArray(new MethodMeta[tempCache.size()]); + } + + @Override /* ResourceMethod */ + int invoke(String methodName, String pathInfo, RestServlet resource, RestRequest req, RestResponse res) throws RestException { + int maxRc = 0; + for (MethodMeta m : childMethods) { + int rc = m.invoke(methodName, pathInfo, resource, req, res); + //if (rc == SC_UNAUTHORIZED) + // return SC_UNAUTHORIZED; + if (rc == SC_OK) + return SC_OK; + maxRc = Math.max(maxRc, rc); + } + return maxRc; + } + + @Override /* Object */ + public String toString() { + StringBuilder sb = new StringBuilder("MultiMethod: [\n"); + for (MethodMeta sm : childMethods) + sb.append("\t" + sm + "\n"); + sb.append("]"); + return sb.toString(); + } + } + + /** + * Returns the method description for the specified method for the OPTIONS page of this servlet. + * <p> + * Subclasses can
<TRUNCATED>