| // *************************************************************************************************************************** |
| // * 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.CollectionUtils.*; |
| |
| import java.io.*; |
| import java.util.*; |
| |
| import javax.servlet.http.*; |
| |
| import org.apache.http.*; |
| import org.apache.juneau.*; |
| import org.apache.juneau.cp.*; |
| import org.apache.juneau.http.response.*; |
| import org.apache.juneau.rest.annotation.*; |
| import org.apache.juneau.rest.logger.*; |
| import org.apache.juneau.rest.util.*; |
| |
| /** |
| * Represents a single HTTP request. |
| * |
| * <ul class='notes'> |
| * <li class='warn'>This class is not thread safe. |
| * </ul> |
| * |
| * <ul class='seealso'> |
| * <li class='extlink'>{@source} |
| * </ul> |
| */ |
| public class RestSession extends ContextSession { |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Static |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Request attribute name for passing path variables from parent to child. |
| */ |
| private static final String REST_PATHVARS_ATTR = "juneau.pathVars"; |
| |
| /** |
| * Creates a builder of this object. |
| * |
| * @param ctx The context creating this builder. |
| * @return A new builder. |
| */ |
| public static Builder create(RestContext ctx) { |
| return new Builder(ctx); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Builder |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Builder class. |
| */ |
| public static class Builder extends ContextSession.Builder { |
| |
| RestContext ctx; |
| Object resource; |
| HttpServletRequest req; |
| HttpServletResponse res; |
| RestLogger logger; |
| String pathInfoUndecoded; |
| UrlPath urlPath; |
| |
| /** |
| * Constructor. |
| * |
| * @param ctx The context creating this session. |
| */ |
| protected Builder(RestContext ctx) { |
| super(ctx); |
| this.ctx = ctx; |
| } |
| |
| /** |
| * Specifies the servlet implementation bean. |
| * |
| * @param value The value for this setting. |
| * @return This object. |
| */ |
| public Builder resource(Object value) { |
| resource = value; |
| return this; |
| } |
| |
| /** |
| * Specifies the HTTP servlet request object on this call. |
| * |
| * @param value The value for this setting. |
| * @return This object. |
| */ |
| public Builder req(HttpServletRequest value) { |
| req = value; |
| return this; |
| } |
| |
| /** |
| * Returns the HTTP servlet request object on this call. |
| * |
| * @return The HTTP servlet request object on this call. |
| */ |
| public HttpServletRequest req() { |
| urlPath = null; |
| pathInfoUndecoded = null; |
| return req; |
| } |
| |
| /** |
| * Specifies the HTTP servlet response object on this call. |
| * |
| * @param value The value for this setting. |
| * @return This object. |
| */ |
| public Builder res(HttpServletResponse value) { |
| res = value; |
| return this; |
| } |
| |
| /** |
| * Returns the HTTP servlet response object on this call. |
| * |
| * @return The HTTP servlet response object on this call. |
| */ |
| public HttpServletResponse res() { |
| return res; |
| } |
| |
| /** |
| * Specifies the logger to use for this session. |
| * |
| * @param value The value for this setting. |
| * @return This object. |
| */ |
| public Builder logger(RestLogger value) { |
| logger = value; |
| return this; |
| } |
| |
| @Override /* Session.Builder */ |
| public RestSession build() { |
| return new RestSession(this); |
| } |
| |
| /** |
| * Returns the request path info as a {@link UrlPath} bean. |
| * |
| * @return The request path info as a {@link UrlPath} bean. |
| */ |
| public UrlPath getUrlPath() { |
| if (urlPath == null) |
| urlPath = UrlPath.of(getPathInfoUndecoded()); |
| return urlPath; |
| } |
| |
| /** |
| * Returns the request path info as a {@link UrlPath} bean. |
| * |
| * @return The request path info as a {@link UrlPath} bean. |
| */ |
| public String getPathInfoUndecoded() { |
| if (pathInfoUndecoded == null) |
| pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); |
| return pathInfoUndecoded; |
| } |
| |
| /** |
| * Adds resolved <c><ja>@Resource</ja>(path)</c> variable values to this call. |
| * |
| * @param value The variables to add to this call. |
| * @return This object. |
| */ |
| @SuppressWarnings("unchecked") |
| public Builder pathVars(Map<String,String> value) { |
| if (value != null && ! value.isEmpty()) { |
| Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); |
| if (m == null) { |
| m = new TreeMap<>(); |
| req.setAttribute(REST_PATHVARS_ATTR, m); |
| } |
| m.putAll(value); |
| } |
| return this; |
| } |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Static |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| private final Object resource; |
| private final RestContext context; |
| private HttpServletRequest req; |
| private HttpServletResponse res; |
| |
| private RestLogger logger; |
| private UrlPath urlPath; |
| private String pathInfoUndecoded; |
| private long startTime = System.currentTimeMillis(); |
| private BeanStore beanStore; |
| private Map<String,String[]> queryParams; |
| private String method; |
| private RestOpSession opSession; |
| |
| private UrlPathMatch urlPathMatch; |
| |
| /** |
| * Constructor. |
| * |
| * @param builder The builder for this object. |
| */ |
| public RestSession(Builder builder) { |
| super(builder); |
| context = builder.ctx; |
| resource = builder.resource; |
| beanStore = BeanStore.of(context.getRootBeanStore(), resource).addBean(RestContext.class, context); |
| |
| req = beanStore.add(HttpServletRequest.class, builder.req); |
| res = beanStore.add(HttpServletResponse.class, builder.res); |
| logger = beanStore.add(RestLogger.class, builder.logger); |
| urlPath = beanStore.add(UrlPath.class, builder.urlPath); |
| pathInfoUndecoded = builder.pathInfoUndecoded; |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------ |
| // Fluent setters. |
| //------------------------------------------------------------------------------------------------------------------ |
| |
| /** |
| * Sets the logger to use when logging this call. |
| * |
| * @param value The new value for this setting. Can be <jk>null</jk>. |
| * @return This object. |
| */ |
| public RestSession logger(RestLogger value) { |
| logger = beanStore.add(RestLogger.class, value); |
| return this; |
| } |
| |
| /** |
| * Enables or disabled debug mode on this call. |
| * |
| * @param value The new value for this setting. |
| * @return This object. |
| * @throws IOException Occurs if request content could not be cached into memory. |
| */ |
| public RestSession debug(boolean value) throws IOException { |
| if (value) { |
| req = CachingHttpServletRequest.wrap(req); |
| res = CachingHttpServletResponse.wrap(res); |
| req.setAttribute("Debug", true); |
| } else { |
| req.removeAttribute("Debug"); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the HTTP status on this call. |
| * |
| * @param value The status code. |
| * @return This object. |
| */ |
| public RestSession status(int value) { |
| res.setStatus(value); |
| return this; |
| } |
| |
| /** |
| * Sets the HTTP status on this call. |
| * |
| * @param value The status code. |
| * @return This object. |
| */ |
| @SuppressWarnings("deprecation") |
| public RestSession status(StatusLine value) { |
| if (value != null) |
| res.setStatus(value.getStatusCode(), value.getReasonPhrase()); |
| return this; |
| } |
| |
| /** |
| * Identifies that an exception occurred during this call. |
| * |
| * @param value The thrown exception. |
| * @return This object. |
| */ |
| public RestSession exception(Throwable value) { |
| req.setAttribute("Exception", value); |
| beanStore.addBean(Throwable.class, value); |
| return this; |
| } |
| |
| /** |
| * Sets the URL path pattern match on this call. |
| * |
| * @param value The match pattern. |
| * @return This object. |
| */ |
| public RestSession urlPathMatch(UrlPathMatch value) { |
| urlPathMatch = beanStore.add(UrlPathMatch.class, value); |
| return this; |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------ |
| // Getters |
| //------------------------------------------------------------------------------------------------------------------ |
| |
| /** |
| * Returns the HTTP servlet request of this REST call. |
| * |
| * @return the HTTP servlet request of this REST call. |
| */ |
| public HttpServletRequest getRequest() { |
| return req; |
| } |
| |
| /** |
| * Returns the HTTP servlet response of this REST call. |
| * |
| * @return the HTTP servlet response of this REST call. |
| */ |
| public HttpServletResponse getResponse() { |
| return res; |
| } |
| |
| /** |
| * Returns the bean store of this call. |
| * |
| * @return The bean store of this call. |
| */ |
| public BeanStore getBeanStore() { |
| return beanStore; |
| } |
| |
| /** |
| * Returns resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. |
| * |
| * @return Resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. |
| */ |
| @SuppressWarnings("unchecked") |
| public Map<String,String> getPathVars() { |
| Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); |
| return m == null ? Collections.emptyMap() : m; |
| } |
| |
| /** |
| * Returns the URL path pattern match on this call. |
| * |
| * @return The URL path pattern match on this call. |
| */ |
| public UrlPathMatch getUrlPathMatch() { |
| return urlPathMatch; |
| } |
| |
| /** |
| * Returns the exception that occurred during this call. |
| * |
| * @return The exception that occurred during this call. |
| */ |
| public Throwable getException() { |
| return (Throwable)req.getAttribute("Exception"); |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------ |
| // Lifecycle methods. |
| //------------------------------------------------------------------------------------------------------------------ |
| |
| /** |
| * Called at the end of a call to finish any remaining tasks such as flushing buffers and logging the response. |
| * |
| * @return This object. |
| */ |
| public RestSession finish() { |
| try { |
| req.setAttribute("ExecTime", System.currentTimeMillis() - startTime); |
| if (opSession != null) |
| opSession.finish(); |
| else { |
| res.flushBuffer(); |
| } |
| } catch (Exception e) { |
| exception(e); |
| } |
| if (logger != null) |
| logger.log(req, res); |
| return this; |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------ |
| // Pass-through convenience methods. |
| //------------------------------------------------------------------------------------------------------------------ |
| |
| /** |
| * Shortcut for calling <c>getRequest().getServletPath()</c>. |
| * |
| * @return The request servlet path. |
| */ |
| public String getServletPath() { |
| return req.getServletPath(); |
| } |
| |
| /** |
| * Returns the request path info as a {@link UrlPath} bean. |
| * |
| * @return The request path info as a {@link UrlPath} bean. |
| */ |
| public UrlPath getUrlPath() { |
| if (urlPath == null) |
| urlPath = UrlPath.of(getPathInfoUndecoded()); |
| return urlPath; |
| } |
| |
| /** |
| * Shortcut for calling <c>getRequest().getPathInfo()</c>. |
| * |
| * @return The request servlet path info. |
| */ |
| public String getPathInfo() { |
| return req.getPathInfo(); |
| } |
| |
| /** |
| * Same as {@link #getPathInfo()} but doesn't decode encoded characters. |
| * |
| * @return The undecoded request servlet path info. |
| */ |
| public String getPathInfoUndecoded() { |
| if (pathInfoUndecoded == null) |
| pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); |
| return pathInfoUndecoded; |
| } |
| |
| /** |
| * Returns the query parameters on the request. |
| * |
| * <p> |
| * Unlike {@link HttpServletRequest#getParameterMap()}, this doesn't parse the content if it's a POST. |
| * |
| * @return The query parameters on the request. |
| */ |
| public Map<String,String[]> getQueryParams() { |
| if (queryParams == null) { |
| if (req.getMethod().equalsIgnoreCase("POST")) |
| queryParams = RestUtils.parseQuery(req.getQueryString(), map()); |
| else |
| queryParams = req.getParameterMap(); |
| } |
| return queryParams; |
| } |
| |
| /** |
| * Returns the HTTP method name. |
| * |
| * @return The HTTP method name, always uppercased. |
| */ |
| public String getMethod() { |
| if (method == null) { |
| |
| Set<String> s1 = context.getAllowedMethodParams(); |
| Set<String> s2 = context.getAllowedMethodHeaders(); |
| |
| if (! s1.isEmpty()) { |
| String[] x = getQueryParams().get("method"); |
| if (x != null && (s1.contains("*") || s1.contains(x[0]))) |
| method = x[0]; |
| } |
| |
| if (method == null && ! s2.isEmpty()) { |
| String x = req.getHeader("X-Method"); |
| if (x != null && (s2.contains("*") || s2.contains(x))) |
| method = x; |
| } |
| |
| if (method == null) |
| method = req.getMethod(); |
| |
| method = method.toUpperCase(Locale.ENGLISH); |
| } |
| |
| return method; |
| } |
| |
| /** |
| * Shortcut for calling <c>getRequest().getStatus()</c>. |
| * |
| * @return The response status code. |
| */ |
| public int getStatus() { |
| return res.getStatus(); |
| } |
| |
| /** |
| * Returns the context that created this call. |
| * |
| * @return The context that created this call. |
| */ |
| @Override |
| public RestContext getContext() { |
| return context; |
| } |
| |
| /** |
| * Returns the REST object. |
| * |
| * @return The rest object. |
| */ |
| public Object getResource() { |
| return resource; |
| } |
| |
| /** |
| * Returns the operation session of this REST session. |
| * |
| * <p> |
| * The operation session is created once the Java method to be invoked has been determined. |
| * |
| * @return The operation session of this REST session. |
| * @throws InternalServerError If operation session has not been created yet. |
| */ |
| public RestOpSession getOpSession() throws InternalServerError { |
| if (opSession == null) |
| throw new InternalServerError("Op Session not created."); |
| return opSession; |
| } |
| |
| /** |
| * Runs this session. |
| * |
| * <p> |
| * Does the following: |
| * <ol> |
| * <li>Finds the Java method to invoke and creates a {@link RestOpSession} for it. |
| * <li>Invokes {@link HookEvent#PRE_CALL} methods by calling {@link RestContext#preCall(RestOpSession)}. |
| * <li>Invokes Java method by calling {@link RestOpSession#run()}. |
| * <li>Invokes {@link HookEvent#POST_CALL} methods by calling {@link RestContext#postCall(RestOpSession)}. |
| * <li>If the Java method produced output, finds the response processor for it and runs it by calling {@link RestContext#processResponse(RestOpSession)}. |
| * <li>If no Java method matched, generates a 404/405/412 by calling {@link RestContext#handleNotFound(RestSession)}. |
| * </ol> |
| * |
| * @throws Throwable Any throwable can be thrown. |
| */ |
| public void run() throws Throwable { |
| try { |
| opSession = context.getRestOperations().findOperation(this).createSession(this).build(); |
| context.preCall(opSession); |
| opSession.run(); |
| context.postCall(opSession); |
| if (res.getStatus() == 0) |
| res.setStatus(200); |
| if (opSession.getResponse().hasContent()) { |
| // Now serialize the output if there was any. |
| // Some subclasses may write to the OutputStream or Writer directly. |
| context.processResponse(opSession); |
| } |
| } catch (NotFound e) { |
| if (getStatus() == 0) |
| status(404); |
| exception(e); |
| context.handleNotFound(this); |
| } |
| } |
| } |