Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,948 @@ +// *************************************************************************************************************************** +// * 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 static org.apache.juneau.internal.ClassUtils.*; +import static org.apache.juneau.internal.StringUtils.*; +import static org.apache.juneau.internal.Utils.*; +import static org.apache.juneau.rest.RestContext.*; +import static org.apache.juneau.rest.RestUtils.*; +import static org.apache.juneau.rest.annotation.Inherit.*; + +import java.lang.annotation.*; +import java.lang.reflect.*; +import java.util.*; + +import javax.servlet.http.*; + +import org.apache.juneau.*; +import org.apache.juneau.dto.swagger.*; +import org.apache.juneau.encoders.*; +import org.apache.juneau.html.*; +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.widget.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; +import org.apache.juneau.urlencoding.*; + +/** + * Represents a single Java servlet/resource method annotated with {@link RestMethod @RestMethod}. + */ +@SuppressWarnings("hiding") +class CallMethod implements Comparable<CallMethod> { + private final java.lang.reflect.Method method; + private final String httpMethod; + private final UrlPathPattern pathPattern; + private final RestParam[] params; + private final RestGuard[] guards; + private final RestMatcher[] optionalMatchers; + private final RestMatcher[] requiredMatchers; + private final RestConverter[] converters; + private final SerializerGroup serializers; + private final ParserGroup parsers; + private final EncoderGroup encoders; + private final UrlEncodingParser urlEncodingParser; + private final UrlEncodingSerializer urlEncodingSerializer; + private final ObjectMap properties; + private final Map<String,String> defaultRequestHeaders, defaultQuery, defaultFormData; + private final String defaultCharset; + private final boolean deprecated; + private final String description, tags, summary, externalDocs; + private final Integer priority; + private final org.apache.juneau.rest.annotation.Parameter[] parameters; + private final Response[] responses; + private final RestContext context; + private final BeanContext beanContext; + final String htmlHeader, htmlNav, htmlAside, htmlFooter, htmlStyle, htmlStylesheet, htmlScript, htmlNoResultsMessage; + final String[] htmlLinks; + final boolean htmlNoWrap; + final HtmlDocTemplate htmlTemplate; + private final Map<String,Widget> widgets; + + CallMethod(Object servlet, java.lang.reflect.Method method, RestContext context) throws RestServletException { + Builder b = new Builder(servlet, method, context); + this.context = context; + this.method = method; + this.httpMethod = b.httpMethod; + this.pathPattern = b.pathPattern; + this.params = b.params; + this.guards = b.guards; + this.optionalMatchers = b.optionalMatchers; + this.requiredMatchers = b.requiredMatchers; + this.converters = b.converters; + this.serializers = b.serializers; + this.parsers = b.parsers; + this.encoders = b.encoders; + this.urlEncodingParser = b.urlEncodingParser; + this.urlEncodingSerializer = b.urlEncodingSerializer; + this.beanContext = b.beanContext; + this.properties = b.properties; + this.defaultRequestHeaders = b.defaultRequestHeaders; + this.defaultQuery = b.defaultQuery; + this.defaultFormData = b.defaultFormData; + this.defaultCharset = b.defaultCharset; + this.deprecated = b.deprecated; + this.description = b.description; + this.tags = b.tags; + this.summary = b.summary; + this.externalDocs = b.externalDocs; + this.priority = b.priority; + this.parameters = b.parameters; + this.responses = b.responses; + this.htmlHeader = b.htmlHeader; + this.htmlLinks = b.htmlLinks; + this.htmlNav = b.htmlNav; + this.htmlAside = b.htmlAside; + this.htmlFooter = b.htmlFooter; + this.htmlStyle = b.htmlStyle; + this.htmlStylesheet = b.htmlStylesheet; + this.htmlScript = b.htmlScript; + this.htmlNoWrap = b.htmlNoWrap; + this.htmlTemplate = b.htmlTemplate; + this.htmlNoResultsMessage = b.htmlNoResultsMessage; + this.widgets = Collections.unmodifiableMap(b.htmlWidgets); + } + + private static class Builder { + private String httpMethod, defaultCharset, description, tags, summary, externalDocs, htmlNav, htmlAside, + htmlFooter, htmlStyle, htmlStylesheet, htmlScript, htmlHeader, htmlNoResultsMessage; + private String[] htmlLinks; + private boolean htmlNoWrap; + private HtmlDocTemplate htmlTemplate; + private UrlPathPattern pathPattern; + private RestParam[] params; + private RestGuard[] guards; + private RestMatcher[] optionalMatchers, requiredMatchers; + private RestConverter[] converters; + private SerializerGroup serializers; + private ParserGroup parsers; + private EncoderGroup encoders; + private UrlEncodingParser urlEncodingParser; + private UrlEncodingSerializer urlEncodingSerializer; + private BeanContext beanContext; + private ObjectMap properties; + private Map<String,String> defaultRequestHeaders, defaultQuery, defaultFormData; + private boolean plainParams, deprecated; + private Integer priority; + private org.apache.juneau.rest.annotation.Parameter[] parameters; + private Response[] responses; + private Map<String,Widget> htmlWidgets; + + private Builder(Object servlet, java.lang.reflect.Method method, RestContext context) throws RestServletException { + String sig = method.getDeclaringClass().getName() + '.' + method.getName(); + + try { + + RestMethod m = method.getAnnotation(RestMethod.class); + if (m == null) + throw new RestServletException("@RestMethod annotation not found on method ''{0}''", sig); + + if (! m.description().isEmpty()) + description = m.description(); + MethodSwagger sm = m.swagger(); + if (! sm.tags().isEmpty()) + tags = sm.tags(); + if (! m.summary().isEmpty()) + summary = m.summary(); + if (! sm.externalDocs().isEmpty()) + externalDocs = sm.externalDocs(); + deprecated = sm.deprecated(); + parameters = sm.parameters(); + responses = sm.responses(); + serializers = context.getSerializers(); + parsers = context.getParsers(); + urlEncodingSerializer = context.getUrlEncodingSerializer(); + urlEncodingParser = context.getUrlEncodingParser(); + beanContext = context.getBeanContext(); + encoders = context.getEncoders(); + properties = context.getProperties(); + + HtmlDoc hd = m.htmldoc(); + htmlWidgets = new HashMap<String,Widget>(context.getHtmlWidgets()); + for (Class<? extends Widget> wc : hd.widgets()) { + Widget w = ClassUtils.newInstance(Widget.class, wc); + htmlWidgets.put(w.getName(), w); + } + + htmlHeader = resolveNewlineSeparatedAnnotation(hd.header(), context.getHtmlHeader()); + htmlNav = resolveNewlineSeparatedAnnotation(hd.nav(), context.getHtmlNav()); + htmlAside = resolveNewlineSeparatedAnnotation(hd.aside(), context.getHtmlAside()); + htmlFooter = resolveNewlineSeparatedAnnotation(hd.footer(), context.getHtmlFooter()); + htmlStyle = resolveNewlineSeparatedAnnotation(hd.style(), context.getHtmlStyle()); + htmlScript = resolveNewlineSeparatedAnnotation(hd.script(), context.getHtmlScript()); + htmlLinks = resolveLinks(hd.links(), context.getHtmlLinks()); + htmlStylesheet = hd.stylesheet().isEmpty() ? context.getHtmlStylesheet() : hd.stylesheet(); + htmlNoWrap = hd.nowrap() ? hd.nowrap() : context.getHtmlNoWrap(); + htmlNoResultsMessage = hd.noResultsMessage().isEmpty() ? context.getHtmlNoResultsMessage() : hd.noResultsMessage(); + htmlTemplate = + hd.template() == HtmlDocTemplate.class + ? context.getHtmlTemplate() + : ClassUtils.newInstance(HtmlDocTemplate.class, hd.template()); + + List<Inherit> si = Arrays.asList(m.serializersInherit()); + List<Inherit> pi = Arrays.asList(m.parsersInherit()); + + SerializerGroupBuilder sgb = null; + ParserGroupBuilder pgb = null; + UrlEncodingParserBuilder uepb = null; + + if (m.serializers().length > 0 || m.parsers().length > 0 || m.properties().length > 0 || m.flags().length > 0 + || m.beanFilters().length > 0 || m.pojoSwaps().length > 0 || m.bpi().length > 0 + || m.bpx().length > 0) { + sgb = new SerializerGroupBuilder(); + pgb = new ParserGroupBuilder(); + uepb = new UrlEncodingParserBuilder(urlEncodingParser.createPropertyStore()); + + if (si.contains(SERIALIZERS) || m.serializers().length == 0) + sgb.append(serializers.getSerializers()); + + if (pi.contains(PARSERS) || m.parsers().length == 0) + pgb.append(parsers.getParsers()); + } + + httpMethod = m.name().toUpperCase(Locale.ENGLISH); + if (httpMethod.equals("") && method.getName().startsWith("do")) + httpMethod = method.getName().substring(2).toUpperCase(Locale.ENGLISH); + if (httpMethod.equals("")) + httpMethod = "GET"; + if (httpMethod.equals("METHOD")) + httpMethod = "*"; + + priority = m.priority(); + + String p = m.path(); + converters = new RestConverter[m.converters().length]; + for (int i = 0; i < converters.length; i++) + converters[i] = newInstance(RestConverter.class, m.converters()[i]); + + guards = new RestGuard[m.guards().length]; + for (int i = 0; i < guards.length; i++) + guards[i] = newInstance(RestGuard.class, m.guards()[i]); + + 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 (isParentClass(RestMatcherReflecting.class, c)) + matcher = newInstance(RestMatcherReflecting.class, c, servlet, method); + else + matcher = newInstance(RestMatcher.class, c); + if (matcher.mustMatch()) + requiredMatchers.add(matcher); + else + optionalMatchers.add(matcher); + } + if (! m.clientVersion().isEmpty()) + requiredMatchers.add(new ClientVersionMatcher(context.getClientVersionHeader(), method)); + + this.requiredMatchers = requiredMatchers.toArray(new RestMatcher[requiredMatchers.size()]); + this.optionalMatchers = optionalMatchers.toArray(new RestMatcher[optionalMatchers.size()]); + + Class<?>[] beanFilters = context.getBeanFilters(), pojoSwaps = context.getPojoSwaps(); + + if (sgb != null) { + sgb.append(m.serializers()); + if (si.contains(TRANSFORMS)) + sgb.beanFilters(beanFilters).pojoSwaps(pojoSwaps); + if (si.contains(PROPERTIES)) + sgb.properties(properties); + for (Property p1 : m.properties()) + sgb.property(p1.name(), p1.value()); + for (String p1 : m.flags()) + sgb.property(p1, true); + if (m.bpi().length > 0) { + Map<String,String> bpiMap = new LinkedHashMap<String,String>(); + for (String s : m.bpi()) { + for (String s2 : split(s, ';')) { + int i = s2.indexOf(':'); + if (i == -1) + throw new RestServletException( + "Invalid format for @RestMethod.bpi() on method ''{0}''. Must be in the format \"ClassName: comma-delimited-tokens\". \nValue: {1}", sig, s); + bpiMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim()); + } + } + sgb.includeProperties(bpiMap); + } + if (m.bpx().length > 0) { + Map<String,String> bpxMap = new LinkedHashMap<String,String>(); + for (String s : m.bpx()) { + for (String s2 : split(s, ';')) { + int i = s2.indexOf(':'); + if (i == -1) + throw new RestServletException( + "Invalid format for @RestMethod.bpx() on method ''{0}''. Must be in the format \"ClassName: comma-delimited-tokens\". \nValue: {1}", sig, s); + bpxMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim()); + } + } + sgb.excludeProperties(bpxMap); + } + sgb.beanFilters(m.beanFilters()); + sgb.pojoSwaps(m.pojoSwaps()); + } + + if (pgb != null) { + pgb.append(m.parsers()); + if (pi.contains(TRANSFORMS)) + pgb.beanFilters(beanFilters).pojoSwaps(pojoSwaps); + if (pi.contains(PROPERTIES)) + pgb.properties(properties); + for (Property p1 : m.properties()) + pgb.property(p1.name(), p1.value()); + for (String p1 : m.flags()) + pgb.property(p1, true); + pgb.beanFilters(m.beanFilters()); + pgb.pojoSwaps(m.pojoSwaps()); + } + + if (uepb != null) { + for (Property p1 : m.properties()) + uepb.property(p1.name(), p1.value()); + for (String p1 : m.flags()) + uepb.property(p1, true); + uepb.beanFilters(m.beanFilters()); + uepb.pojoSwaps(m.pojoSwaps()); + } + + if (m.properties().length > 0 || m.flags().length > 0) { + properties = new ObjectMap().setInner(properties); + for (Property p1 : m.properties()) + properties.put(p1.name(), p1.value()); + for (String p1 : m.flags()) + properties.put(p1, true); + } + + if (m.encoders().length > 0 || ! m.inheritEncoders()) { + EncoderGroupBuilder g = new EncoderGroupBuilder(); + if (m.inheritEncoders()) + g.append(encoders); + 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 on method ''{0}'': ''{1}''", sig, c.getSimpleName()).initCause(e); + } + } + encoders = g.build(); + } + + defaultRequestHeaders = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER); + for (String s : m.defaultRequestHeaders()) { + String[] h = RestUtils.parseKeyValuePair(s); + if (h == null) + throw new RestServletException( + "Invalid default request header specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); + defaultRequestHeaders.put(h[0], h[1]); + } + + defaultQuery = new LinkedHashMap<String,String>(); + for (String s : m.defaultQuery()) { + String[] h = RestUtils.parseKeyValuePair(s); + if (h == null) + throw new RestServletException( + "Invalid default query parameter specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); + defaultQuery.put(h[0], h[1]); + } + + defaultFormData = new LinkedHashMap<String,String>(); + for (String s : m.defaultFormData()) { + String[] h = RestUtils.parseKeyValuePair(s); + if (h == null) + throw new RestServletException( + "Invalid default form data parameter specified on method ''{0}'': ''{1}''. Must be in the format: ''name[:=]value''", sig, s); + defaultFormData.put(h[0], h[1]); + } + + Type[] pt = method.getGenericParameterTypes(); + Annotation[][] pa = method.getParameterAnnotations(); + for (int i = 0; i < pt.length; i++) { + for (Annotation a : pa[i]) { + if (a instanceof Header) { + Header h = (Header)a; + if (! h.def().isEmpty()) + defaultRequestHeaders.put(firstNonEmpty(h.name(), h.value()), h.def()); + } else if (a instanceof Query) { + Query q = (Query)a; + if (! q.def().isEmpty()) + defaultQuery.put(firstNonEmpty(q.name(), q.value()), q.def()); + } else if (a instanceof FormData) { + FormData f = (FormData)a; + if (! f.def().isEmpty()) + defaultFormData.put(firstNonEmpty(f.name(), f.value()), f.def()); + } + } + } + + defaultCharset = properties.getString(REST_defaultCharset, context.getDefaultCharset()); + String paramFormat = properties.getString(REST_paramFormat, context.getParamFormat()); + plainParams = paramFormat.equals("PLAIN"); + + pathPattern = new UrlPathPattern(p); + + params = context.findParams(method, plainParams, pathPattern, false); + + if (sgb != null) { + serializers = sgb.build(); + beanContext = serializers.getBeanContext(); + } + if (pgb != null) + parsers = pgb.build(); + if (uepb != null) + urlEncodingParser = uepb.build(); + + // Need this to access methods in anonymous inner classes. + method.setAccessible(true); + } catch (RestServletException e) { + throw e; + } catch (Exception e) { + throw new RestServletException("Exception occurred while initializing method ''{0}''", sig).initCause(e); + } + } + } + + /** + * Returns <jk>true</jk> if this Java method has any guards or matchers. + */ + boolean hasGuardsOrMatchers() { + return (guards.length != 0 || requiredMatchers.length != 0 || optionalMatchers.length != 0); + } + + /** + * Returns the HTTP method name (e.g. <js>"GET"</js>). + */ + String getHttpMethod() { + return httpMethod; + } + + /** + * Returns the path pattern for this method. + */ + String getPathPattern() { + return pathPattern.toString(); + } + + /** + * Returns the localized Swagger for this Java method. + */ + Operation getSwaggerOperation(RestRequest req) throws ParseException { + Operation o = operation() + .operationId(method.getName()) + .description(getDescription(req)) + .tags(getTags(req)) + .summary(getSummary(req)) + .externalDocs(getExternalDocs(req)) + .parameters(getParameters(req)) + .responses(getResponses(req)); + + if (isDeprecated()) + o.deprecated(true); + + if (! parsers.getSupportedMediaTypes().equals(context.getParsers().getSupportedMediaTypes())) + o.consumes(parsers.getSupportedMediaTypes()); + + if (! serializers.getSupportedMediaTypes().equals(context.getSerializers().getSupportedMediaTypes())) + o.produces(serializers.getSupportedMediaTypes()); + + return o; + } + + private Operation getSwaggerOperationFromFile(RestRequest req) { + Swagger s = req.getSwaggerFromFile(); + if (s != null && s.getPaths() != null && s.getPaths().get(pathPattern.getPatternString()) != null) + return s.getPaths().get(pathPattern.getPatternString()).get(httpMethod); + return null; + } + + /** + * Returns the localized summary for this Java method. + */ + String getSummary(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (summary != null) + return vr.resolve(summary); + String summary = context.getMessages().findFirstString(req.getLocale(), method.getName() + ".summary"); + if (summary != null) + return vr.resolve(summary); + Operation o = getSwaggerOperationFromFile(req); + if (o != null) + return o.getSummary(); + return null; + } + + /** + * Returns the localized description for this Java method. + */ + String getDescription(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + if (description != null) + return vr.resolve(description); + String description = context.getMessages().findFirstString(req.getLocale(), method.getName() + ".description"); + if (description != null) + return vr.resolve(description); + Operation o = getSwaggerOperationFromFile(req); + if (o != null) + return o.getDescription(); + return null; + } + + /** + * Returns the localized Swagger tags for this Java method. + */ + private List<String> getTags(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (tags != null) + return jp.parse(vr.resolve(tags), ArrayList.class, String.class); + String tags = context.getMessages().findFirstString(req.getLocale(), method.getName() + ".tags"); + if (tags != null) + return jp.parse(vr.resolve(tags), ArrayList.class, String.class); + Operation o = getSwaggerOperationFromFile(req); + if (o != null) + return o.getTags(); + return null; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the localized Swagger external docs for this Java method. + */ + private ExternalDocumentation getExternalDocs(RestRequest req) { + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + try { + if (externalDocs != null) + return jp.parse(vr.resolve(externalDocs), ExternalDocumentation.class); + String externalDocs = context.getMessages().findFirstString(req.getLocale(), method.getName() + ".externalDocs"); + if (externalDocs != null) + return jp.parse(vr.resolve(externalDocs), ExternalDocumentation.class); + Operation o = getSwaggerOperationFromFile(req); + if (o != null) + return o.getExternalDocs(); + return null; + } catch (Exception e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the Swagger deprecated flag for this Java method. + */ + private boolean isDeprecated() { + return deprecated; + } + + /** + * Returns the localized Swagger parameter information for this Java method. + */ + private List<ParameterInfo> getParameters(RestRequest req) throws ParseException { + Operation o = getSwaggerOperationFromFile(req); + if (o != null && o.getParameters() != null) + return o.getParameters(); + + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + Map<String,ParameterInfo> m = new TreeMap<String,ParameterInfo>(); + + // First parse @RestMethod.parameters() annotation. + for (org.apache.juneau.rest.annotation.Parameter v : parameters) { + String in = vr.resolve(v.in()); + ParameterInfo p = parameterInfo(in, vr.resolve(v.name())); + + if (! v.description().isEmpty()) + p.description(vr.resolve(v.description())); + if (v.required()) + p.required(v.required()); + + if ("body".equals(in)) { + if (! v.schema().isEmpty()) + p.schema(jp.parse(vr.resolve(v.schema()), SchemaInfo.class)); + } else { + if (v.allowEmptyValue()) + p.allowEmptyValue(v.allowEmptyValue()); + if (! v.collectionFormat().isEmpty()) + p.collectionFormat(vr.resolve(v.collectionFormat())); + if (! v._default().isEmpty()) + p._default(vr.resolve(v._default())); + if (! v.format().isEmpty()) + p.format(vr.resolve(v.format())); + if (! v.items().isEmpty()) + p.items(jp.parse(vr.resolve(v.items()), Items.class)); + p.type(vr.resolve(v.type())); + } + m.put(p.getIn() + '.' + p.getName(), p); + } + + // Next, look in resource bundle. + String prefix = method.getName() + ".req"; + for (String key : context.getMessages().keySet(prefix)) { + if (key.length() > prefix.length()) { + String value = vr.resolve(context.getMessages().getString(key)); + String[] parts = key.substring(prefix.length() + 1).split("\\."); + String in = parts[0], name, field; + boolean isBody = "body".equals(in); + if (parts.length == (isBody ? 2 : 3)) { + if ("body".equals(in)) { + name = null; + field = parts[1]; + } else { + name = parts[1]; + field = parts[2]; + } + String k2 = in + '.' + name; + ParameterInfo p = m.get(k2); + if (p == null) { + p = parameterInfoStrict(in, name); + m.put(k2, p); + } + + if (field.equals("description")) + p.description(value); + else if (field.equals("required")) + p.required(Boolean.valueOf(value)); + + if ("body".equals(in)) { + if (field.equals("schema")) + p.schema(jp.parse(value, SchemaInfo.class)); + } else { + if (field.equals("allowEmptyValue")) + p.allowEmptyValue(Boolean.valueOf(value)); + else if (field.equals("collectionFormat")) + p.collectionFormat(value); + else if (field.equals("default")) + p._default(value); + else if (field.equals("format")) + p.format(value); + else if (field.equals("items")) + p.items(jp.parse(value, Items.class)); + else if (field.equals("type")) + p.type(value); + } + } else { + System.err.println("Unknown bundle key '"+key+"'"); + } + } + } + + // Finally, look for parameters defined on method. + for (RestParam mp : this.params) { + RestParamType in = mp.getParamType(); + if (in != RestParamType.OTHER) { + String k2 = in.toString() + '.' + (in == RestParamType.BODY ? null : mp.getName()); + ParameterInfo p = m.get(k2); + if (p == null) { + p = parameterInfoStrict(in.toString(), mp.getName()); + m.put(k2, p); + } + } + } + + if (m.isEmpty()) + return null; + return new ArrayList<ParameterInfo>(m.values()); + } + + /** + * Returns the localized Swagger response information about this Java method. + */ + @SuppressWarnings("unchecked") + private Map<Integer,ResponseInfo> getResponses(RestRequest req) throws ParseException { + Operation o = getSwaggerOperationFromFile(req); + if (o != null && o.getResponses() != null) + return o.getResponses(); + + VarResolverSession vr = req.getVarResolverSession(); + JsonParser jp = JsonParser.DEFAULT; + Map<Integer,ResponseInfo> m = new TreeMap<Integer,ResponseInfo>(); + Map<String,HeaderInfo> m2 = new TreeMap<String,HeaderInfo>(); + + // First parse @RestMethod.parameters() annotation. + for (Response r : responses) { + int httpCode = r.value(); + String description = r.description().isEmpty() ? RestUtils.getHttpResponseText(r.value()) : vr.resolve(r.description()); + ResponseInfo r2 = responseInfo(description); + + if (r.headers().length > 0) { + for (org.apache.juneau.rest.annotation.Parameter v : r.headers()) { + HeaderInfo h = headerInfoStrict(vr.resolve(v.type())); + if (! v.collectionFormat().isEmpty()) + h.collectionFormat(vr.resolve(v.collectionFormat())); + if (! v._default().isEmpty()) + h._default(vr.resolve(v._default())); + if (! v.description().isEmpty()) + h.description(vr.resolve(v.description())); + if (! v.format().isEmpty()) + h.format(vr.resolve(v.format())); + if (! v.items().isEmpty()) + h.items(jp.parse(vr.resolve(v.items()), Items.class)); + r2.header(v.name(), h); + m2.put(httpCode + '.' + v.name(), h); + } + } + m.put(httpCode, r2); + } + + // Next, look in resource bundle. + String prefix = method.getName() + ".res"; + for (String key : context.getMessages().keySet(prefix)) { + if (key.length() > prefix.length()) { + String value = vr.resolve(context.getMessages().getString(key)); + String[] parts = key.substring(prefix.length() + 1).split("\\."); + int httpCode = Integer.parseInt(parts[0]); + ResponseInfo r2 = m.get(httpCode); + if (r2 == null) { + r2 = responseInfo(null); + m.put(httpCode, r2); + } + + String name = parts.length > 1 ? parts[1] : ""; + + if ("header".equals(name) && parts.length > 3) { + String headerName = parts[2]; + String field = parts[3]; + + String k2 = httpCode + '.' + headerName; + HeaderInfo h = m2.get(k2); + if (h == null) { + h = headerInfoStrict("string"); + m2.put(k2, h); + r2.header(name, h); + } + if (field.equals("collectionFormat")) + h.collectionFormat(value); + else if (field.equals("default")) + h._default(value); + else if (field.equals("description")) + h.description(value); + else if (field.equals("format")) + h.format(value); + else if (field.equals("items")) + h.items(jp.parse(value, Items.class)); + else if (field.equals("type")) + h.type(value); + + } else if ("description".equals(name)) { + r2.description(value); + } else if ("schema".equals(name)) { + r2.schema(jp.parse(value, SchemaInfo.class)); + } else if ("examples".equals(name)) { + r2.examples(jp.parse(value, TreeMap.class)); + } else { + System.err.println("Unknown bundle key '"+key+"'"); + } + } + } + + return m.isEmpty() ? null : m; + } + + /** + * Returns <jk>true</jk> if the specified request object can call this method. + */ + boolean isRequestAllowed(RestRequest req) { + for (RestGuard guard : guards) { + req.setJavaMethod(method); + if (! guard.isRequestAllowed(req)) + return false; + } + return true; + } + + /** + * Workhorse method. + * + * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta) + * @return The HTTP response code. + */ + int invoke(String pathInfo, 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.getVars().length) + remainder = patternVals[pathPattern.getVars().length]; + for (int i = 0; i < pathPattern.getVars().length; i++) + req.getPathMatch().put(pathPattern.getVars()[i], patternVals[i]); + req.getPathMatch().setRemainder(remainder); + + ObjectMap requestProperties = createRequestProperties(properties, req); + req.init(method, requestProperties, defaultRequestHeaders, defaultQuery, defaultFormData, defaultCharset, + serializers, parsers, urlEncodingParser, beanContext, encoders, widgets); + res.init(requestProperties, defaultCharset, serializers, urlEncodingSerializer, encoders); + + // Class-level guards + for (RestGuard guard : context.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; + } + + context.preCall(req, res); + + Object[] args = new Object[params.length]; + for (int i = 0; i < params.length; i++) { + try { + args[i] = params[i].resolve(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].getParamType().name(), params[i].getName(), params[i].getType(), method.getDeclaringClass().getName(), method.getName() + ).initCause(e); + } + } + + try { + + for (RestGuard guard : guards) + if (! guard.guard(req, res)) + return SC_OK; + + Object output = method.invoke(context.getResource(), args); + if (! method.getReturnType().equals(Void.TYPE)) + if (output != null || ! res.getOutputStreamCalled()) + res.setOutput(output); + + context.postCall(req, res); + + if (res.hasOutput()) { + output = res.getOutput(); + for (RestConverter converter : converters) + output = converter.convert(req, output, beanContext.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(), getReadableClassNames(args) + ).initCause(e); + } 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; + } + + /** + * This method creates all the request-time properties. + */ + ObjectMap createRequestProperties(final ObjectMap methodProperties, final RestRequest req) { + @SuppressWarnings("serial") + ObjectMap m = new ObjectMap() { + @Override /* Map */ + public Object get(Object key) { + Object o = super.get(key); + if (o == null) { + String k = key.toString(); + int i = k.indexOf('.'); + if (i != -1) { + String prefix = k.substring(0, i); + String remainder = k.substring(i+1); + Object v = req.resolveProperty(CallMethod.this, prefix, remainder); + if (v != null) + return v; + } + o = req.getPathMatch().get(k); + if (o == null) + o = req.getHeader(k); + } + if (o instanceof String) + o = req.getVarResolverSession().resolve(o.toString()); + return o; + } + }; + m.setInner(methodProperties); + return m; + } + + @Override /* Object */ + public String toString() { + return "SimpleMethod: name=" + httpMethod + ", path=" + pathPattern.getPatternString(); + } + + /* + * compareTo() method is used to keep SimpleMethods ordered in the CallRouter list. + * It maintains the order in which matches are made during requests. + */ + @Override /* Comparable */ + public int compareTo(CallMethod o) { + int c; + + c = priority.compareTo(o.priority); + if (c != 0) + return c; + + c = pathPattern.compareTo(o.pathPattern); + if (c != 0) + return c; + + c = compare(o.requiredMatchers.length, requiredMatchers.length); + if (c != 0) + return c; + + c = compare(o.optionalMatchers.length, optionalMatchers.length); + if (c != 0) + return c; + + c = compare(o.guards.length, guards.length); + if (c != 0) + return c; + + return 0; + } + + @Override /* Object */ + public boolean equals(Object o) { + if (! (o instanceof CallMethod)) + return false; + return (compareTo((CallMethod)o) == 0); + } + + @Override /* Object */ + public int hashCode() { + return super.hashCode(); + } +} \ No newline at end of file
Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,100 @@ +// *************************************************************************************************************************** +// * 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.util.*; + +import javax.servlet.http.*; + +/** + * Represents a group of CallMethods on a REST resource that handle the same HTTP Method name but with different + * paths/matchers/guards/etc... + * + * <p> + * Incoming requests for a particular HTTP method type (e.g. <js>"GET"</js>) are handed off to this class and then + * dispatched to the appropriate CallMethod. + */ +class CallRouter { + private final CallMethod[] callMethods; + + private CallRouter(CallMethod[] callMethods) { + this.callMethods = callMethods; + } + + /** + * Builder class. + */ + static class Builder { + private List<CallMethod> childMethods = new ArrayList<CallMethod>(); + private Set<String> collisions = new HashSet<String>(); + private String httpMethodName; + + Builder(String httpMethodName) { + this.httpMethodName = httpMethodName; + } + + String getHttpMethodName() { + return httpMethodName; + } + + Builder add(CallMethod m) throws RestServletException { + if (! m.hasGuardsOrMatchers()) { + String p = m.getHttpMethod() + ":" + m.getPathPattern(); + if (collisions.contains(p)) + throw new RestServletException("Duplicate Java methods assigned to the same method/pattern: ''{0}''", p); + collisions.add(p); + } + childMethods.add(m); + return this; + } + + CallRouter build() { + Collections.sort(childMethods); + return new CallRouter(childMethods.toArray(new CallMethod[childMethods.size()])); + } + } + + /** + * Workhorse method. + * + * <p> + * Routes this request to one of the CallMethods. + * + * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta) + * @return The HTTP response code. + */ + int invoke(String pathInfo, RestRequest req, RestResponse res) throws RestException { + if (callMethods.length == 1) + return callMethods[0].invoke(pathInfo, req, res); + + int maxRc = 0; + for (CallMethod m : callMethods) { + int rc = m.invoke(pathInfo, req, res); + if (rc == SC_OK) + return SC_OK; + maxRc = Math.max(maxRc, rc); + } + return maxRc; + } + + @Override /* Object */ + public String toString() { + StringBuilder sb = new StringBuilder("CallRouter: [\n"); + for (CallMethod sm : callMethods) + sb.append("\t" + sm + "\n"); + sb.append("]"); + return sb.toString(); + } +} \ No newline at end of file Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,54 @@ +// *************************************************************************************************************************** +// * 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 org.apache.juneau.internal.StringUtils.*; + +import org.apache.juneau.internal.*; +import org.apache.juneau.rest.annotation.*; + +/** + * Specialized matcher for matching client versions. + * + * <p> + * See {@link RestResource#clientVersionHeader} and {@link RestMethod#clientVersion} for more info. + */ +public class ClientVersionMatcher extends RestMatcher { + + private final String clientVersionHeader; + private final VersionRange range; + + /** + * Constructor. + * + * @param clientVersionHeader + * The HTTP request header name containing the client version. + * If <jk>null</jk> or an empty string, uses <js>"X-Client-Version"</js> + * @param javaMethod The version string that the client version must match. + */ + protected ClientVersionMatcher(String clientVersionHeader, java.lang.reflect.Method javaMethod) { + this.clientVersionHeader = isEmpty(clientVersionHeader) ? "X-Client-Version" : clientVersionHeader; + RestMethod m = javaMethod.getAnnotation(RestMethod.class); + range = new VersionRange(m.clientVersion()); + } + + @Override /* RestMatcher */ + public boolean matches(RestRequest req) { + return range.matches(req.getHeader(clientVersionHeader)); + } + + @Override /* RestMatcher */ + public boolean mustMatch() { + return true; + } +} Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,265 @@ +// *************************************************************************************************************************** +// * 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 org.apache.juneau.internal.IOUtils.*; + +import java.io.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.http.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.rest.annotation.*; +import org.apache.juneau.rest.response.*; +import org.apache.juneau.svl.*; + +/** + * Represents the contents of a text file with convenience methods for resolving {@link Parameter} variables and adding + * HTTP response headers. + * + * <p> + * This class is handled special by the {@link WritableHandler} class. + */ +public class ReaderResource implements Writable { + + private final MediaType mediaType; + private final String[] contents; + private final VarResolverSession varSession; + private final Map<String,String> headers; + + /** + * Constructor. + * + * @param mediaType The HTTP media type. + * @param contents + * The contents of this resource. + * <br>If multiple contents are specified, the results will be concatenated. + * <br>Contents can be any of the following: + * <ul> + * <li><code>CharSequence</code> + * <li><code>Reader</code> + * <li><code>File</code> + * </ul> + * @throws IOException + */ + protected ReaderResource(MediaType mediaType, Object...contents) throws IOException { + this(mediaType, null, null, contents); + } + + /** + * Constructor. + * + * @param mediaType The resource media type. + * @param headers The HTTP response headers for this streamed resource. + * @param varSession Optional variable resolver for resolving variables in the string. + * @param contents + * The resource contents. + * <br>If multiple contents are specified, the results will be concatenated. + * <br>Contents can be any of the following: + * <ul> + * <li><code>InputStream</code> + * <li><code>Reader</code> - Converted to UTF-8 bytes. + * <li><code>File</code> + * <li><code>CharSequence</code> - Converted to UTF-8 bytes. + * </ul> + * @throws IOException + */ + public ReaderResource(MediaType mediaType, Map<String,String> headers, VarResolverSession varSession, Object...contents) throws IOException { + this.mediaType = mediaType; + this.varSession = varSession; + + Map<String,String> m = new LinkedHashMap<String,String>(); + if (headers != null) + for (Map.Entry<String,String> e : headers.entrySet()) + m.put(e.getKey(), StringUtils.toString(e.getValue())); + this.headers = Collections.unmodifiableMap(m); + + this.contents = new String[contents.length]; + for (int i = 0; i < contents.length; i++) { + Object c = contents[i]; + if (c == null) + this.contents[i] = ""; + else if (c instanceof InputStream) + this.contents[i] = read((InputStream)c); + else if (c instanceof File) + this.contents[i] = read((File)c); + else if (c instanceof Reader) + this.contents[i] = read((Reader)c); + else if (c instanceof CharSequence) + this.contents[i] = ((CharSequence)c).toString(); + else + throw new IOException("Invalid class type passed to ReaderResource: " + c.getClass().getName()); + } + } + + /** + * Builder class for constructing {@link ReaderResource} objects. + */ + @SuppressWarnings("hiding") + public static class Builder { + ArrayList<Object> contents = new ArrayList<Object>(); + MediaType mediaType; + VarResolverSession varResolver; + Map<String,String> headers = new LinkedHashMap<String,String>(); + + /** + * Specifies the resource media type string. + * + * @param mediaType The resource media type string. + * @return This object (for method chaining). + */ + public Builder mediaType(String mediaType) { + this.mediaType = MediaType.forString(mediaType); + return this; + } + + /** + * Specifies the resource media type string. + * + * @param mediaType The resource media type string. + * @return This object (for method chaining). + */ + public Builder mediaType(MediaType mediaType) { + this.mediaType = mediaType; + return this; + } + + /** + * Specifies the contents for this resource. + * + * <p> + * This method can be called multiple times to add more content. + * + * @param contents + * The resource contents. + * <br>If multiple contents are specified, the results will be concatenated. + * <br>Contents can be any of the following: + * <ul> + * <li><code>InputStream</code> + * <li><code>Reader</code> - Converted to UTF-8 bytes. + * <li><code>File</code> + * <li><code>CharSequence</code> - Converted to UTF-8 bytes. + * </ul> + * @return This object (for method chaining). + */ + public Builder contents(Object...contents) { + this.contents.addAll(Arrays.asList(contents)); + return this; + } + + /** + * Specifies an HTTP response header value. + * + * @param name The HTTP header name. + * @param value + * The HTTP header value. + * Will be converted to a <code>String</code> using {@link Object#toString()}. + * @return This object (for method chaining). + */ + public Builder header(String name, Object value) { + this.headers.put(name, StringUtils.toString(value)); + return this; + } + + /** + * Specifies HTTP response header values. + * + * @param headers + * The HTTP headers. + * Values will be converted to <code>Strings</code> using {@link Object#toString()}. + * @return This object (for method chaining). + */ + public Builder headers(Map<String,Object> headers) { + for (Map.Entry<String,Object> e : headers.entrySet()) + header(e.getKey(), e.getValue()); + return this; + } + + /** + * Specifies the variable resolver to use for this resource. + * + * @param varResolver The variable resolver. + * @return This object (for method chaining). + */ + public Builder varResolver(VarResolverSession varResolver) { + this.varResolver = varResolver; + return this; + } + + /** + * Create a new {@link ReaderResource} using values in this builder. + * + * @return A new immutable {@link ReaderResource} object. + * @throws IOException + */ + public ReaderResource build() throws IOException { + return new ReaderResource(mediaType, headers, varResolver, contents.toArray()); + } + } + + /** + * Get the HTTP response headers. + * + * @return The HTTP response headers. + */ + public Map<String,String> getHeaders() { + return headers; + } + + @Override /* Writeable */ + public void writeTo(Writer w) throws IOException { + for (String s : contents) { + if (varSession != null) + varSession.resolveTo(s, w); + else + w.write(s); + } + } + + @Override /* Writeable */ + public MediaType getMediaType() { + return mediaType; + } + + @Override /* Object */ + public String toString() { + if (contents.length == 1 && varSession == null) + return contents[0]; + StringWriter sw = new StringWriter(); + for (String s : contents) { + if (varSession != null) + return varSession.resolve(s); + sw.write(s); + } + return sw.toString(); + } + + /** + * Same as {@link #toString()} but strips comments from the text before returning it. + * + * <p> + * Supports stripping comments from the following media types: HTML, XHTML, XML, JSON, Javascript, CSS. + * + * @return The resource contents stripped of any comments. + */ + public String toCommentStrippedString() { + String s = toString(); + String subType = mediaType.getSubType(); + if ("html".equals(subType) || "xhtml".equals(subType) || "xml".equals(subType)) + s = s.replaceAll("(?s)<!--(.*?)-->\\s*", ""); + else if ("json".equals(subType) || "javascript".equals(subType) || "css".equals(subType)) + s = s.replaceAll("(?s)\\/\\*(.*?)\\*\\/\\s*", ""); + return s; + } +} Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,165 @@ +// *************************************************************************************************************************** +// * 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 org.apache.juneau.internal.StringUtils.*; + +import java.net.*; +import java.text.*; + +import org.apache.juneau.urlencoding.*; + +/** + * REST methods can return this object as a shortcut for performing <code>HTTP 302</code> redirects. + * + * <p> + * The following example shows the difference between handling redirects via the {@link RestRequest}/{@link RestResponse}, + * and the simplified approach of using this class. + * <p class='bcode'> + * <jc>// Redirect to "/contextPath/servletPath/foobar"</jc> + * + * <jc>// Using RestRequest and RestResponse</jc> + * <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example1"</js>) + * <jk>public void</jk> example1(RestRequest req, RestResponse res) <jk>throws</jk> IOException { + * res.sendRedirect(req.getServletURI() + <js>"/foobar"</js>); + * } + * + * <jc>// Using Redirect</jc> + * <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example2"</js>) + * <jk>public</jk> Redirect example2() { + * <jk>return new</jk> Redirect(<js>"foobar"</js>); + * } + * </p> + * + * <p> + * The constructor can use a {@link MessageFormat}-style pattern with multiple arguments: + * <p class='bcode'> + * <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example3"</js>) + * <jk>public</jk> Redirect example3() { + * <jk>return new</jk> Redirect(<js>"foo/{0}/bar/{1}"</js>, id1, id2); + * } + * </p> + * + * <p> + * The arguments are serialized to strings using the servlet's {@link UrlEncodingSerializer}, so any filters defined on + * the serializer or REST method/class will be used when present. + * The arguments will also be automatically URL-encoded. + * + * <p> + * Redirecting to the servlet root can be accomplished by simply using the no-arg constructor. + * <p class='bcode'> + * <jc>// Simply redirect to the servlet root. + * // Equivalent to res.sendRedirect(req.getServletURI()).</jc> + * <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example4"</js>) + * <jk>public</jk> Redirect exmaple4() { + * <jk>return new</jk> Redirect(); + * } + * </p> + * + * <p> + * This class is handled by {@link org.apache.juneau.rest.response.RedirectHandler}, a built-in default response + * handler created in {@link RestConfig}. + */ +public final class Redirect { + + private final int httpResponseCode; + private final URI uri; + + /** + * Redirect to the specified URL. + * + * <p> + * Relative paths are interpreted as relative to the servlet path. + * + * @param uri + * The URL to redirect to. + * <br>Can be any of the following: + * <ul> + * <li><code>URL</code> + * <li><code>URI</code> + * <li><code>CharSequence</code> + * </ul> + * @param args Optional {@link MessageFormat}-style arguments. + */ + public Redirect(Object uri, Object...args) { + this(0, uri, args); + } + + /** + * Convenience method for redirecting to instance of {@link URL} and {@link URI}. + * + * <p> + * Same as calling <code>toString()</code> on the object and using the other constructor. + * + * @param uri + * The URL to redirect to. + * <br>Can be any of the following: + * <ul> + * <li><code>URL</code> + * <li><code>URI</code> + * <li><code>CharSequence</code> + * </ul> + */ + public Redirect(Object uri) { + this(0, uri, (Object[])null); + } + + /** + * Redirect to the specified URL. + * + * <p> + * Relative paths are interpreted as relative to the servlet path. + * + * @param httpResponseCode The HTTP response code. + * @param url + * The URL to redirect to. + * <br>Can be any of the following: + * <ul> + * <li><code>URL</code> + * <li><code>URI</code> + * <li><code>CharSequence</code> + * </ul> + * @param args Optional {@link MessageFormat}-style arguments. + */ + public Redirect(int httpResponseCode, Object url, Object...args) { + this.httpResponseCode = httpResponseCode; + if (url == null) + url = ""; + this.uri = toURI(format(url.toString(), args)); + } + + /** + * Shortcut for redirecting to the servlet root. + */ + public Redirect() { + this(0, null, (Object[])null); + } + + /** + * Returns the response code passed in through the constructor. + * + * @return The response code passed in through the constructor, or <code>0</code> if response code wasn't specified. + */ + public int getHttpResponseCode() { + return httpResponseCode; + } + + /** + * Returns the URI to redirect to. + * + * @return The URI to redirect to. + */ + public URI getURI() { + return uri; + } +} Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java ============================================================================== --- release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java (added) +++ release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java Fri Sep 8 23:25:34 2017 @@ -0,0 +1,457 @@ +// *************************************************************************************************************************** +// * 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.internal.IOUtils.*; +import static org.apache.juneau.internal.StringUtils.*; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import javax.servlet.*; + +import org.apache.juneau.*; +import org.apache.juneau.encoders.*; +import org.apache.juneau.http.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.urlencoding.*; + +/** + * Contains the body of the HTTP request. + */ +@SuppressWarnings("unchecked") +public class RequestBody { + + private byte[] body; + private final RestRequest req; + private EncoderGroup encoders; + private Encoder encoder; + private ParserGroup parsers; + private UrlEncodingParser urlEncodingParser; + private RequestHeaders headers; + private BeanSession beanSession; + private int contentLength = 0; + + RequestBody(RestRequest req) { + this.req = req; + } + + RequestBody setEncoders(EncoderGroup encoders) { + this.encoders = encoders; + return this; + } + + RequestBody setParsers(ParserGroup parsers) { + this.parsers = parsers; + return this; + } + + RequestBody setHeaders(RequestHeaders headers) { + this.headers = headers; + return this; + } + + RequestBody setUrlEncodingParser(UrlEncodingParser urlEncodingParser) { + this.urlEncodingParser = urlEncodingParser; + return this; + } + + RequestBody setBeanSession(BeanSession beanSession) { + this.beanSession = beanSession; + return this; + } + + @SuppressWarnings("hiding") + RequestBody load(byte[] body) { + this.body = body; + return this; + } + + boolean isLoaded() { + return body != null; + } + + /** + * Reads the input from the HTTP request as JSON, XML, or HTML and converts the input to a POJO. + * + * <p> + * If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &body=xxx} in the URL + * query string. + * + * <p> + * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined + * automatically based on the following input: + * <table class='styled'> + * <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr> + * <tr> + * <td>object</td> + * <td><js>"{...}"</js></td> + * <td><code><xt><object></xt>...<xt></object></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link ObjectMap}</td> + * </tr> + * <tr> + * <td>array</td> + * <td><js>"[...]"</js></td> + * <td><code><xt><array></xt>...<xt></array></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link ObjectList}</td> + * </tr> + * <tr> + * <td>string</td> + * <td><js>"'...'"</js></td> + * <td><code><xt><string></xt>...<xt></string></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link String}</td> + * </tr> + * <tr> + * <td>number</td> + * <td><code>123</code></td> + * <td><code><xt><number></xt>123<xt></number></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link Number}</td> + * </tr> + * <tr> + * <td>boolean</td> + * <td><jk>true</jk></td> + * <td><code><xt><boolean></xt>true<xt></boolean></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link Boolean}</td> + * </tr> + * <tr> + * <td>null</td> + * <td><jk>null</jk> or blank</td> + * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></xt></code></td> + * <td><jk>null</jk></td> + * </tr> + * </table> + * + * <p> + * Refer to <a class="doclink" href="../../../../overview-summary.html#Core.PojoCategories">POJO Categories</a> for + * a complete definition of supported POJOs. + * + * <h5 class='section'>Examples:</h5> + * <p class='bcode'> + * <jc>// Parse into an integer.</jc> + * <jk>int</jk> body = req.getBody().asType(<jk>int</jk>.<jk>class</jk>); + * + * <jc>// Parse into an int array.</jc> + * <jk>int</jk>[] body = req.getBody().asType(<jk>int</jk>[].<jk>class</jk>); + + * <jc>// Parse into a bean.</jc> + * MyBean body = req.getBody().asType(MyBean.<jk>class</jk>); + * + * <jc>// Parse into a linked-list of objects.</jc> + * List body = req.getBody().asType(LinkedList.<jk>class</jk>); + * + * <jc>// Parse into a map of object keys/values.</jc> + * Map body = req.getBody().asType(TreeMap.<jk>class</jk>); + * </p> + * + * @param type The class type to instantiate. + * @param <T> The class type to instantiate. + * @return The input parsed to a POJO. + * @throws IOException If a problem occurred trying to read from the reader. + * @throws ParseException + * If the input contains a syntax error or is malformed for the requested {@code Accept} header or is not valid + * for the specified type. + */ + public <T> T asType(Class<T> type) throws IOException, ParseException { + return parse(beanSession.getClassMeta(type)); + } + + /** + * Reads the input from the HTTP request as JSON, XML, or HTML and converts the input to a POJO. + * + * <h5 class='section'>Examples:</h5> + * <p class='bcode'> + * <jc>// Parse into a linked-list of strings.</jc> + * List<String> body = req.getBody().asType(LinkedList.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a linked-list of linked-lists of strings.</jc> + * List<List<String>> body = req.getBody().asType(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a map of string keys/values.</jc> + * Map<String,String> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> + * Map<String,List<MyBean>> body = req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); + * </p> + * + * @param type + * The type of object to create. + * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, + * {@link GenericArrayType} + * @param args + * The type arguments of the class if it's a collection or map. + * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, + * {@link GenericArrayType} + * <br>Ignored if the main type is not a map or collection. + * @param <T> The class type to instantiate. + * @return The input parsed to a POJO. + */ + public <T> T asType(Type type, Type...args) { + return (T)parse(beanSession.getClassMeta(type, args)); + } + + /** + * Returns the HTTP body content as a plain string. + * + * <p> + * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query + * string. + * + * @return The incoming input from the connection as a plain string. + * @throws IOException If a problem occurred trying to read from the reader. + */ + public String asString() throws IOException { + if (body == null) + body = readBytes(getInputStream(), 1024); + return new String(body, UTF8); + } + + /** + * Returns the HTTP body content as a simple hexadecimal character string. + * + * @return The incoming input from the connection as a plain string. + * @throws IOException If a problem occurred trying to read from the reader. + */ + public String asHex() throws IOException { + if (body == null) + body = readBytes(getInputStream(), 1024); + return toHex(body); + } + + /** + * Returns the HTTP body content as a {@link Reader}. + * + * <p> + * If {@code allowHeaderParams} init parameter is true, then first looks for {@code &body=xxx} in the URL query + * string. + * + * <p> + * Automatically handles GZipped input streams. + * + * @return The body contents as a reader. + * @throws IOException + */ + public BufferedReader getReader() throws IOException { + Reader r = getUnbufferedReader(); + if (r instanceof BufferedReader) + return (BufferedReader)r; + int len = req.getContentLength(); + int buffSize = len <= 0 ? 8192 : Math.max(len, 8192); + return new BufferedReader(r, buffSize); + } + + /** + * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader}; + * + * @return An unbuffered reader. + * @throws IOException + */ + protected Reader getUnbufferedReader() throws IOException { + if (body != null) + return new CharSequenceReader(new String(body, UTF8)); + return new InputStreamReader(getInputStream(), req.getCharacterEncoding()); + } + + /** + * Returns the HTTP body content as an {@link InputStream}. + * + * <p> + * Automatically handles GZipped input streams. + * + * @return The negotiated input stream. + * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper. + */ + public ServletInputStream getInputStream() throws IOException { + + if (body != null) + return new ServletInputStream2(body); + + Encoder enc = getEncoder(); + + ServletInputStream is = req.getRawInputStream(); + if (enc != null) { + final InputStream is2 = enc.getInputStream(is); + return new ServletInputStream2(is2); + } + return is; + } + + /** + * Returns the parser and media type matching the request <code>Content-Type</code> header. + * + * @return + * The parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching parser was + * found. + * Includes the matching media type. + */ + public ParserMatch getParserMatch() { + MediaType mediaType = headers.getContentType(); + if (isEmpty(mediaType)) { + if (body != null) + mediaType = MediaType.UON; + else + mediaType = MediaType.JSON; + } + ParserMatch pm = parsers.getParserMatch(mediaType); + + // If no patching parser for URL-encoding, use the one defined on the servlet. + if (pm == null && mediaType.equals(MediaType.URLENCODING)) + pm = new ParserMatch(MediaType.URLENCODING, urlEncodingParser); + + return pm; + } + + /** + * Returns the parser matching the request <code>Content-Type</code> header. + * + * @return + * The parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching parser was + * found. + */ + public Parser getParser() { + ParserMatch pm = getParserMatch(); + return (pm == null ? null : pm.getParser()); + } + + /** + * Returns the reader parser matching the request <code>Content-Type</code> header. + * + * @return + * The reader parser matching the request <code>Content-Type</code> header, or <jk>null</jk> if no matching + * reader parser was found, or the matching parser was an input stream parser. + */ + public ReaderParser getReaderParser() { + Parser p = getParser(); + if (p != null && p.isReaderParser()) + return (ReaderParser)p; + return null; + } + + /* Workhorse method */ + private <T> T parse(ClassMeta<T> cm) throws RestException { + + try { + if (cm.isReader()) + return (T)getReader(); + + if (cm.isInputStream()) + return (T)getInputStream(); + + TimeZone timeZone = headers.getTimeZone(); + Locale locale = req.getLocale(); + ParserMatch pm = getParserMatch(); + + if (pm != null) { + Parser p = pm.getParser(); + MediaType mediaType = pm.getMediaType(); + try { + req.getProperties().append("mediaType", mediaType).append("characterEncoding", req.getCharacterEncoding()); + ParserSession session = p.createSession(new ParserSessionArgs(req.getProperties(), req.getJavaMethod(), locale, timeZone, mediaType, req.getContext().getResource())); + Object in = session.isReaderParser() ? getUnbufferedReader() : getInputStream(); + return session.parse(in, cm); + } catch (ParseException e) { + throw new RestException(SC_BAD_REQUEST, + "Could not convert request body content to class type ''{0}'' using parser ''{1}''.", + cm, p.getClass().getName() + ).initCause(e); + } + } + + throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE, + "Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}", + headers.getContentType(), req.getParserGroup().getSupportedMediaTypes() + ); + + } catch (IOException e) { + throw new RestException(SC_INTERNAL_SERVER_ERROR, + "I/O exception occurred while attempting to handle request ''{0}''.", + req.getDescription() + ).initCause(e); + } + } + + private Encoder getEncoder() { + if (encoder == null) { + String ce = req.getHeader("content-encoding"); + if (! isEmpty(ce)) { + ce = ce.trim(); + encoder = encoders.getEncoder(ce); + if (encoder == null) + throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE, + "Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}", + req.getHeader("content-encoding"), encoders.getSupportedEncodings() + ); + } + + if (encoder != null) + contentLength = -1; + } + // Note that if this is the identity encoder, we want to return null + // so that we don't needlessly wrap the input stream. + if (encoder == IdentityEncoder.INSTANCE) + return null; + return encoder; + } + + /** + * Returns the content length of the body. + * + * @return The content length of the body in bytes. + */ + public int getContentLength() { + return contentLength == 0 ? req.getRawContentLength() : contentLength; + } + + /** + * ServletInputStream wrapper around a normal input stream. + */ + private static class ServletInputStream2 extends ServletInputStream { + + private final InputStream is; + + private ServletInputStream2(InputStream is) { + this.is = is; + } + + private ServletInputStream2(byte[] b) { + this(new ByteArrayInputStream(b)); + } + + @Override /* InputStream */ + public final int read() throws IOException { + return is.read(); + } + + @Override /* InputStream */ + public final void close() throws IOException { + is.close(); + } + + @Override /* ServletInputStream */ + public boolean isFinished() { + return false; + } + + @Override /* ServletInputStream */ + public boolean isReady() { + return true; + } + + @Override /* ServletInputStream */ + public void setReadListener(ReadListener arg0) { + throw new NoSuchMethodError(); + } + } +} Propchange: release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java ------------------------------------------------------------------------------ svn:mime-type = text/plain