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}