| // *************************************************************************************************************************** |
| // * 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 static org.apache.juneau.rest.Enablement.*; |
| |
| import java.io.*; |
| import java.util.*; |
| |
| import javax.servlet.*; |
| import javax.servlet.http.*; |
| |
| import org.apache.juneau.http.StreamResource; |
| import org.apache.juneau.http.annotation.*; |
| import org.apache.juneau.rest.RestContext.*; |
| import org.apache.juneau.http.exception.*; |
| import org.apache.juneau.reflect.*; |
| import org.apache.juneau.rest.util.*; |
| |
| /** |
| * Default implementation of {@link RestCallHandler}. |
| * |
| * <p> |
| * Subclasses can override these methods to tailor how HTTP REST calls are handled. |
| * <br>Subclasses MUST implement a public constructor that takes in a {@link RestContext} object. |
| * |
| * <ul class='seealso'> |
| * <li class='jf'>{@link RestContext#REST_callHandler} |
| * </ul> |
| */ |
| public class BasicRestCallHandler implements RestCallHandler { |
| |
| private final RestContext context; |
| private final Map<String,RestCallRouter> restCallRouters; |
| |
| /** |
| * Constructor. |
| * |
| * @param context The resource context. |
| */ |
| public BasicRestCallHandler(RestContext context) { |
| this.context = context; |
| this.restCallRouters = context.getCallRouters(); |
| } |
| |
| @Override /* RestCallHandler */ |
| public RestCall createCall(HttpServletRequest req, HttpServletResponse res) { |
| return new RestCall(req, res).logger(context.getCallLogger()).loggerConfig(context.getCallLoggerConfig()); |
| } |
| |
| @Override /* RestCallHandler */ |
| public RestRequest createRequest(RestCall call) throws ServletException { |
| return new RestRequest(context, call.getRequest()); |
| } |
| |
| @Override /* RestCallHandler */ |
| public RestResponse createResponse(RestCall call) throws ServletException { |
| return new RestResponse(context, call.getRestRequest(), call.getResponse()); |
| } |
| |
| /** |
| * The main service method. |
| * |
| * <p> |
| * Subclasses can optionally override this method if they want to tailor the behavior of requests. |
| * |
| * @param r1 The incoming HTTP servlet request object. |
| * @param r2 The incoming HTTP servlet response object. |
| * @throws ServletException General servlet exception. |
| * @throws IOException Thrown by underlying stream. |
| */ |
| @Override /* RestCallHandler */ |
| public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException { |
| |
| RestCall call = createCall(r1, r2); |
| |
| try { |
| context.checkForInitException(); |
| |
| // If the resource path contains variables (e.g. @Rest(path="/f/{a}/{b}"), then we want to resolve |
| // those variables and push the servletPath to include the resolved variables. The new pathInfo will be |
| // the remainder after the new servletPath. |
| // Only do this for the top-level resource because the logic for child resources are processed next. |
| if (context.pathPattern.hasVars() && context.getParentContext() == null) { |
| String sp = call.getServletPath(); |
| String pi = call.getPathInfoUndecoded(); |
| UrlPathInfo upi2 = new UrlPathInfo(pi == null ? sp : sp + pi); |
| UrlPathPatternMatch uppm = context.pathPattern.match(upi2); |
| if (uppm != null && ! uppm.hasEmptyVars()) { |
| RequestPath.addPathVars(call.getRequest(), uppm.getVars()); |
| call.request( |
| new OverrideableHttpServletRequest(call.getRequest()) |
| .pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix()))) |
| .servletPath(uppm.getPrefix()) |
| ); |
| } else { |
| call.debug(isDebug(call)).status(SC_NOT_FOUND).finish(); |
| return; |
| } |
| } |
| |
| // If this resource has child resources, try to recursively call them. |
| String pi = call.getPathInfoUndecoded(); |
| if (context.hasChildResources() && pi != null && ! pi.equals("/")) { |
| for (RestContext rc : context.getChildResources().values()) { |
| UrlPathPattern upp = rc.pathPattern; |
| UrlPathPatternMatch uppm = upp.match(call.getUrlPathInfo()); |
| if (uppm != null) { |
| if (! uppm.hasEmptyVars()) { |
| RequestPath.addPathVars(call.getRequest(), uppm.getVars()); |
| HttpServletRequest childRequest = new OverrideableHttpServletRequest(call.getRequest()) |
| .pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix()))) |
| .servletPath(call.getServletPath() + uppm.getPrefix()); |
| rc.getCallHandler().service(childRequest, call.getResponse()); |
| } else { |
| call.debug(isDebug(call)).status(SC_NOT_FOUND).finish(); |
| } |
| return; |
| } |
| } |
| } |
| |
| call.debug(isDebug(call)); |
| |
| context.startCall(call); |
| |
| call.restRequest(createRequest(call)); |
| call.restResponse(createResponse(call)); |
| |
| context.setRequest(call.getRestRequest()); |
| context.setResponse(call.getRestResponse()); |
| |
| StreamResource r = null; |
| if (call.getPathInfoUndecoded() != null) { |
| String p = call.getPathInfoUndecoded().substring(1); |
| if (context.isStaticFile(p)) { |
| StaticFile sf = context.resolveStaticFile(p); |
| r = sf.resource; |
| call.responseMeta(sf.meta); |
| } else if (p.equals("favicon.ico")) { |
| call.output(null); |
| } |
| } |
| |
| if (r != null) { |
| call.status(SC_OK); |
| call.output(r); |
| } else { |
| |
| // If the specified method has been defined in a subclass, invoke it. |
| int rc = 0; |
| String m = call.getMethod(); |
| |
| if (restCallRouters.containsKey(m)) |
| rc = restCallRouters.get(m).invoke(call); |
| |
| if ((rc == 0 || rc == 404) && restCallRouters.containsKey("*")) |
| rc = restCallRouters.get("*").invoke(call); |
| |
| // Should be 405 if the URL pattern matched but HTTP method did not. |
| if (rc == 0) |
| for (RestCallRouter rcc : restCallRouters.values()) |
| if (rcc.matches(call)) |
| rc = SC_METHOD_NOT_ALLOWED; |
| |
| // Should be 404 if URL pattern didn't match. |
| if (rc == 0) |
| rc = SC_NOT_FOUND; |
| |
| // If not invoked above, see if it's an OPTIONs request |
| if (rc != SC_OK) |
| handleNotFound(call.status(rc)); |
| |
| if (call.getStatus() == 0) |
| call.status(rc); |
| } |
| |
| if (call.hasOutput()) { |
| // Now serialize the output if there was any. |
| // Some subclasses may write to the OutputStream or Writer directly. |
| handleResponse(call); |
| } |
| |
| |
| } catch (Throwable e) { |
| handleError(call, convertThrowable(e)); |
| } finally { |
| context.clearState(); |
| } |
| |
| call.finish(); |
| context.finishCall(call); |
| } |
| |
| private boolean isDebug(RestCall call) { |
| Enablement e = context.getDebug(); |
| if (e == TRUE) |
| return true; |
| if (e == FALSE) |
| return false; |
| return "true".equalsIgnoreCase(call.getRequest().getHeader("X-Debug")); |
| } |
| |
| /** |
| * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or |
| * returned by the Java method. |
| * |
| * <p> |
| * Subclasses may override this method if they wish to modify the way the output is rendered or support other output |
| * formats. |
| * |
| * <p> |
| * The default implementation simply iterates through the response handlers on this resource |
| * looking for the first one whose {@link ResponseHandler#handle(RestRequest,RestResponse)} method returns |
| * <jk>true</jk>. |
| * |
| * @param call The HTTP call. |
| * @throws IOException Thrown by underlying stream. |
| * @throws HttpException Non-200 response. |
| */ |
| @Override /* RestCallHandler */ |
| public void handleResponse(RestCall call) throws IOException, HttpException, NotImplemented { |
| |
| RestRequest req = call.getRestRequest(); |
| RestResponse res = call.getRestResponse(); |
| |
| // Loop until we find the correct handler for the POJO. |
| for (ResponseHandler h : context.getResponseHandlers()) |
| if (h.handle(req, res)) |
| return; |
| |
| Object output = res.getOutput(); |
| throw new NotImplemented("No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'"); |
| } |
| |
| /** |
| * Method that can be subclassed to allow uncaught throwables to be treated as other types of throwables. |
| * |
| * <p> |
| * The default implementation looks at the throwable class name to determine whether it can be converted to another type: |
| * |
| * <ul> |
| * <li><js>"*AccessDenied*"</js> - Converted to {@link Unauthorized}. |
| * <li><js>"*Empty*"</js>,<js>"*NotFound*"</js> - Converted to {@link NotFound}. |
| * </ul> |
| * |
| * @param t The thrown object. |
| * @return The converted thrown object. |
| */ |
| @SuppressWarnings("deprecation") |
| @Override |
| public Throwable convertThrowable(Throwable t) { |
| ClassInfo ci = ClassInfo.ofc(t); |
| if (ci.is(HttpRuntimeException.class)) |
| t = ((HttpRuntimeException)t).getInner(); |
| if (ci.isChildOf(RestException.class) || ci.hasAnnotation(Response.class)) |
| return t; |
| String n = t.getClass().getName(); |
| if (n.contains("AccessDenied")) |
| return new Unauthorized(t); |
| if (n.contains("Empty") || n.contains("NotFound")) |
| return new NotFound(t); |
| return t; |
| } |
| |
| /** |
| * Handle the case where a matching method was not found. |
| * |
| * <p> |
| * Subclasses can override this method to provide a 2nd-chance for specifying a response. |
| * The default implementation will simply throw an exception with an appropriate message. |
| * |
| * @param call The HTTP call. |
| */ |
| @Override /* RestCallHandler */ |
| public void handleNotFound(RestCall call) throws Exception { |
| String pathInfo = call.getPathInfo(); |
| String methodUC = call.getMethod(); |
| int rc = call.getStatus(); |
| String onPath = pathInfo == null ? " on no pathInfo" : String.format(" on path '%s'", pathInfo); |
| if (rc == SC_NOT_FOUND) |
| throw new NotFound("Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath); |
| else if (rc == SC_PRECONDITION_FAILED) |
| throw new PreconditionFailed("Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath); |
| else if (rc == SC_METHOD_NOT_ALLOWED) |
| throw new MethodNotAllowed("Method ''{0}'' not found on resource{1}.", methodUC, onPath); |
| else |
| throw new ServletException("Invalid method response: " + rc); |
| } |
| |
| /** |
| * Method for handling response errors. |
| * |
| * <p> |
| * Subclasses can override this method to provide their own custom error response handling. |
| * |
| * @param call The rest call. |
| * @param e The exception that occurred. |
| * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. |
| */ |
| @Override /* RestCallHandler */ |
| @SuppressWarnings("deprecation") |
| public synchronized void handleError(RestCall call, Throwable e) throws IOException { |
| |
| call.exception(e); |
| |
| int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e); |
| |
| int code = 500; |
| |
| ClassInfo ci = ClassInfo.ofc(e); |
| Response r = ci.getAnnotation(Response.class); |
| if (r != null) |
| if (r.code().length > 0) |
| code = r.code()[0]; |
| |
| RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, code)).setOccurrence(occurrence); |
| |
| HttpServletRequest req = call.getRequest(); |
| HttpServletResponse res = call.getResponse(); |
| |
| Throwable t = null; |
| if (e instanceof HttpRuntimeException) |
| t = ((HttpRuntimeException)e).getInner(); |
| if (t == null) |
| t = e2.getRootCause(); |
| if (t != null) { |
| res.setHeader("Exception-Name", stripInvalidHttpHeaderChars(t.getClass().getName())); |
| res.setHeader("Exception-Message", stripInvalidHttpHeaderChars(t.getMessage())); |
| } |
| |
| try { |
| res.setContentType("text/plain"); |
| res.setHeader("Content-Encoding", "identity"); |
| res.setStatus(e2.getStatus()); |
| |
| PrintWriter w = null; |
| try { |
| w = res.getWriter(); |
| } catch (IllegalStateException x) { |
| w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); |
| } |
| |
| try (PrintWriter w2 = w) { |
| String httpMessage = RestUtils.getHttpResponseText(e2.getStatus()); |
| if (httpMessage != null) |
| w2.append("HTTP ").append(String.valueOf(e2.getStatus())).append(": ").append(httpMessage).append("\n\n"); |
| if (context != null && context.isRenderResponseStackTraces()) |
| e.printStackTrace(w2); |
| else |
| w2.append(e2.getFullStackMessage(true)); |
| } |
| |
| } catch (Exception e1) { |
| req.setAttribute("Exception", e1); |
| } |
| } |
| |
| /** |
| * Returns the session objects for the specified request. |
| * |
| * <p> |
| * The default implementation simply returns a single map containing <c>{'req':req}</c>. |
| * |
| * @param req The REST request. |
| * @return The session objects for that request. |
| */ |
| @Override /* RestCallHandler */ |
| public Map<String,Object> getSessionObjects(RestRequest req, RestResponse res) { |
| Map<String,Object> m = new HashMap<>(); |
| m.put("req", req); |
| m.put("res", res); |
| return m; |
| } |
| } |