org.apache.juneau.rest.RestCallHandler Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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 java.util.logging.Level.*;
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.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.vars.*;
/**
* Class that handles the basic lifecycle of an HTTP REST call.
*
*
* Subclasses can override these methods to tailor how HTTP REST calls are handled.
* Subclasses MUST implement a public constructor that takes in a {@link RestContext} object.
*
*
* RestCallHandlers are associated with servlets/resources in one of the following ways:
*
* - The {@link RestResource#callHandler @RestResource.callHandler()} annotation.
*
- The {@link RestConfig#setCallHandler(Class)} method.
*
*/
public class RestCallHandler {
private final RestContext context;
private final RestLogger logger;
private final Map callRouters;
/**
* Constructor.
*
* @param context The resource context.
*/
public RestCallHandler(RestContext context) {
this.context = context;
this.logger = context.getLogger();
this.callRouters = context.getCallRouters();
}
/**
* Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object.
*
*
* Subclasses may choose to override this method to provide a specialized request object.
*
* @param req The request object from the {@link #service(HttpServletRequest, HttpServletResponse)} method.
* @return The wrapped request object.
* @throws ServletException If any errors occur trying to interpret the request.
*/
protected RestRequest createRequest(HttpServletRequest req) throws ServletException {
return new RestRequest(context, req);
}
/**
* Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object
* and the request returned by {@link #createRequest(HttpServletRequest)}.
*
*
* Subclasses may choose to override this method to provide a specialized response object.
*
* @param req The request object returned by {@link #createRequest(HttpServletRequest)}.
* @param res The response object from the {@link #service(HttpServletRequest, HttpServletResponse)} method.
* @return The wrapped response object.
* @throws ServletException If any errors occur trying to interpret the request or response.
*/
protected RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException {
return new RestResponse(context, req, res);
}
/**
* The main service method.
*
*
* 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
* @throws IOException
*/
protected void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException {
logger.log(FINE, "HTTP: {0} {1}", r1.getMethod(), r1.getRequestURI());
long startTime = System.currentTimeMillis();
try {
context.checkForInitException();
String pathInfo = RestUtils.getPathInfoUndecoded(r1); // Can't use r1.getPathInfo() because we don't want '%2F' resolved.
// If this resource has child resources, try to recursively call them.
if (pathInfo != null && context.hasChildResources() && (! pathInfo.equals("/"))) {
int i = pathInfo.indexOf('/', 1);
String pathInfoPart = i == -1 ? pathInfo.substring(1) : pathInfo.substring(1, i);
RestContext childResource = context.getChildResource(pathInfoPart);
if (childResource != null) {
final String pathInfoRemainder = (i == -1 ? null : pathInfo.substring(i));
final String servletPath = r1.getServletPath() + "/" + pathInfoPart;
final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) {
@Override /* ServletRequest */
public String getPathInfo() {
return urlDecode(pathInfoRemainder);
}
@Override /* ServletRequest */
public String getServletPath() {
return servletPath;
}
};
childResource.getCallHandler().service(childRequest, r2);
return;
}
}
context.startCall(r1, r2);
RestRequest req = createRequest(r1);
RestResponse res = createResponse(req, r2);
String method = req.getMethod();
String methodUC = method.toUpperCase(Locale.ENGLISH);
StreamResource r = null;
if (pathInfo != null) {
String p = pathInfo.substring(1);
if (context.isStaticFile(p))
r = context.resolveStaticFile(p);
else if (p.equals("favicon.ico"))
res.setOutput(null);
}
if (r != null) {
res.setStatus(SC_OK);
res.setOutput(r);
} else {
// If the specified method has been defined in a subclass, invoke it.
int rc = SC_METHOD_NOT_ALLOWED;
if (callRouters.containsKey(methodUC)) {
rc = callRouters.get(methodUC).invoke(pathInfo, req, res);
} else if (callRouters.containsKey("*")) {
rc = callRouters.get("*").invoke(pathInfo, req, res);
}
// If not invoked above, see if it's an OPTIONs request
if (rc != SC_OK)
handleNotFound(rc, req, res);
}
if (res.hasOutput()) {
Object output = res.getOutput();
// Do any class-level transforming.
for (RestConverter converter : context.getConverters())
output = converter.convert(req, output, context.getBeanContext().getClassMetaForObject(output));
res.setOutput(output);
// Now serialize the output if there was any.
// Some subclasses may write to the OutputStream or Writer directly.
handleResponse(req, res, output);
}
// Make sure our writer in RestResponse gets written.
res.flushBuffer();
r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
} catch (RestException e) {
r1.setAttribute("Exception", e);
r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
handleError(r1, r2, e);
} catch (Throwable e) {
RestException e2 = new RestException(SC_INTERNAL_SERVER_ERROR, e);
r1.setAttribute("Exception", e);
r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
handleError(r1, r2, e2);
}
context.finishCall(r1, r2);
logger.log(FINE, "HTTP: [{0} {1}] finished in {2}ms", r1.getMethod(), r1.getRequestURI(), System.currentTimeMillis()-startTime);
}
/**
* The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or
* returned by the Java method.
*
*
* Subclasses may override this method if they wish to modify the way the output is rendered or support other output
* formats.
*
*
* The default implementation simply iterates through the response handlers on this resource
* looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse, Object)} method returns
* true .
*
* @param req The HTTP request.
* @param res The HTTP response.
* @param output The output to serialize in the response.
* @throws IOException
* @throws RestException
*/
protected void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException {
// Loop until we find the correct handler for the POJO.
for (ResponseHandler h : context.getResponseHandlers())
if (h.handle(req, res, output))
return;
throw new RestException(SC_NOT_IMPLEMENTED, "No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'");
}
/**
* Handle the case where a matching method was not found.
*
*
* 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 rc The HTTP response code.
* @param req The HTTP request.
* @param res The HTTP response.
* @throws Exception
*/
protected void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception {
String pathInfo = req.getPathInfo();
String methodUC = req.getMethod();
String onPath = pathInfo == null ? " on no pathInfo" : String.format(" on path '%s'", pathInfo);
if (rc == SC_NOT_FOUND)
throw new RestException(rc, "Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath);
else if (rc == SC_PRECONDITION_FAILED)
throw new RestException(rc, "Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath);
else if (rc == SC_METHOD_NOT_ALLOWED)
throw new RestException(rc, "Method ''{0}'' not found on resource.", methodUC);
else
throw new ServletException("Invalid method response: " + rc);
}
/**
* Method for handling response errors.
*
*
* The default implementation logs the error and calls
* {@link #renderError(HttpServletRequest,HttpServletResponse,RestException)}.
*
*
* Subclasses can override this method to provide their own custom error response handling.
*
* @param req The servlet request.
* @param res The servlet response.
* @param e The exception that occurred.
* @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
*/
protected synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException {
e.setOccurrence(context == null ? 0 : context.getStackTraceOccurrence(e));
logger.onError(req, res, e);
renderError(req, res, e);
}
/**
* Method for rendering response errors.
*
*
* The default implementation renders a plain text English message, optionally with a stack trace if
* {@link RestResource#renderResponseStackTraces()} is enabled.
*
*
* Subclasses can override this method to provide their own custom error response handling.
*
* @param req The servlet request.
* @param res The servlet response.
* @param e The exception that occurred.
* @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
*/
protected void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException {
int status = e.getStatus();
res.setStatus(status);
res.setContentType("text/plain");
res.setHeader("Content-Encoding", "identity");
Throwable t = e.getRootCause();
if (t != null) {
res.setHeader("Exception-Name", t.getClass().getName());
res.setHeader("Exception-Message", t.getMessage());
}
PrintWriter w = null;
try {
w = res.getWriter();
} catch (IllegalStateException e2) {
w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8));
}
String httpMessage = RestUtils.getHttpResponseText(status);
if (httpMessage != null)
w.append("HTTP ").append(String.valueOf(status)).append(": ").append(httpMessage).append("\n\n");
if (context != null && context.isRenderResponseStackTraces())
e.printStackTrace(w);
else
w.append(e.getFullStackMessage(true));
w.flush();
w.close();
}
/**
* Returns the session objects for the specified request.
*
*
* The default implementation simply returns a single map containing {'req':req}
.
*
* @param req The REST request.
* @return The session objects for that request.
*/
public Map getSessionObjects(RestRequest req) {
Map m = new HashMap();
m.put(RequestVar.SESSION_req, req);
return m;
}
}