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