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}