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 javax.servlet.http.HttpServletResponse.*; 016import static org.apache.juneau.internal.IOUtils.*; 017import static org.apache.juneau.internal.StringUtils.*; 018import static org.apache.juneau.rest.Enablement.*; 019 020import java.io.*; 021import java.util.*; 022 023import javax.servlet.*; 024import javax.servlet.http.*; 025 026import org.apache.juneau.http.StreamResource; 027import org.apache.juneau.rest.RestContext.*; 028import org.apache.juneau.rest.exception.*; 029import org.apache.juneau.rest.util.*; 030 031/** 032 * Default implementation of {@link RestCallHandler}. 033 * 034 * <p> 035 * Subclasses can override these methods to tailor how HTTP REST calls are handled. 036 * <br>Subclasses MUST implement a public constructor that takes in a {@link RestContext} object. 037 * 038 * <ul class='seealso'> 039 * <li class='jf'>{@link RestContext#REST_callHandler} 040 * </ul> 041 */ 042public class BasicRestCallHandler implements RestCallHandler { 043 044 private final RestContext context; 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.restCallRouters = context.getCallRouters(); 055 } 056 057 /** 058 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object. 059 * 060 * <p> 061 * Subclasses may choose to override this method to provide a specialized request object. 062 * 063 * @param req The request object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. 064 * @return The wrapped request object. 065 * @throws ServletException If any errors occur trying to interpret the request. 066 */ 067 @Override /* RestCallHandler */ 068 public RestRequest createRequest(HttpServletRequest req) throws ServletException { 069 return new RestRequest(context, req); 070 } 071 072 /** 073 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object 074 * and the request returned by {@link #createRequest(HttpServletRequest)}. 075 * 076 * <p> 077 * Subclasses may choose to override this method to provide a specialized response object. 078 * 079 * @param req The request object returned by {@link #createRequest(HttpServletRequest)}. 080 * @param res The response object from the {@link #service(HttpServletRequest, HttpServletResponse)} method. 081 * @return The wrapped response object. 082 * @throws ServletException If any errors occur trying to interpret the request or response. 083 */ 084 @Override /* RestCallHandler */ 085 public RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException { 086 return new RestResponse(context, req, res); 087 } 088 089 /** 090 * The main service method. 091 * 092 * <p> 093 * Subclasses can optionally override this method if they want to tailor the behavior of requests. 094 * 095 * @param r1 The incoming HTTP servlet request object. 096 * @param r2 The incoming HTTP servlet response object. 097 * @throws ServletException General servlet exception. 098 * @throws IOException Thrown by underlying stream. 099 */ 100 @Override /* RestCallHandler */ 101 public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException { 102 103 long startTime = System.currentTimeMillis(); 104 RestRequest req = null; 105 RestResponse res = null; 106 RestCallLogger logger = context.getCallLogger(); 107 RestCallLoggerConfig loggerConfig = context.getCallLoggerConfig(); 108 109 try { 110 context.checkForInitException(); 111 112 String pathInfo = RestUtils.getPathInfoUndecoded(r1); // Can't use r1.getPathInfo() because we don't want '%2F' resolved. 113 UrlPathInfo upi = new UrlPathInfo(pathInfo); 114 115 // If the resource path contains variables (e.g. @RestResource(path="/f/{a}/{b}"), then we want to resolve 116 // those variables and push the servletPath to include the resolved variables. The new pathInfo will be 117 // the remainder after the new servletPath. 118 // Only do this for the top-level resource because the logic for child resources are processed next. 119 if (context.pathPattern.hasVars() && context.getParentContext() == null) { 120 String sp = r1.getServletPath(); 121 UrlPathInfo upi2 = new UrlPathInfo(pathInfo == null ? sp : sp + pathInfo); 122 UrlPathPatternMatch uppm = context.pathPattern.match(upi2); 123 if (uppm != null && ! uppm.hasEmptyVars()) { 124 RequestPath.addPathVars(r1, uppm.getVars()); 125 r1 = new OverrideableHttpServletRequest(r1) 126 .pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix()))) 127 .servletPath(uppm.getPrefix()); 128 pathInfo = RestUtils.getPathInfoUndecoded(r1); // Can't use r1.getPathInfo() because we don't want '%2F' resolved. 129 upi = new UrlPathInfo(pathInfo); 130 } else { 131 if (isDebug(r1)) { 132 r1 = CachingHttpServletRequest.wrap(r1); 133 r1.setAttribute("Debug", true); 134 } 135 r2.setStatus(SC_NOT_FOUND); 136 return; 137 } 138 } 139 140 // If this resource has child resources, try to recursively call them. 141 if (context.hasChildResources() && pathInfo != null && ! pathInfo.equals("/")) { 142 for (RestContext rc : context.getChildResources().values()) { 143 UrlPathPattern upp = rc.pathPattern; 144 UrlPathPatternMatch uppm = upp.match(upi); 145 if (uppm != null) { 146 if (! uppm.hasEmptyVars()) { 147 RequestPath.addPathVars(r1, uppm.getVars()); 148 HttpServletRequest childRequest = new OverrideableHttpServletRequest(r1) 149 .pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix()))) 150 .servletPath(r1.getServletPath() + uppm.getPrefix()); 151 rc.getCallHandler().service(childRequest, r2); 152 } else { 153 if (isDebug(r1)) { 154 r1 = CachingHttpServletRequest.wrap(r1); 155 r1.setAttribute("Debug", true); 156 } 157 r2.setStatus(SC_NOT_FOUND); 158 } 159 return; 160 } 161 } 162 } 163 164 if (isDebug(r1)) { 165 r1 = CachingHttpServletRequest.wrap(r1); 166 r2 = CachingHttpServletResponse.wrap(r2); 167 r1.setAttribute("Debug", true); 168 } 169 170 context.startCall(r1, r2); 171 172 req = createRequest(r1); 173 res = createResponse(req, r2); 174 req.setResponse(res); 175 context.setRequest(req); 176 context.setResponse(res); 177 String method = req.getMethod(); 178 String methodUC = method.toUpperCase(Locale.ENGLISH); 179 180 StreamResource r = null; 181 if (pathInfo != null) { 182 String p = pathInfo.substring(1); 183 if (context.isStaticFile(p)) { 184 StaticFile sf = context.resolveStaticFile(p); 185 r = sf.resource; 186 res.setResponseMeta(sf.meta); 187 } else if (p.equals("favicon.ico")) { 188 res.setOutput(null); 189 } 190 } 191 192 if (r != null) { 193 res.setStatus(SC_OK); 194 res.setOutput(r); 195 } else { 196 197 // If the specified method has been defined in a subclass, invoke it. 198 int rc = 0; 199 if (restCallRouters.containsKey(methodUC)) { 200 rc = restCallRouters.get(methodUC).invoke(upi, req, res); 201 } else if (restCallRouters.containsKey("*")) { 202 rc = restCallRouters.get("*").invoke(upi, req, res); 203 } 204 205 // Should be 405 if the URL pattern matched but HTTP method did not. 206 if (rc == 0) 207 for (RestCallRouter rcc : restCallRouters.values()) 208 if (rcc.matches(upi)) 209 rc = SC_METHOD_NOT_ALLOWED; 210 211 // Should be 404 if URL pattern didn't match. 212 if (rc == 0) 213 rc = SC_NOT_FOUND; 214 215 // If not invoked above, see if it's an OPTIONs request 216 if (rc != SC_OK) 217 handleNotFound(rc, req, res); 218 219 if (res.getStatus() == 0) 220 res.setStatus(rc); 221 } 222 223 if (res.hasOutput()) { 224 // Now serialize the output if there was any. 225 // Some subclasses may write to the OutputStream or Writer directly. 226 handleResponse(req, res); 227 } 228 229 // Make sure our writer in RestResponse gets written. 230 res.flushBuffer(); 231 req.close(); 232 233 r1 = req.getInner(); 234 r2 = res.getInner(); 235 loggerConfig = req.getCallLoggerConfig(); 236 237 r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 238 239 } catch (Throwable e) { 240 e = convertThrowable(e); 241 if (req != null) { 242 r1 = req.getInner(); 243 loggerConfig = req.getCallLoggerConfig(); 244 } 245 if (res != null) 246 r2 = res.getInner(); 247 r1.setAttribute("Exception", e); 248 r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 249 handleError(r1, r2, e); 250 } finally { 251 context.clearState(); 252 } 253 254 logger.log(loggerConfig, r1, r2); 255 context.finishCall(r1, r2); 256 } 257 258 private boolean isDebug(HttpServletRequest req) { 259 Enablement e = context.getDebug(); 260 if (e == TRUE) 261 return true; 262 if (e == FALSE) 263 return false; 264 return "true".equalsIgnoreCase(req.getHeader("X-Debug")); 265 } 266 267 /** 268 * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or 269 * returned by the Java method. 270 * 271 * <p> 272 * Subclasses may override this method if they wish to modify the way the output is rendered or support other output 273 * formats. 274 * 275 * <p> 276 * The default implementation simply iterates through the response handlers on this resource 277 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse)} method returns 278 * <jk>true</jk>. 279 * 280 * @param req The HTTP request. 281 * @param res The HTTP response. 282 * @throws IOException Thrown by underlying stream. 283 * @throws RestException Non-200 response. 284 */ 285 @Override /* RestCallHandler */ 286 public void handleResponse(RestRequest req, RestResponse res) throws IOException, RestException, NotImplemented { 287 // Loop until we find the correct handler for the POJO. 288 for (ResponseHandler h : context.getResponseHandlers()) 289 if (h.handle(req, res)) 290 return; 291 Object output = res.getOutput(); 292 throw new NotImplemented("No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'"); 293 } 294 295 /** 296 * Method that can be subclassed to allow uncaught throwables to be treated as other types of throwables. 297 * 298 * <p> 299 * The default implementation looks at the throwable class name to determine whether it can be converted to another type: 300 * 301 * <ul> 302 * <li><js>"*AccessDenied*"</js> - Converted to {@link Unauthorized}. 303 * <li><js>"*Empty*"</js>,<js>"*NotFound*"</js> - Converted to {@link NotFound}. 304 * </ul> 305 * 306 * @param t The thrown object. 307 * @return The converted thrown object. 308 */ 309 @Override 310 public Throwable convertThrowable(Throwable t) { 311 if (t instanceof RestException) 312 return t; 313 String n = t.getClass().getName(); 314 if (n.contains("AccessDenied")) 315 return new Unauthorized(t); 316 if (n.contains("Empty") || n.contains("NotFound")) 317 return new NotFound(t); 318 return t; 319 } 320 321 /** 322 * Handle the case where a matching method was not found. 323 * 324 * <p> 325 * Subclasses can override this method to provide a 2nd-chance for specifying a response. 326 * The default implementation will simply throw an exception with an appropriate message. 327 * 328 * @param rc The HTTP response code. 329 * @param req The HTTP request. 330 * @param res The HTTP response. 331 */ 332 @Override /* RestCallHandler */ 333 public void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception { 334 String pathInfo = req.getPathInfo(); 335 String methodUC = req.getMethod(); 336 String onPath = pathInfo == null ? " on no pathInfo" : String.format(" on path '%s'", pathInfo); 337 if (rc == SC_NOT_FOUND) 338 throw new NotFound("Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath); 339 else if (rc == SC_PRECONDITION_FAILED) 340 throw new PreconditionFailed("Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath); 341 else if (rc == SC_METHOD_NOT_ALLOWED) 342 throw new MethodNotAllowed("Method ''{0}'' not found on resource.", methodUC); 343 else 344 throw new ServletException("Invalid method response: " + rc); 345 } 346 347 /** 348 * Method for handling response errors. 349 * 350 * <p> 351 * Subclasses can override this method to provide their own custom error response handling. 352 * 353 * @param req The servlet request. 354 * @param res The servlet response. 355 * @param e The exception that occurred. 356 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. 357 */ 358 @Override /* RestCallHandler */ 359 @SuppressWarnings("deprecation") 360 public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, Throwable e) throws IOException { 361 362 int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e); 363 RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, 500)).setOccurrence(occurrence); 364 365 Throwable t = e2.getRootCause(); 366 if (t != null) { 367 res.setHeader("Exception-Name", t.getClass().getName()); 368 res.setHeader("Exception-Message", t.getMessage()); 369 } 370 371 try { 372 res.setContentType("text/plain"); 373 res.setHeader("Content-Encoding", "identity"); 374 res.setStatus(e2.getStatus()); 375 376 PrintWriter w = null; 377 try { 378 w = res.getWriter(); 379 } catch (IllegalStateException x) { 380 w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); 381 } 382 383 try (PrintWriter w2 = w) { 384 String httpMessage = RestUtils.getHttpResponseText(e2.getStatus()); 385 if (httpMessage != null) 386 w2.append("HTTP ").append(String.valueOf(e2.getStatus())).append(": ").append(httpMessage).append("\n\n"); 387 if (context != null && context.isRenderResponseStackTraces()) 388 e.printStackTrace(w2); 389 else 390 w2.append(e2.getFullStackMessage(true)); 391 } 392 393 } catch (Exception e1) { 394 req.setAttribute("Exception", e1); 395 } 396 } 397 398 /** 399 * Returns the session objects for the specified request. 400 * 401 * <p> 402 * The default implementation simply returns a single map containing <c>{'req':req}</c>. 403 * 404 * @param req The REST request. 405 * @return The session objects for that request. 406 */ 407 @Override /* RestCallHandler */ 408 public Map<String,Object> getSessionObjects(RestRequest req, RestResponse res) { 409 Map<String,Object> m = new HashMap<>(); 410 m.put("req", req); 411 m.put("res", res); 412 return m; 413 } 414}