001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.rest; 014 015import static java.util.logging.Level.*; 016import static javax.servlet.http.HttpServletResponse.*; 017import static org.apache.juneau.internal.IOUtils.*; 018import static org.apache.juneau.internal.StringUtils.*; 019 020import java.io.*; 021import java.util.*; 022 023import javax.servlet.*; 024import javax.servlet.http.*; 025 026import org.apache.juneau.rest.annotation.*; 027import org.apache.juneau.rest.vars.*; 028 029/** 030 * Default implementation of {@link RestCallHandler}. 031 * 032 * <p> 033 * Subclasses can override these methods to tailor how HTTP REST calls are handled. 034 * <br>Subclasses MUST implement a public constructor that takes in a {@link RestContext} object. 035 * 036 * <h5 class='section'>See Also:</h5> 037 * <ul> 038 * <li class='jf'>{@link RestContext#REST_callHandler} 039 * </ul> 040 */ 041public class BasicRestCallHandler implements RestCallHandler { 042 043 private final RestContext context; 044 private final RestLogger logger; 045 private final Map<String,RestCallRouter> restCallRouters; 046 047 /** 048 * Constructor. 049 * 050 * @param context The resource context. 051 */ 052 public BasicRestCallHandler(RestContext context) { 053 this.context = context; 054 this.logger = context.getLogger(); 055 this.restCallRouters = context.getCallRouters(); 056 } 057 058 /** 059 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object. 060 * 061 * <p> 062 * Subclasses may choose to override this method to provide a specialized request object. 063 * 064 * @param req The request object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. 065 * @return The wrapped request object. 066 * @throws ServletException If any errors occur trying to interpret the request. 067 */ 068 @Override /* RestCallHandler */ 069 public RestRequest createRequest(HttpServletRequest req) throws ServletException { 070 return new RestRequest(context, req); 071 } 072 073 /** 074 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object 075 * and the request returned by {@link #createRequest(HttpServletRequest)}. 076 * 077 * <p> 078 * Subclasses may choose to override this method to provide a specialized response object. 079 * 080 * @param req The request object returned by {@link #createRequest(HttpServletRequest)}. 081 * @param res The response object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. 082 * @return The wrapped response object. 083 * @throws ServletException If any errors occur trying to interpret the request or response. 084 */ 085 @Override /* RestCallHandler */ 086 public RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException { 087 return new RestResponse(context, req, res); 088 } 089 090 /** 091 * The main service method. 092 * 093 * <p> 094 * Subclasses can optionally override this method if they want to tailor the behavior of requests. 095 * 096 * @param r1 The incoming HTTP servlet request object. 097 * @param r2 The incoming HTTP servlet response object. 098 * @throws ServletException 099 * @throws IOException 100 */ 101 @Override /* RestCallHandler */ 102 public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException { 103 104 logger.log(FINE, "HTTP: {0} {1}", r1.getMethod(), r1.getRequestURI()); 105 long startTime = System.currentTimeMillis(); 106 107 try { 108 context.checkForInitException(); 109 110 String pathInfo = RestUtils.getPathInfoUndecoded(r1); // Can't use r1.getPathInfo() because we don't want '%2F' resolved. 111 112 // If this resource has child resources, try to recursively call them. 113 if (pathInfo != null && context.hasChildResources() && (! pathInfo.equals("/"))) { 114 int i = pathInfo.indexOf('/', 1); 115 String pathInfoPart = i == -1 ? pathInfo.substring(1) : pathInfo.substring(1, i); 116 RestContext childResource = context.getChildResource(pathInfoPart); 117 if (childResource != null) { 118 final String pathInfoRemainder = (i == -1 ? null : pathInfo.substring(i)); 119 final String servletPath = r1.getServletPath() + "/" + pathInfoPart; 120 final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) { 121 @Override /* ServletRequest */ 122 public String getPathInfo() { 123 return urlDecode(pathInfoRemainder); 124 } 125 @Override /* ServletRequest */ 126 public String getServletPath() { 127 return servletPath; 128 } 129 }; 130 childResource.getCallHandler().service(childRequest, r2); 131 return; 132 } 133 } 134 135 context.startCall(r1, r2); 136 137 RestRequest req = createRequest(r1); 138 RestResponse res = createResponse(req, r2); 139 String method = req.getMethod(); 140 String methodUC = method.toUpperCase(Locale.ENGLISH); 141 142 StreamResource r = null; 143 if (pathInfo != null) { 144 String p = pathInfo.substring(1); 145 if (context.isStaticFile(p)) 146 r = context.resolveStaticFile(p); 147 else if (p.equals("favicon.ico")) 148 res.setOutput(null); 149 } 150 151 if (r != null) { 152 res.setStatus(SC_OK); 153 res.setOutput(r); 154 } else { 155 // If the specified method has been defined in a subclass, invoke it. 156 int rc = SC_METHOD_NOT_ALLOWED; 157 if (restCallRouters.containsKey(methodUC)) { 158 rc = restCallRouters.get(methodUC).invoke(pathInfo, req, res); 159 } else if (restCallRouters.containsKey("*")) { 160 rc = restCallRouters.get("*").invoke(pathInfo, req, res); 161 } 162 163 // If not invoked above, see if it's an OPTIONs request 164 if (rc != SC_OK) 165 handleNotFound(rc, req, res); 166 } 167 168 if (res.hasOutput()) { 169 Object output = res.getOutput(); 170 171 // Do any class-level transforming. 172 for (RestConverter converter : context.getConverters()) 173 output = converter.convert(req, output); 174 175 res.setOutput(output); 176 177 // Now serialize the output if there was any. 178 // Some subclasses may write to the OutputStream or Writer directly. 179 handleResponse(req, res, output); 180 } 181 182 // Make sure our writer in RestResponse gets written. 183 res.flushBuffer(); 184 req.close(); 185 186 r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 187 188 } catch (RestException e) { 189 r1.setAttribute("Exception", e); 190 r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 191 handleError(r1, r2, e); 192 } catch (Throwable e) { 193 RestException e2 = new RestException(SC_INTERNAL_SERVER_ERROR, e); 194 r1.setAttribute("Exception", e); 195 r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 196 handleError(r1, r2, e2); 197 } 198 199 context.finishCall(r1, r2); 200 201 logger.log(FINE, "HTTP: [{0} {1}] finished in {2}ms", r1.getMethod(), r1.getRequestURI(), System.currentTimeMillis()-startTime); 202 } 203 204 /** 205 * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or 206 * returned by the Java method. 207 * 208 * <p> 209 * Subclasses may override this method if they wish to modify the way the output is rendered or support other output 210 * formats. 211 * 212 * <p> 213 * The default implementation simply iterates through the response handlers on this resource 214 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse, Object)} method returns 215 * <jk>true</jk>. 216 * 217 * @param req The HTTP request. 218 * @param res The HTTP response. 219 * @param output The output to serialize in the response. 220 * @throws IOException 221 * @throws RestException 222 */ 223 @Override /* RestCallHandler */ 224 public void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException { 225 // Loop until we find the correct handler for the POJO. 226 for (ResponseHandler h : context.getResponseHandlers()) 227 if (h.handle(req, res, output)) 228 return; 229 throw new RestException(SC_NOT_IMPLEMENTED, "No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'"); 230 } 231 232 /** 233 * Handle the case where a matching method was not found. 234 * 235 * <p> 236 * Subclasses can override this method to provide a 2nd-chance for specifying a response. 237 * The default implementation will simply throw an exception with an appropriate message. 238 * 239 * @param rc The HTTP response code. 240 * @param req The HTTP request. 241 * @param res The HTTP response. 242 * @throws Exception 243 */ 244 @Override /* RestCallHandler */ 245 public void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception { 246 String pathInfo = req.getPathInfo(); 247 String methodUC = req.getMethod(); 248 String onPath = pathInfo == null ? " on no pathInfo" : String.format(" on path '%s'", pathInfo); 249 if (rc == SC_NOT_FOUND) 250 throw new RestException(rc, "Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath); 251 else if (rc == SC_PRECONDITION_FAILED) 252 throw new RestException(rc, "Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath); 253 else if (rc == SC_METHOD_NOT_ALLOWED) 254 throw new RestException(rc, "Method ''{0}'' not found on resource.", methodUC); 255 else 256 throw new ServletException("Invalid method response: " + rc); 257 } 258 259 /** 260 * Method for handling response errors. 261 * 262 * <p> 263 * The default implementation logs the error and calls 264 * {@link #renderError(HttpServletRequest,HttpServletResponse,RestException)}. 265 * 266 * <p> 267 * Subclasses can override this method to provide their own custom error response handling. 268 * 269 * @param req The servlet request. 270 * @param res The servlet response. 271 * @param e The exception that occurred. 272 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. 273 */ 274 @Override /* RestCallHandler */ 275 public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { 276 e.setOccurrence(context == null ? 0 : context.getStackTraceOccurrence(e)); 277 logger.onError(req, res, e); 278 renderError(req, res, e); 279 } 280 281 /** 282 * Method for rendering response errors. 283 * 284 * <p> 285 * The default implementation renders a plain text English message, optionally with a stack trace if 286 * {@link RestResource#renderResponseStackTraces() @RestResource.renderResponseStackTraces()} is enabled. 287 * 288 * <p> 289 * Subclasses can override this method to provide their own custom error response handling. 290 * 291 * @param req The servlet request. 292 * @param res The servlet response. 293 * @param e The exception that occurred. 294 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. 295 */ 296 @Override /* RestCallHandler */ 297 public void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { 298 299 int status = e.getStatus(); 300 res.setStatus(status); 301 res.setContentType("text/plain"); 302 res.setHeader("Content-Encoding", "identity"); 303 304 Throwable t = e.getRootCause(); 305 if (t != null) { 306 res.setHeader("Exception-Name", t.getClass().getName()); 307 res.setHeader("Exception-Message", t.getMessage()); 308 } 309 310 PrintWriter w = null; 311 try { 312 w = res.getWriter(); 313 } catch (IllegalStateException e2) { 314 w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); 315 } 316 317 try (PrintWriter w2 = w) { 318 String httpMessage = RestUtils.getHttpResponseText(status); 319 if (httpMessage != null) 320 w2.append("HTTP ").append(String.valueOf(status)).append(": ").append(httpMessage).append("\n\n"); 321 if (context != null && context.isRenderResponseStackTraces()) 322 e.printStackTrace(w2); 323 else 324 w2.append(e.getFullStackMessage(true)); 325 } 326 } 327 328 /** 329 * Returns the session objects for the specified request. 330 * 331 * <p> 332 * The default implementation simply returns a single map containing <code>{'req':req}</code>. 333 * 334 * @param req The REST request. 335 * @return The session objects for that request. 336 */ 337 @Override /* RestCallHandler */ 338 public Map<String,Object> getSessionObjects(RestRequest req) { 339 Map<String,Object> m = new HashMap<>(); 340 m.put(RequestVar.SESSION_req, req); 341 return m; 342 } 343}