http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java ---------------------------------------------------------------------- diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java new file mode 100644 index 0000000..243afee --- /dev/null +++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java @@ -0,0 +1,1418 @@ +// *************************************************************************************************************************** +// * 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.rest; + +import static javax.servlet.http.HttpServletResponse.*; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import javax.activation.*; +import javax.servlet.*; + +import org.apache.juneau.*; +import org.apache.juneau.encoders.*; +import org.apache.juneau.ini.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.rest.annotation.*; +import org.apache.juneau.rest.vars.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; +import org.apache.juneau.urlencoding.*; +import org.apache.juneau.utils.*; + +/** + * Contains all the configuration on a REST resource and the entry points for handling REST calls. + * <p> + * See {@link PropertyStore} for more information about context properties. + */ +public final class RestContext extends Context { + + /** + * <b>Configuration property:</b> Enable header URL parameters. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.allowHeaderParams"</js> + * <li><b>Data type:</b> <code>Boolean</code> + * <li><b>Default:</b> <jk>true</jk> + * </ul> + * <p> + * When enabled, headers such as <js>"Accept"</js> and <js>"Content-Type"</js> to be passed in as URL query parameters. + * For example: <js>"?Accept=text/json&Content-Type=text/json"</js> + * <p> + * Parameter names are case-insensitive. + * <p> + * Useful for debugging REST interface using only a browser. + * <p> + * Applicable to servlet class only. + */ + public static final String REST_allowHeaderParams = "RestServlet.allowHeaderParams"; + + /** + * <b>Configuration property:</b> Enable <js>"method"</js> URL parameter for specific HTTP methods. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.allowMethodParam"</js> + * <li><b>Data type:</b> <code>String</code> + * <li><b>Default:</b> <js>""</js> + * </ul> + * <p> + * When specified, the HTTP method can be overridden by passing in a <js>"method"</js> URL parameter on a regular GET request. + * For example: <js>"?method=OPTIONS"</js> + * <p> + * Format is a comma-delimited list of HTTP method names that can be passed in as a method parameter. + * Parameter name is case-insensitive. + * Use "*" to represent all methods. + * For backwards compatibility, "true" also means "*". + * <p> + * Note that per the <a class="doclink" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html">HTTP specification</a>, special care should + * be taken when allowing non-safe (POST, PUT, DELETE) methods to be invoked through GET requests. + * <p> + * Applicable to servlet class only. + * <p> + * Example: <js>"HEAD,OPTIONS"</js> + */ + public static final String REST_allowMethodParam = "RestServlet.allowMethodParam"; + + /** + * <b>Configuration property:</b> Enable <js>"body"</js> URL parameter. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.allowBodyParam"</js> + * <li><b>Data type:</b> <code>Boolean</code> + * <li><b>Default:</b> <jk>true</jk> + * </ul> + * <p> + * When enabled, the HTTP body content on PUT and POST requests can be passed in as text using the <js>"body"</js> URL parameter. + * For example: <js>"?body={name:'John%20Smith',age:45}"</js> + * <p> + * Parameter name is case-insensitive. + * <p> + * Useful for debugging PUT and POST methods using only a browser. + * <p> + * Applicable to servlet class only. + */ + public static final String REST_allowBodyParam = "RestServlet.allowBodyParam"; + + /** + * <b>Configuration property:</b> Render stack traces. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.renderResponseStackTraces"</js> + * <li><b>Data type:</b> <code>Boolean</code> + * <li><b>Default:</b> <jk>false</jk> + * </ul> + * <p> + * Render stack traces in HTTP response bodies when errors occur. + * <p> + * When enabled, Java stack traces will be rendered in the output response. + * Useful for debugging, although allowing stack traces to be rendered may cause security concerns. + * <p> + * Applicable to servlet class only. + */ + public static final String REST_renderResponseStackTraces = "RestServlet.renderResponseStackTraces"; + + /** + * <b>Configuration property:</b> Use stack trace hashes. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.useStackTraceHashes"</js> + * <li><b>Data type:</b> <code>Boolean</code> + * <li><b>Default:</b> <jk>true</jk> + * </ul> + * <p> + * When enabled, the number of times an exception has occurred will be determined based on stack trace hashsums, + * made available through the {@link RestException#getOccurrence()} method. + * <p> + * Applicable to servlet class only. + */ + public static final String REST_useStackTraceHashes = "RestServlet.useStackTraceHashes"; + + /** + * <b>Configuration property:</b> Default character encoding. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.defaultCharset"</js> + * <li><b>Data type:</b> <code>String</code> + * <li><b>Default:</b> <js>"utf-8"</js> + * </ul> + * <p> + * The default character encoding for the request and response if not specified on the request. + * <p> + * Applicable to servlet class and methods. + */ + public static final String REST_defaultCharset = "RestServlet.defaultCharset"; + + /** + * <b>Configuration property:</b> Expected format of request parameters. + * <p> + * <ul> + * <li><b>Name:</b> <js>"RestServlet.paramFormat"</js> + * <li><b>Data type:</b> <code>String</code> + * <li><b>Default:</b> <js>"UON"</js> + * </ul> + * <p> + * Possible values: + * <ul class='spaced-list'> + * <li><js>"UON"</js> - URL-Encoded Object Notation.<br> + * This notation allows for request parameters to contain arbitrarily complex POJOs. + * <li><js>"PLAIN"</js> - Plain text.<br> + * This treats request parameters as plain text.<br> + * Only POJOs directly convertable from <l>Strings</l> can be represented in parameters when using this mode. + * </ul> + * <p> + * Note that the parameter value <js>"(foo)"</js> is interpreted as <js>"(foo)"</js> when using plain mode, but + * <js>"foo"</js> when using UON mode. + * <p> + * The format can also be specified per-parameter using the {@link FormData#format() @FormData.format()} and {@link Query#format() @Query.format()} + * annotations. + * <p> + * Applicable to servlet class and methods. + */ + public static final String REST_paramFormat = "RestServlet.paramFormat"; + + + //-------------------------------------------------------------------------------- + // Automatically added properties. + //-------------------------------------------------------------------------------- + + /** + * The request servlet path. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getServletPath()} + */ + public static final String REST_servletPath = "RestServlet.servletPath"; + + /** + * The request servlet URI. + * <p> + * Equivalent to the value returned by {@link RestRequest#getServletURI()} + */ + public static final String REST_servletURI = "RestServlet.servletURI"; + + /** + * The request servlet URI. + * <p> + * Equivalent to the value returned by {@link RestRequest#getRelativeServletURI()} + */ + public static final String REST_relativeServletURI = "RestServlet.relativeServletURI"; + + /** + * The request URI path info. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getPathInfo()} + */ + public static final String REST_pathInfo = "RestServlet.pathInfo"; + + /** + * The request URI. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getRequestURI()} + */ + public static final String REST_requestURI = "RestServlet.requestURI"; + + /** + * The request method. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getMethod()} + */ + public static final String REST_method = "RestServlet.method"; + + /** + * The localized servlet title. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getServletTitle()} + */ + public static final String REST_servletTitle = "RestServlet.servletTitle"; + + /** + * The localized servlet description. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getServletDescription()} + */ + public static final String REST_servletDescription = "RestServlet.servletDescription"; + + /** + * The localized method summary. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getMethodSummary()} + */ + public static final String REST_methodSummary = "RestServlet.methodSummary"; + + /** + * The localized method description. + * <p> + * Automatically added to properties returned by {@link SerializerSession#getProperties()} and {@link ParserSession#getProperties()}. + * <p> + * Equivalent to the value returned by {@link RestRequest#getMethodDescription()} + */ + public static final String REST_methodDescription = "RestServlet.methodDescription"; + + private final Object resource; + private final RestConfig config; + private final boolean + allowHeaderParams, + allowBodyParam, + renderResponseStackTraces, + useStackTraceHashes; + private final String + defaultCharset, + paramFormat, + clientVersionHeader, + fullPath; + private final Set<String> allowMethodParams; + + private final ObjectMap properties; + private final Class<?>[] + beanFilters, + pojoSwaps; + private final SerializerGroup serializers; + private final ParserGroup parsers; + private final UrlEncodingSerializer urlEncodingSerializer; + private final UrlEncodingParser urlEncodingParser; + private final EncoderGroup encoders; + private final MediaType[] + supportedContentTypes, + supportedAcceptTypes; + private final Map<String,String> defaultRequestHeaders; + private final Map<String,Object> defaultResponseHeaders; + private final BeanContext beanContext; + private final RestConverter[] converters; + private final RestGuard[] guards; + private final ResponseHandler[] responseHandlers; + private final MimetypesFileTypeMap mimetypesFileTypeMap; + private final StreamResource styleSheet, favIcon; + private final Map<String,String> staticFilesMap; + private final String[] staticFilesPrefixes; + private final MessageBundle msgs; + private final ConfigFile configFile; + private final VarResolver varResolver; + private final Map<String,CallRouter> callRouters; + private final Map<String,CallMethod> callMethods; + private final Map<String,RestContext> childResources; + private final RestLogger logger; + private final RestCallHandler callHandler; + private final RestInfoProvider infoProvider; + private final RestException initException; + + // In-memory cache of images and stylesheets in the org.apache.juneau.rest.htdocs package. + private final Map<String,StreamResource> staticFilesCache = new ConcurrentHashMap<String,StreamResource>(); + private final Map<String,byte[]> resourceStreams = new ConcurrentHashMap<String,byte[]>(); + private final Map<String,String> resourceStrings = new ConcurrentHashMap<String,String>(); + private final ConcurrentHashMap<Integer,AtomicInteger> stackTraceHashes = new ConcurrentHashMap<Integer,AtomicInteger>(); + + + /** + * Constructor. + * + * @param resource The resource class (a class annotated with {@link RestResource @RestResource}). + * @param config The servlet configuration object. + * @throws Exception If any initialization problems were encountered. + */ + @SuppressWarnings("unchecked") + public RestContext(Object resource, RestConfig config) throws Exception { + super(null); + try { + this.resource = resource; + this.config = config; + + Builder b = new Builder(resource, config); + this.allowHeaderParams = b.allowHeaderParams; + this.allowBodyParam = b.allowBodyParam; + this.renderResponseStackTraces = b.renderResponseStackTraces; + this.useStackTraceHashes = b.useStackTraceHashes; + this.allowMethodParams = Collections.unmodifiableSet(b.allowMethodParams); + this.defaultCharset = b.defaultCharset; + this.paramFormat = b.paramFormat; + this.varResolver = b.varResolver; + this.configFile = b.configFile; + this.properties = b.properties; + this.beanFilters = b.beanFilters; + this.pojoSwaps = b.pojoSwaps; + this.serializers = b.serializers; + this.parsers = b.parsers; + this.urlEncodingSerializer = b.urlEncodingSerializer; + this.urlEncodingParser = b.urlEncodingParser; + this.encoders = b.encoders; + this.supportedContentTypes = ArrayUtils.toObjectArray(b.supportedContentTypes, MediaType.class); + this.supportedAcceptTypes = ArrayUtils.toObjectArray(b.supportedAcceptTypes, MediaType.class); + this.clientVersionHeader = b.clientVersionHeader; + this.defaultRequestHeaders = Collections.unmodifiableMap(b.defaultRequestHeaders); + this.defaultResponseHeaders = Collections.unmodifiableMap(b.defaultResponseHeaders); + this.beanContext = b.beanContext; + this.converters = b.converters.toArray(new RestConverter[b.converters.size()]); + this.guards = b.guards.toArray(new RestGuard[b.guards.size()]); + this.responseHandlers = ArrayUtils.toObjectArray(b.responseHandlers, ResponseHandler.class); + this.mimetypesFileTypeMap = b.mimetypesFileTypeMap; + this.styleSheet = b.styleSheet; + this.favIcon = b.favIcon; + this.staticFilesMap = Collections.unmodifiableMap(b.staticFilesMap); + this.staticFilesPrefixes = b.staticFilesPrefixes; + this.msgs = b.messageBundle; + this.childResources = Collections.synchronizedMap(new LinkedHashMap<String,RestContext>()); // Not unmodifiable on purpose so that children can be replaced. + this.logger = b.logger; + this.fullPath = b.fullPath; + + //---------------------------------------------------------------------------------------------------- + // Initialize the child resources. + // Done after initializing fields above since we pass this object to the child resources. + //---------------------------------------------------------------------------------------------------- + List<String> methodsFound = new LinkedList<String>(); // Temporary to help debug transient duplicate method issue. + Map<String,CallRouter.Builder> routers = new LinkedHashMap<String,CallRouter.Builder>(); + Map<String,CallMethod> _javaRestMethods = new LinkedHashMap<String,CallMethod>(); + for (java.lang.reflect.Method method : resource.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()); + + CallMethod sm = new CallMethod(resource, method, this); + _javaRestMethods.put(method.getName(), sm); + + String httpMethod = sm.getHttpMethod(); + if (! routers.containsKey(httpMethod)) + routers.put(httpMethod, new CallRouter.Builder(httpMethod)); + + routers.get(httpMethod).add(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); + } + } + } + this.callMethods = Collections.unmodifiableMap(_javaRestMethods); + + Map<String,CallRouter> _callRouters = new LinkedHashMap<String,CallRouter>(); + for (CallRouter.Builder crb : routers.values()) + _callRouters.put(crb.getHttpMethodName(), crb.build()); + this.callRouters = Collections.unmodifiableMap(_callRouters); + + // Initialize our child resources. + RestResourceResolver rrr = resolve(RestResourceResolver.class, config.resourceResolver); + for (Object o : config.childResources) { + String path = null; + Object r = null; + if (o instanceof Pair) { + Pair<String,Object> p = (Pair<String,Object>)o; + path = p.first(); + r = p.second(); + } else if (o instanceof Class<?>) { + Class<?> c = (Class<?>)o; + r = c; + } else { + r = o; + } + + RestConfig childConfig = null; + + if (o instanceof Class) { + Class<?> oc = (Class<?>)o; + childConfig = new RestConfig(config.inner, oc, this); + r = rrr.resolve(oc, childConfig); + } else { + r = o; + childConfig = new RestConfig(config.inner, o.getClass(), this); + } + + if (r instanceof RestServlet) { + RestServlet rs = (RestServlet)r; + rs.init(childConfig); + path = childConfig.path; + childResources.put(path, rs.getContext()); + } else { + + // Call the init(RestConfig) method. + java.lang.reflect.Method m2 = ClassUtils.findPublicMethod(r.getClass(), "init", Void.class, RestConfig.class); + if (m2 != null) + m2.invoke(r, childConfig); + + RestContext rc2 = new RestContext(r, childConfig); + + // Call the init(RestContext) method. + m2 = ClassUtils.findPublicMethod(r.getClass(), "init", Void.class, RestContext.class); + if (m2 != null) + m2.invoke(r, rc2); + + path = childConfig.path; + childResources.put(path, rc2); + } + } + + callHandler = config.callHandler == null ? new RestCallHandler(this) : resolve(RestCallHandler.class, config.callHandler, this); + infoProvider = config.infoProvider == null ? new RestInfoProvider(this) : resolve(RestInfoProvider.class, config.infoProvider, this); + + } catch (RestException e) { + initException = e; + throw e; + } catch (Exception e) { + initException = new RestException(SC_INTERNAL_SERVER_ERROR, e); + throw e; + } + initException = null; + } + + private static class Builder { + + boolean allowHeaderParams, allowBodyParam, renderResponseStackTraces, useStackTraceHashes; + VarResolver varResolver; + ConfigFile configFile; + ObjectMap properties; + Class<?>[] beanFilters; + Class<?>[] pojoSwaps; + SerializerGroup serializers; + ParserGroup parsers; + UrlEncodingSerializer urlEncodingSerializer; + UrlEncodingParser urlEncodingParser; + EncoderGroup encoders; + String clientVersionHeader = "", defaultCharset, paramFormat; + List<MediaType> supportedContentTypes, supportedAcceptTypes; + Map<String,String> defaultRequestHeaders = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER); + Map<String,Object> defaultResponseHeaders; + BeanContext beanContext; + List<RestConverter> converters = new ArrayList<RestConverter>(); + List<RestGuard> guards = new ArrayList<RestGuard>(); + List<ResponseHandler> responseHandlers = new ArrayList<ResponseHandler>(); + MimetypesFileTypeMap mimetypesFileTypeMap; + StreamResource styleSheet, favIcon; + Map<String,String> staticFilesMap; + String[] staticFilesPrefixes; + MessageBundle messageBundle; + Set<String> allowMethodParams = new LinkedHashSet<String>(); + RestLogger logger; + String fullPath; + + @SuppressWarnings("unchecked") + private Builder(Object resource, RestConfig sc) throws Exception { + + PropertyStore ps = sc.createPropertyStore(); + + LinkedHashMap<Class<?>,RestResource> restResourceAnnotationsChildFirst = ReflectionUtils.findAnnotationsMap(RestResource.class, resource.getClass()); + + allowHeaderParams = ps.getProperty(REST_allowHeaderParams, boolean.class, true); + allowBodyParam = ps.getProperty(REST_allowBodyParam, boolean.class, true); + renderResponseStackTraces = ps.getProperty(REST_renderResponseStackTraces, boolean.class, false); + useStackTraceHashes = ps.getProperty(REST_useStackTraceHashes, boolean.class, true); + defaultCharset = ps.getProperty(REST_defaultCharset, String.class, "utf-8"); + paramFormat = ps.getProperty(REST_paramFormat, String.class, ""); + + for (String m : StringUtils.split(ps.getProperty(REST_allowMethodParam, String.class, ""), ',')) + if (m.equals("true")) // For backwards compatibility when this was a boolean field. + allowMethodParams.add("*"); + else + allowMethodParams.add(m.toUpperCase()); + + varResolver = sc.varResolverBuilder + .vars(LocalizationVar.class, RequestVar.class, SerializedRequestAttrVar.class, ServletInitParamVar.class, UrlEncodeVar.class) + .build() + ; + configFile = sc.configFile.getResolving(this.varResolver); + properties = sc.properties; + Collections.reverse(sc.beanFilters); + Collections.reverse(sc.pojoSwaps); + beanFilters = ArrayUtils.toObjectArray(sc.beanFilters, Class.class); + pojoSwaps = ArrayUtils.toObjectArray(sc.pojoSwaps, Class.class); + clientVersionHeader = sc.clientVersionHeader; + + // 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 (messageBundle == null) + messageBundle = new MessageBundle(c, r.messages()); + else + messageBundle.addSearchPath(c, r.messages()); + } + } + + if (messageBundle == null) + messageBundle = new MessageBundle(resource.getClass(), ""); + + ps.addBeanFilters(beanFilters).addPojoSwaps(pojoSwaps).setProperties(properties); + + serializers = sc.serializers.beanFilters(beanFilters).pojoSwaps(pojoSwaps).properties(properties).build(); + parsers = sc.parsers.beanFilters(beanFilters).pojoSwaps(pojoSwaps).properties(properties).build(); + urlEncodingSerializer = new UrlEncodingSerializer(ps); + urlEncodingParser = new UrlEncodingParser(ps); + encoders = sc.encoders.build(); + supportedContentTypes = sc.supportedContentTypes != null ? sc.supportedContentTypes : serializers.getSupportedMediaTypes(); + supportedAcceptTypes = sc.supportedAcceptTypes != null ? sc.supportedAcceptTypes : parsers.getSupportedMediaTypes(); + defaultRequestHeaders.putAll(sc.defaultRequestHeaders); + defaultResponseHeaders = Collections.unmodifiableMap(new LinkedHashMap<String,Object>(sc.defaultResponseHeaders)); + beanContext = ps.getBeanContext(); + + for (Object o : sc.converters) + converters.add(resolve(RestConverter.class, o)); + + for (Object o : sc.guards) + guards.add(resolve(RestGuard.class, o)); + + for (Object o : sc.responseHandlers) + responseHandlers.add(resolve(ResponseHandler.class, o)); + + mimetypesFileTypeMap = sc.mimeTypes; + + VarResolver vr = sc.getVarResolverBuilder().build(); + + if (sc.styleSheets != null) { + List<InputStream> contents = new ArrayList<InputStream>(); + for (Object o : sc.styleSheets) { + if (o instanceof Pair) { + Pair<Class<?>,String> p = (Pair<Class<?>,String>)o; + for (String path : StringUtils.split(vr.resolve(StringUtils.toString(p.second())), ',')) + if (path.startsWith("file://")) + contents.add(new FileInputStream(path)); + else + contents.add(ReflectionUtils.getResource(p.first(), path)); + } else { + contents.add(IOUtils.toInputStream(o)); + } + } + styleSheet = new StreamResource(MediaType.forString("text/css"), contents.toArray()); + } + + if (sc.favIcon != null) { + Object o = sc.favIcon; + InputStream is = null; + if (o instanceof Pair) { + Pair<Class<?>,String> p = (Pair<Class<?>,String>)o; + is = ReflectionUtils.getResource(p.first(), vr.resolve(p.second())); + } else { + is = IOUtils.toInputStream(o); + } + if (is != null) + favIcon = new StreamResource(MediaType.forString("image/x-icon"), is); + } + + staticFilesMap = new LinkedHashMap<String,String>(); + if (sc.staticFiles != null) { + for (Object o : sc.staticFiles) { + if (o instanceof Pair) { + Pair<Class<?>,String> p = (Pair<Class<?>,String>)o; + // TODO - Currently doesn't take parent class location into account. + staticFilesMap.putAll(JsonParser.DEFAULT.parse(vr.resolve(p.second()), LinkedHashMap.class)); + } else { + throw new RuntimeException("TODO"); + } + } + } + staticFilesPrefixes = staticFilesMap.keySet().toArray(new String[0]); + + logger = sc.logger == null ? new RestLogger.NoOp() : resolve(RestLogger.class, sc.logger); + + fullPath = (sc.parentContext == null ? "" : (sc.parentContext.fullPath + '/')) + sc.path; + } + } + + /** + * Returns the variable resolver for this servlet. + * <p> + * Variable resolvers are used to replace variables in property values. + * </p> + * <h6 class='figure'>Example:</h6> + * <p class='bcode'> + * <ja>@RestResource</ja>( + * messages=<js>"nls/Messages"</js>, + * properties={ + * <ja>@Property</ja>(name=<js>"title"</js>,value=<js>"$L{title}"</js>), <jc>// Localized variable in Messages.properties</jc> + * <ja>@Property</ja>(name=<js>"javaVendor"</js>,value=<js>"$S{java.vendor,Oracle}"</js>), <jc>// System property with default value</jc> + * <ja>@Property</ja>(name=<js>"foo"</js>,value=<js>"bar"</js>), + * <ja>@Property</ja>(name=<js>"bar"</js>,value=<js>"baz"</js>), + * <ja>@Property</ja>(name=<js>"v1"</js>,value=<js>"$R{foo}"</js>), <jc>// Request variable. value="bar"</jc> + * <ja>@Property</ja>(name=<js>"v2"</js>,value=<js>"$R{$R{foo}}"</js>) <jc>// Nested request variable. value="baz"</jc> + * } + * ) + * <jk>public class</jk> MyRestResource <jk>extends</jk> RestServletDefault { + * </p> + * <p> + * A typical usage pattern is using variables for resolving URL links when rendering HTML: + * </p> + * <p class='bcode'> + * <ja>@RestMethod</ja>( + * name=<js>"GET"</js>, path=<js>"/{name}/*"</js>, + * properties={ + * <ja>@Property</ja>( + * name=<jsf>HTMLDOC_links</jsf>, + * value=<js>"{up:'$R{requestParentURI}', options:'?method=OPTIONS', editLevel:'$R{servletURI}/editLevel?logger=$R{attribute.name}'}"</js> + * ) + * } + * ) + * <jk>public</jk> LoggerEntry getLogger(RestRequest req, <ja>@Path</ja> String name) <jk>throws</jk> Exception { + * </p> + * <p> + * Calls to <code>req.getProperties().getString(<js>"key"</js>)</code> returns strings with variables resolved. + * + * @return The var resolver in use by this resource. + */ + public VarResolver getVarResolver() { + return varResolver; + } + + /** + * Returns the config file associated with this servlet. + * <p> + * The config file is identified via one of the following: + * <ul> + * <li>{@link RestResource#config() @RestResource.config()} annotation. + * <li>{@link RestConfig#setConfigFile(ConfigFile)} method. + * </ul> + * + * @return The resolving config file associated with this servlet. Never <jk>null</jk>. + */ + public ConfigFile getConfigFile() { + return configFile; + } + + /** + * Resolve a static resource file. + * <p> + * The location of static resources are defined via one of the following: + * <ul> + * <li>{@link RestResource#staticFiles() @RestResource.staticFiles()} annotation. + * <li>{@link RestConfig#addStaticFiles(Class, String)} method. + * </ul> + * + * @param pathInfo The unencoded path info. + * @return The resource, or <jk>null</jk> if the resource could not be resolved. + * @throws IOException + */ + public 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, null); + if (is != null) { + try { + int i = p2.lastIndexOf('/'); + String name = (i == -1 ? p2 : p2.substring(i+1)); + String mediaType = mimetypesFileTypeMap.getContentType(name); + ObjectMap headers = new ObjectMap().append("Cache-Control", "max-age=86400, public"); + staticFilesCache.put(pathInfo, new StreamResource(MediaType.forString(mediaType), headers, is)); + return staticFilesCache.get(pathInfo); + } finally { + is.close(); + } + } + } + } + } + } + return staticFilesCache.get(pathInfo); + } + + /** + * Same as {@link Class#getResourceAsStream(String)} except if it doesn't find the resource + * on this class, searches up the parent hierarchy chain. + * <p> + * If the resource cannot be found in the classpath, then an attempt is made to look in the + * JVM working directory. + * <p> + * If the <code>locale</code> is specified, then we look for resources whose name matches that locale. + * For example, if looking for the resource <js>"MyResource.txt"</js> for the Japanese locale, we will + * look for files in the following order: + * <ol> + * <li><js>"MyResource_ja_JP.txt"</js> + * <li><js>"MyResource_ja.txt"</js> + * <li><js>"MyResource.txt"</js> + * </ol> + * + * @param name The resource name. + * @param locale Optional locale. + * @return An input stream of the resource, or <jk>null</jk> if the resource could not be found. + * @throws IOException + */ + protected InputStream getResource(String name, Locale locale) throws IOException { + String n = (locale == null || locale.toString().isEmpty() ? name : name + '|' + locale); + if (! resourceStreams.containsKey(n)) { + InputStream is = ReflectionUtils.getLocalizedResource(resource.getClass(), name, locale); + if (is == null && name.indexOf("..") == -1) { + for (String n2 : FileUtils.getCandidateFileNames(name, locale)) { + File f = new File(n2); + if (f.exists() && f.canRead()) { + is = new FileInputStream(f); + break; + } + } + } + if (is != null) { + try { + resourceStreams.put(n, ByteArrayCache.DEFAULT.cache(is)); + } finally { + is.close(); + } + } + } + byte[] b = resourceStreams.get(n); + return b == null ? null : new ByteArrayInputStream(b); + } + + /** + * Reads the input stream from {@link #getResource(String, Locale)} into a String. + * + * @param name The resource name. + * @param locale Optional locale. + * @return The contents of the stream as a string, or <jk>null</jk> if the resource could not be found. + * @throws IOException If resource could not be found. + */ + public String getResourceAsString(String name, Locale locale) throws IOException { + String n = (locale == null || locale.toString().isEmpty() ? name : name + '|' + locale); + if (! resourceStrings.containsKey(n)) { + String s = IOUtils.read(getResource(name, locale)); + if (s == null) + throw new IOException("Resource '"+name+"' not found."); + resourceStrings.put(n, s); + } + return resourceStrings.get(n); + } + + /** + * Reads the input stream from {@link #getResource(String, Locale)} and parses it into a POJO + * using the parser matched by the specified media type. + * <p> + * Useful if you want to load predefined POJOs from JSON files in your classpath. + * + * @param c The class type of the POJO to create. + * @param mediaType The media type of the data in the stream (e.g. <js>"text/json"</js>) + * @param name The resource name (e.g. "htdocs/styles.css"). + * @param locale Optional locale. + * @return The parsed resource, or <jk>null</jk> if the resource could not be found. + * @throws IOException + * @throws ServletException If the media type was unknown or the input could not be parsed into a POJO. + */ + public <T> T getResource(Class<T> c, MediaType mediaType, String name, Locale locale) throws IOException, ServletException { + InputStream is = getResource(name, locale); + if (is == null) + return null; + try { + Parser p = parsers.getParser(mediaType); + if (p != null) { + try { + if (p.isReaderParser()) + return p.parse(new InputStreamReader(is, IOUtils.UTF8), c); + return p.parse(is, c); + } catch (ParseException e) { + throw new ServletException("Could not parse resource '' as media type '"+mediaType+"'."); + } + } + throw new ServletException("Unknown media type '"+mediaType+"'"); + } catch (Exception e) { + throw new ServletException("Could not parse resource with name '"+name+"'", e); + } + } + + /** + * Returns the path for this resource as defined by the {@link RestResource#path()} annotation or {@link RestConfig#setPath(String)} method + * concatenated with those on all parent classes. + * <p> + * If path is not specified, returns <js>"/"</js>. + * <p> + * Path always starts with <js>"/"</js>. + * + * @return The servlet path. + */ + public String getPath() { + return fullPath; + } + + /** + * Returns the logger to use for this resource. + * <p> + * The logger for a resource is defined via one of the following: + * <ul> + * <li>{@link RestResource#logger() @RestResource.logger()} annotation. + * <li>{@link RestConfig#setLogger(Class)}/{@link RestConfig#setLogger(RestLogger)} methods. + * </ul> + * + * @return The logger to use for this resource. Never <jk>null</jk>. + */ + public RestLogger getLogger() { + return logger; + } + + /** + * Returns the resource bundle used by this resource. + * <p> + * The resource bundle is defined via one of the following: + * <ul> + * <li>{@link RestResource#messages() @RestResource.messages()} annotation. + * </ul> + * + * @return The resource bundle for this resource. Never <jk>null</jk>. + */ + public MessageBundle getMessages() { + return msgs; + } + + /** + * Returns the REST information provider used by this resource. + * <p> + * The information provider is defined via one of the following: + * <ul> + * <li>{@link RestResource#infoProvider() @RestResource.infoProvider()} annotation. + * <li>{@link RestConfig#setInfoProvider(Class)}/{@link RestConfig#setInfoProvider(RestInfoProvider)} methods. + * </ul> + * + * @return The information provider for this resource. Never <jk>null</jk>. + */ + public RestInfoProvider getInfoProvider() { + return infoProvider; + } + + /** + * Returns the REST call handler used by this resource. + * <p> + * The call handler is defined via one of the following: + * <ul> + * <li>{@link RestResource#callHandler() @RestResource.callHandler()} annotation. + * <li>{@link RestConfig#setCallHandler(Class)}/{@link RestConfig#setCallHandler(RestCallHandler)} methods. + * </ul> + * + * @return The call handler for this resource. Never <jk>null</jk>. + */ + protected RestCallHandler getCallHandler() { + return callHandler; + } + + /** + * Returns a map of HTTP method names to call routers. + * + * @return A map with HTTP method names uppercased as the keys, and call routers as the values. + */ + protected Map<String,CallRouter> getCallRouters() { + return callRouters; + } + + /** + * Returns the resource object. + * <p> + * This is the instance of the class annotated with the {@link RestResource @RestResource} annotation, usually + * an instance of {@link RestServlet}. + * + * @return The resource object. Never <jk>null</jk>. + */ + protected Object getResource() { + return resource; + } + + /** + * Returns the resource object as a {@link RestServlet}. + * + * @return The resource object cast to {@link RestServlet}, or + * <jk>null</jk> if the resource doesn't subclass from {@link RestServlet} + */ + protected RestServlet getRestServlet() { + return resource instanceof RestServlet ? (RestServlet)resource : null; + } + + /** + * Throws a {@link RestException} if an exception occurred in the constructor of this object. + * + * @throws RestException The initialization exception wrapped in a {@link RestException}. + */ + protected void checkForInitException() throws RestException { + if (initException != null) + throw initException; + } + + /** + * Returns the {@link BeanContext} object used for parsing path variables and header values. + * + * @return The bean context used for parsing path variables and header values. + */ + public BeanContext getBeanContext() { + return beanContext; + } + + /** + * Returns the class-level properties associated with this servlet. + * <p> + * Properties at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#properties() @RestResource.properties()} annotation. + * <li>{@link RestConfig#setProperty(String, Object)}/{@link RestConfig#setProperties(Map)} methods. + * </ul> + * <p> + * <h5 class='section'>Notes:</h5> + * <ul> + * <li>The returned {@code Map} is mutable. Therefore, subclasses are free to override + * or set additional initialization parameters in their {@code init()} method. + * </ul> + * + * @return The resource properties as an {@link ObjectMap}. + */ + public ObjectMap getProperties() { + return properties; + } + + /** + * Returns the serializers registered with this resource. + * <p> + * Serializers at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#serializers() @RestResource.serializers()} annotation. + * <li>{@link RestConfig#addSerializers(Class...)}/{@link RestConfig#addSerializers(Serializer...)} methods. + * </ul> + * + * @return The serializers registered with this resource. + */ + public SerializerGroup getSerializers() { + return serializers; + } + + /** + * Returns the parsers registered with this resource. + * <p> + * Parsers at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#parsers() @RestResource.parsers()} annotation. + * <li>{@link RestConfig#addParsers(Class...)}/{@link RestConfig#addParsers(Parser...)} methods. + * </ul> + * + * @return The parsers registered with this resource. + */ + public ParserGroup getParsers() { + return parsers; + } + + /** + * Returns the servlet init parameter returned by {@link ServletConfig#getInitParameter(String)}. + * + * @param name The init parameter name. + * @return The servlet init parameter, or <jk>null</jk> if not found. + */ + public String getServletInitParameter(String name) { + return config.getInitParameter(name); + } + + /** + * 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,RestContext> getChildResources() { + return Collections.unmodifiableMap(childResources); + } + + /** + * Returns the number of times this exception was thrown based on a hash of its stacktrace. + * + * @param e The exception to check. + * @return The number of times this exception was thrown, or <code>0</code> if <code>stackTraceHashes</code> + * setting is not enabled. + */ + protected int getStackTraceOccurrence(Throwable e) { + if (! useStackTraceHashes) + return 0; + int h = e.hashCode(); + stackTraceHashes.putIfAbsent(h, new AtomicInteger()); + return stackTraceHashes.get(h).incrementAndGet(); + } + + /** + * Returns the value of the {@link #REST_renderResponseStackTraces} setting. + * @return The value of the {@link #REST_renderResponseStackTraces} setting. + */ + protected boolean isRenderResponseStackTraces() { + return renderResponseStackTraces; + } + + /** + * Returns the value of the {@link #REST_allowHeaderParams} setting. + * @return The value of the {@link #REST_allowHeaderParams} setting. + */ + protected boolean isAllowHeaderParams() { + return allowHeaderParams; + } + + /** + * Returns the value of the {@link #REST_allowBodyParam} setting. + * @return The value of the {@link #REST_allowBodyParam} setting. + */ + protected boolean isAllowBodyParam() { + return allowBodyParam; + } + + /** + * Returns the value of the {@link #REST_defaultCharset} setting. + * @return The value of the {@link #REST_defaultCharset} setting. + */ + protected String getDefaultCharset() { + return defaultCharset; + } + + /** + * Returns the value of the {@link #REST_paramFormat} setting. + * @return The value of the {@link #REST_paramFormat} setting. + */ + protected String getParamFormat() { + return paramFormat; + } + + /** + * Returns the name of the client version header name used by this resource. + * <p> + * The client version header is the name of the HTTP header on requests that identify a client version. + * <p> + * The client version header is defined via one of the following: + * <ul> + * <li>{@link RestResource#clientVersionHeader() @RestResource.clientVersion()} annotation. + * </ul> + * + * @return The name of the client version header used by this resource. Never <jk>null</jk>. + */ + protected String getClientVersionHeader() { + return clientVersionHeader; + } + + /** + * Returns <jk>true</jk> if the specified <code>Method</code> GET parameter value can be used to override + * the method name in the HTTP header. + * + * @param m The method name, uppercased. + * @return <jk>true</jk> if this resource allows the specified method to be overridden. + */ + protected boolean allowMethodParam(String m) { + return (! StringUtils.isEmpty(m) && (allowMethodParams.contains(m) || allowMethodParams.contains("*"))); + } + + /** + * Returns the bean filters associated with this resource. + * <p> + * Bean filters at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#beanFilters() @RestResource.beanFilters()} annotation. + * <li>{@link RestConfig#addBeanFilters(Class...)} method. + * </ul> + * + * @return The bean filters associated with this resource. Never <jk>null</jk>. + */ + protected Class<?>[] getBeanFilters() { + return beanFilters; + } + + /** + * Returns the POJO swaps associated with this resource. + * <p> + * POJO swaps at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#pojoSwaps() @RestResource.pojoSwaps()} annotation. + * <li>{@link RestConfig#addPojoSwaps(Class...)} method. + * </ul> + * + * @return The POJO swaps associated with this resource. Never <jk>null</jk>. + */ + protected Class<?>[] getPojoSwaps() { + return pojoSwaps; + } + + /** + * Returns the URL-encoding parser associated with this resource. + * @return The URL-encoding parser associated with this resource. Never <jk>null</jk>. + */ + protected UrlEncodingParser getUrlEncodingParser() { + return urlEncodingParser; + } + + /** + * Returns the URL-encoding serializer associated with this resource. + * @return The URL-encoding serializer associated with this resource. Never <jk>null</jk>. + */ + protected UrlEncodingSerializer getUrlEncodingSerializer() { + return urlEncodingSerializer; + } + + /** + * Returns the encoders associated with this resource. + * <p> + * Encoders are used to provide various types of encoding such as <code>gzip</code> encoding. + * <p> + * Encoders at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#encoders() @RestResource.encoders()} annotation. + * <li>{@link RestConfig#addEncoders(Class...)}/{@link RestConfig#addEncoders(org.apache.juneau.encoders.Encoder...)} methods. + * </ul> + * + * @return The encoders associated with this resource. Never <jk>null</jk>. + */ + protected EncoderGroup getEncoders() { + return encoders; + } + + /** + * Returns the explicit list of supported accept types for this resource. + * <p> + * By default, this is simply the list of accept types supported by the registered parsers, but + * can be overridden via the {@link RestConfig#setSupportedAcceptTypes(MediaType...)}/{@link RestConfig#setSupportedAcceptTypes(String...)} methods. + * + * @return The supported <code>Accept</code> header values for this resource. Never <jk>null</jk>. + */ + protected MediaType[] getSupportedAcceptTypes() { + return supportedAcceptTypes; + } + + /** + * Returns the explicit list of supported content types for this resource. + * <p> + * By default, this is simply the list of content types supported by the registered serializers, but + * can be overridden via the {@link RestConfig#setSupportedContentTypes(MediaType...)}/{@link RestConfig#setSupportedContentTypes(String...)} methods. + * + * @return The supported <code>Content-Type</code> header values for this resource. Never <jk>null</jk>. + */ + protected MediaType[] getSupportedContentTypes() { + return supportedContentTypes; + } + + /** + * Returns the default request headers for this resource. + * <p> + * These are headers automatically added to requests if not present. + * <p> + * Default request headers are defined via one of the following: + * <ul> + * <li>{@link RestResource#defaultRequestHeaders() @RestResource.defaultRequestHeaders()} annotation. + * <li>{@link RestConfig#addDefaultRequestHeader(String, Object)}/{@link RestConfig#addDefaultRequestHeaders(String...)} methods. + * </ul> + * + * @return The default request headers for this resource. Never <jk>null</jk>. + */ + protected Map<String,String> getDefaultRequestHeaders() { + return defaultRequestHeaders; + } + + /** + * Returns the default response headers for this resource. + * <p> + * These are headers automatically added to responses if not otherwise specified during the request. + * <p> + * Default response headers are defined via one of the following: + * <ul> + * <li>{@link RestResource#defaultResponseHeaders() @RestResource.defaultResponseHeaders()} annotation. + * <li>{@link RestConfig#addDefaultResponseHeader(String, Object)}/{@link RestConfig#addDefaultResponseHeaders(String...)} methods. + * </ul> + * + * @return The default response headers for this resource. Never <jk>null</jk>. + */ + public Map<String,Object> getDefaultResponseHeaders() { + return defaultResponseHeaders; + } + + /** + * Returns the converters associated with this resource at the class level. + * <p> + * Converters are used to 'convert' POJOs from one form to another before being passed of to the response handlers. + * <p> + * Converters at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#converters() @RestResource.converters()} annotation. + * <li>{@link RestConfig#addConverters(Class...)}/{@link RestConfig#addConverters(RestConverter...)} methods. + * </ul> + * + * @return The converters associated with this resource. Never <jk>null</jk>. + */ + protected RestConverter[] getConverters() { + return converters; + } + + /** + * Returns the guards associated with this resource at the class level. + * <p> + * Guards are used to restrict access to resources. + * <p> + * Guards at the class level are defined via one of the following: + * <ul> + * <li>{@link RestResource#guards() @RestResource.guards()} annotation. + * <li>{@link RestConfig#addGuards(Class...)}/{@link RestConfig#addGuards(RestGuard...)} methods. + * </ul> + * + * @return The guards associated with this resource. Never <jk>null</jk>. + */ + protected RestGuard[] getGuards() { + return guards; + } + + /** + * Returns the response handlers associated with this resource. + * <p> + * Response handlers are used to convert POJOs returned by REST Java methods into actual HTTP responses. + * <p> + * Response handlers are defined via one of the following: + * <ul> + * <li>{@link RestResource#responseHandlers() @RestResource.responseHandlers()} annotation. + * <li>{@link RestConfig#addResponseHandlers(Class...)}/{@link RestConfig#addResponseHandlers(ResponseHandler...)} methods. + * </ul> + * + * @return The response handlers associated with this resource. Never <jk>null</jk>. + */ + protected ResponseHandler[] getResponseHandlers() { + return responseHandlers; + } + + /** + * Returns the media type for the specified file name. + * <p> + * The list of MIME-type mappings can be augmented through the {@link RestConfig#addMimeTypes(String...)} method. + * See that method for a description of predefined MIME-type mappings. + * + * @param name The file name. + * @return The MIME-type, or <jk>null</jk> if it could not be determined. + */ + protected String getMediaTypeForName(String name) { + return mimetypesFileTypeMap.getContentType(name); + } + + /** + * Returns the favicon of the resource. + * <p> + * This is the icon served up under <js>"/favicon.ico"</jk> recognized by browsers. + * <p> + * The favicon is defined via one of the following: + * <ul> + * <li>{@link RestResource#favicon() @RestResource.favicon()} annotation. + * <li>{@link RestConfig#setFavIcon(Object)}/{@link RestConfig#setFavIcon(Class, String)} methods. + * </ul> + * + * @return The favicon of this resource. Can be <jk>null</jk>. + */ + protected StreamResource getFavIcon() { + return favIcon; + } + + /** + * Returns the stylesheet for use in the HTML views of the resource. + * <p> + * This is the contents of the page served up under <js>"/styles.css"</jk>. + * <p> + * The stylesheet is defined via one of the following: + * <ul> + * <li>{@link RestResource#stylesheet() @RestResource.stylesheet()} annotation. + * <li>{@link RestConfig#setStyleSheet(Object...)}/{@link RestConfig#setStyleSheet(Class, String)} methods. + * </ul> + * + * @return The aggregated stylesheet of this resource. Never <jk>null</jk>. + */ + protected StreamResource getStyleSheet() { + return styleSheet; + } + + /** + * Returns <jk>true</jk> if the specified path refers to a static file. + * <p> + * Static files are files pulled from the classpath and served up directly to the browser. + * <p> + * Static files are defined via one of the following: + * <ul> + * <li>{@link RestResource#staticFiles() @RestResource.staticFiles()} annotation. + * <li>{@link RestConfig#addStaticFiles(Class, String)} method. + * </ul> + * + * @param p The URL path remainder after the servlet match. + * @return <jk>true</jk> if the specified path refers to a static file. + */ + protected boolean isStaticFile(String p) { + return StringUtils.pathStartsWith(p, staticFilesPrefixes); + } + + /** + * Returns the REST Java methods defined in this resource. + * <p> + * These are the methods annotated with the {@link RestMethod @RestMethod} annotation. + * + * @return A map of Java method names to call method objects. + */ + protected Map<String,CallMethod> getCallMethods() { + return callMethods; + } + + /** + * Calls {@link Servlet#destroy()} on any child resources defined on this resource. + */ + protected void destroy() { + for (RestContext r : childResources.values()) { + r.destroy(); + if (r.resource instanceof Servlet) + ((Servlet)r.resource).destroy(); + } + } + + /** + * Returns <jk>true</jk> if this resource has any child resources associated with it. + * @return <jk>true</jk> if this resource has any child resources associated with it. + */ + protected boolean hasChildResources() { + return ! childResources.isEmpty(); + } + + /** + * Returns the context of the child resource associated with the specified path. + * + * @param path The path of the child resource to resolve. + * @return The resolved context, or <jk>null</jk> if it could not be resolved. + */ + protected RestContext getChildResource(String path) { + return childResources.get(path); + } + + + //---------------------------------------------------------------------------------------------------- + // Utility methods + //---------------------------------------------------------------------------------------------------- + + /** + * Takes in an object of type T or a Class<T> and either casts or constructs a T. + */ + @SuppressWarnings("unchecked") + private static <T> T resolve(Class<T> c, Object o, Object...cArgs) throws RestServletException { + if (c.isInstance(o)) + return (T)o; + if (! (o instanceof Class)) + throw new RestServletException("Invalid object type passed to resolve: ''{0}''. Must be an object of type T or a Class<? extend T>.", o.getClass()); + Constructor<T> n = ClassUtils.findPublicConstructor((Class<T>)o, cArgs); + if (n == null) + throw new RestServletException("Could not find public constructor for class ''{0}'' that takes in args {1}", c, JsonSerializer.DEFAULT_LAX.toString(ClassUtils.getClasses(cArgs))); + try { + return n.newInstance(cArgs); + } catch (Exception e) { + throw new RestServletException("Exception occurred while constructing class ''{0}''", c).initCause(e); + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/RestException.java ---------------------------------------------------------------------- diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestException.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestException.java index 77d695a..299883a 100644 --- a/juneau-rest/src/main/java/org/apache/juneau/rest/RestException.java +++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestException.java @@ -116,9 +116,9 @@ public class RestException extends RuntimeException { /** * Returns the number of times this exception occurred on this servlet. * <p> - * This only gets set if {@link RestServletContext#REST_useStackTraceHashes} is enabled on the servlet. + * This only gets set if {@link RestContext#REST_useStackTraceHashes} is enabled on the servlet. * - * @return The occurrence number if {@link RestServletContext#REST_useStackTraceHashes} is enabled, or <code>0</code> otherwise. + * @return The occurrence number if {@link RestContext#REST_useStackTraceHashes} is enabled, or <code>0</code> otherwise. */ public int getOccurrence() { return occurrence; http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/RestInfoProvider.java ---------------------------------------------------------------------- diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestInfoProvider.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestInfoProvider.java new file mode 100644 index 0000000..e43f514 --- /dev/null +++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestInfoProvider.java @@ -0,0 +1,499 @@ +// *************************************************************************************************************************** +// * 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.rest; + +import static javax.servlet.http.HttpServletResponse.*; +import static org.apache.juneau.dto.swagger.SwaggerBuilder.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.apache.juneau.*; +import org.apache.juneau.dto.swagger.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.rest.annotation.*; +import org.apache.juneau.svl.*; + +/** + * Class that provides documentation and other related information about a REST resource. + * <p> + * Subclasses can override these methods to tailor how HTTP REST resources are documented. + * Subclasses MUST implement a public constructor that takes in a {@link RestContext} object. + * <p> + * RestInfoProviders are associated with servlets/resources in one of the following ways: + * <ul> + * <li>The {@link RestResource#infoProvider @RestResource.infoProvider()} annotation. + * <li>The {@link RestConfig#setInfoProvider(Class)}/{@link RestConfig#setInfoProvider(RestInfoProvider)} methods. + * </ul> + */ +@SuppressWarnings("hiding") +public class RestInfoProvider { + + private final RestContext context; + private final String + title, + description, + termsOfService, + contact, + license, + version, + tags, + externalDocs; + private final ConcurrentHashMap<Locale,Swagger> swaggers = new ConcurrentHashMap<Locale,Swagger>(); + + /** + * Constructor. + * @param context The resource context. + */ + public RestInfoProvider(RestContext context) { + this.context = context; + + Builder b = new Builder(context); + this.title = b.title; + this.description = b.description; + this.termsOfService = b.termsOfService; + this.contact = b.contact; + this.license = b.license; + this.version = b.version; + this.tags = b.tags; + this.externalDocs = b.externalDocs; + } + + private static class Builder { + private String + title, + description, + termsOfService, + contact, + license, + version, + tags, + externalDocs; + + Builder(RestContext context) { + + LinkedHashMap<Class<?>,RestResource> restResourceAnnotationsParentFirst = ReflectionUtils.findAnnotationsMapParentFirst(RestResource.class, context.getResource().getClass()); + + for (RestResource r : restResourceAnnotationsParentFirst.values()) { + if (! r.title().isEmpty()) + title = r.title(); + if (! r.description().isEmpty()) + description = r.description(); + if (! r.termsOfService().isEmpty()) + termsOfService = r.termsOfService(); + if (! r.contact().isEmpty()) + contact = r.contact(); + if (! r.license().isEmpty()) + license = r.license(); + if (! r.version().isEmpty()) + version = r.version(); + if (! r.tags().isEmpty()) + tags = r.tags(); + if (! r.externalDocs().isEmpty()) + externalDocs = r.externalDocs(); + } + } + } + + /** + * Returns the localized swagger for this REST resource. + * + * @param req The incoming HTTP request. + * @return A new Swagger instance. + * @throws RestException + */ + protected Swagger getSwagger(RestRequest req) throws RestException { + try { + // If a file is defined, use that. + Swagger s = req.getSwaggerFromFile(); + if (s != null) + return s; + + s = swagger( + info(getTitle(req), getVersion(req)) + .contact(getContact(req)) + .license(getLicense(req)) + .description(getDescription(req)) + .termsOfService(getTermsOfService(req)) + ) + .consumes(context.getSupportedAcceptTypes()) + .produces(context.getSupportedContentTypes()) + .tags(getTags(req)) + .externalDocs(getExternalDocs(req)); + + for (CallMethod sm : context.getCallMethods().values()) { + if (sm.isRequestAllowed(req)) { + Operation o = sm.getSwaggerOperation(req); + s.path( + sm.getPathPattern(), + sm.getHttpMethod().toLowerCase(), + o + ); + } + } + return s; + } catch (RestException e) { + throw e; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the localized Swagger from the file system. + * <p> + * Looks for a file called <js>"{ServletClass}_{locale}.json"</js> in the same package + * as this servlet and returns it as a parsed {@link Swagger} object. + * <p> + * Returned objects are cached for later quick-lookup. + * + * @param locale The locale of the swagger. + * @return The parsed swagger object, or <jk>null</jk> if the swagger file could not be found. + * @throws RestException + */ + protected Swagger getSwaggerFromFile(Locale locale) throws RestException { + Swagger s = swaggers.get(locale); + if (s == null) { + try { + s = context.getResource(Swagger.class, MediaType.JSON, getClass().getSimpleName() + ".json", locale); + swaggers.putIfAbsent(locale, s == null ? Swagger.NULL : s); + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + return s == Swagger.NULL ? null : s; + } + + /** + * Returns the localized summary of the specified java method on this servlet. + * <p> + * Subclasses can override this method to provide their own summary. + * <p> + * The default implementation returns the summary from the following locations (whichever matches first): + * </p> + * <ol> + * <li>{@link RestMethod#summary() @RestMethod.summary()} annotation on the method. + * <li><ck>[ClassName].[javaMethodName].summary</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>[javaMethodName].summary</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 summary of the method, or a blank string if no summary was found. + */ + public String getMethodSummary(String javaMethodName, RestRequest req) { + CallMethod m = context.getCallMethods().get(javaMethodName); + if (m != null) + return m.getSummary(req); + return ""; + } + + /** + * Returns the localized description of the specified java method on this servlet. + * <p> + * Subclasses can override this method to provide their own description. + * <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].description</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>[javaMethodName].description</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. + */ + protected String getMethodDescription(String javaMethodName, RestRequest req) { + CallMethod m = context.getCallMethods().get(javaMethodName); + if (m != null) + return m.getDescription(req); + return ""; + } + + /** + * Returns the localized title of this REST resource. + * <p> + * Subclasses can override this method to provide their own title. + * <p> + * The default implementation returns the description from the following locations (whichever matches first): + * <p> + * <ol> + * <li>{@link RestResource#title() @RestResourcel.title()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].title</ck> property in resource bundle identified by {@link RestResource#messages() @ResourceBundle.messages()} + * annotation for this class, then any parent classes. + * <li><ck>title</ck> in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/title</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized description of this REST resource, or <jk>null</jk> if no resource description was found. + */ + public String getTitle(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (this.title != null) + return vr.resolve(this.title); + String title = context.getMessages().findFirstString(req.getLocale(), "title"); + if (title != null) + return vr.resolve(title); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getTitle(); + return null; + } + + /** + * Returns the localized description of this REST resource. + * <p> + * Subclasses can override this method to provide their own description. + * <p> + * The default implementation returns the description from the following locations (whichever matches first): + * <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. + * <li><ck>/info/description</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized description of this REST resource, or <jk>null</jk> if no resource description was found. + */ + public String getDescription(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (this.description != null) + return vr.resolve(this.description); + String description = context.getMessages().findFirstString(req.getLocale(), "description"); + if (description != null) + return vr.resolve(description); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getDescription(); + return null; + } + + /** + * Returns the localized contact information of this REST resource. + * <p> + * Subclasses can override this method to provide their own contact information. + * <p> + * The default implementation returns the contact information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#contact() @RestResource.contact()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].contact</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>contact</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/contact</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public Contact getContact(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (this.contact != null) + return jp.parse(vr.resolve(this.contact), Contact.class); + String contact = context.getMessages().findFirstString(req.getLocale(), "contact"); + if (contact != null) + return jp.parse(vr.resolve(contact), Contact.class); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getContact(); + return null; + } catch (ParseException e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the localized license information of this REST resource. + * <p> + * Subclasses can override this method to provide their own license information. + * <p> + * The default implementation returns the license information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#license() @RestResource.license()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].license</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>license</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/license</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public License getLicense(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (this.license != null) + return jp.parse(vr.resolve(this.license), License.class); + String license = context.getMessages().findFirstString(req.getLocale(), "license"); + if (license != null) + return jp.parse(vr.resolve(license), License.class); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getLicense(); + return null; + } catch (ParseException e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the terms-of-service information of this REST resource. + * <p> + * Subclasses can override this method to provide their own terms-of-service information. + * <p> + * The default implementation returns the terms-of-service information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#termsOfService() @RestResource.termsOfService()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].termsOfService</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>termsOfService</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/termsOfService</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public String getTermsOfService(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (this.termsOfService != null) + return vr.resolve(this.termsOfService); + String termsOfService = context.getMessages().findFirstString(req.getLocale(), "termsOfService"); + if (termsOfService != null) + return vr.resolve(termsOfService); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getTermsOfService(); + return null; + } + + /** + * Returns the version information of this REST resource. + * <p> + * Subclasses can override this method to provide their own version information. + * <p> + * The default implementation returns the version information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#version() @RestResource.version()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/version</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public String getVersion(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (this.version != null) + return vr.resolve(this.version); + String version = context.getMessages().findFirstString(req.getLocale(), "version"); + if (version != null) + return vr.resolve(version); + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getInfo() != null) + return s.getInfo().getVersion(); + return null; + } + + /** + * Returns the version information of this REST resource. + * <p> + * Subclasses can override this method to provide their own version information. + * <p> + * The default implementation returns the version information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#version() @RestResource.version()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/version</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public List<Tag> getTags(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (this.tags != null) + return jp.parse(vr.resolve(this.tags), ArrayList.class, Tag.class); + String tags = context.getMessages().findFirstString(req.getLocale(), "tags"); + if (tags != null) + return jp.parse(vr.resolve(tags), ArrayList.class, Tag.class); + Swagger s = req.getSwaggerFromFile(); + if (s != null) + return s.getTags(); + return null; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the version information of this REST resource. + * <p> + * Subclasses can override this method to provide their own version information. + * <p> + * The default implementation returns the version information from the following locations (whichever matches first): + * <ol> + * <li>{@link RestResource#version() @RestResource.version()} annotation on this class, and then any parent classes. + * <li><ck>[ClassName].version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>version</ck> property in resource bundle identified by {@link RestResource#messages() @RestResource.messages()} + * annotation for this class, then any parent classes. + * <li><ck>/info/version</ck> entry in swagger file. + * </ol> + * + * @param req The current request. + * @return The localized contact information of this REST resource, or <jk>null</jk> if no contact information was found. + */ + public ExternalDocumentation getExternalDocs(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (this.externalDocs != null) + return jp.parse(vr.resolve(this.externalDocs), ExternalDocumentation.class); + String externalDocs = context.getMessages().findFirstString(req.getLocale(), "externalDocs"); + if (externalDocs != null) + return jp.parse(vr.resolve(externalDocs), ExternalDocumentation.class); + Swagger s = req.getSwaggerFromFile(); + if (s != null) + return s.getExternalDocs(); + return null; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } +}
