001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest;
018
019import static org.apache.juneau.internal.CollectionUtils.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.http.*;
025import org.apache.juneau.*;
026import org.apache.juneau.cp.*;
027import org.apache.juneau.http.response.*;
028import org.apache.juneau.rest.annotation.*;
029import org.apache.juneau.rest.logger.*;
030import org.apache.juneau.rest.util.*;
031
032import jakarta.servlet.http.*;
033
034/**
035 * Represents a single HTTP request.
036 *
037 * <h5 class='section'>Notes:</h5><ul>
038 *    <li class='warn'>This class is not thread safe.
039 * </ul>
040 *
041 * <h5 class='section'>See Also:</h5><ul>
042 * </ul>
043 */
044public class RestSession extends ContextSession {
045
046   //-----------------------------------------------------------------------------------------------------------------
047   // Static
048   //-----------------------------------------------------------------------------------------------------------------
049
050   /**
051    * Request attribute name for passing path variables from parent to child.
052    */
053   private static final String REST_PATHVARS_ATTR = "juneau.pathVars";
054
055   /**
056    * Creates a builder of this object.
057    *
058    * @param ctx The context creating this builder.
059    * @return A new builder.
060    */
061   public static Builder create(RestContext ctx) {
062      return new Builder(ctx);
063   }
064
065   //-----------------------------------------------------------------------------------------------------------------
066   // Builder
067   //-----------------------------------------------------------------------------------------------------------------
068
069   /**
070    * Builder class.
071    */
072   public static class Builder extends ContextSession.Builder {
073
074      RestContext ctx;
075      Object resource;
076      HttpServletRequest req;
077      HttpServletResponse res;
078      CallLogger logger;
079      String pathInfoUndecoded;
080      UrlPath urlPath;
081
082      /**
083       * Constructor.
084       *
085       * @param ctx The context creating this session.
086       */
087      protected Builder(RestContext ctx) {
088         super(ctx);
089         this.ctx = ctx;
090      }
091
092      /**
093       * Specifies the servlet implementation bean.
094       *
095       * @param value The value for this setting.
096       * @return This object.
097       */
098      public Builder resource(Object value) {
099         resource = value;
100         return this;
101      }
102
103      /**
104       * Specifies the HTTP servlet request object on this call.
105       *
106       * @param value The value for this setting.
107       * @return This object.
108       */
109      public Builder req(HttpServletRequest value) {
110         req = value;
111         return this;
112      }
113
114      /**
115       * Returns the HTTP servlet request object on this call.
116       *
117       * @return The HTTP servlet request object on this call.
118       */
119      public HttpServletRequest req() {
120         urlPath = null;
121         pathInfoUndecoded = null;
122         return req;
123      }
124
125      /**
126       * Specifies the HTTP servlet response object on this call.
127       *
128       * @param value The value for this setting.
129       * @return This object.
130       */
131      public Builder res(HttpServletResponse value) {
132         res = value;
133         return this;
134      }
135
136      /**
137       * Returns the HTTP servlet response object on this call.
138       *
139       * @return The HTTP servlet response object on this call.
140       */
141      public HttpServletResponse res() {
142         return res;
143      }
144
145      /**
146       * Specifies the logger to use for this session.
147       *
148       * @param value The value for this setting.
149       * @return This object.
150       */
151      public Builder logger(CallLogger value) {
152         logger = value;
153         return this;
154      }
155
156      @Override /* Session.Builder */
157      public RestSession build() {
158         return new RestSession(this);
159      }
160
161      /**
162       * Returns the request path info as a {@link UrlPath} bean.
163       *
164       * @return The request path info as a {@link UrlPath} bean.
165       */
166      public UrlPath getUrlPath() {
167         if (urlPath == null)
168            urlPath = UrlPath.of(getPathInfoUndecoded());
169         return urlPath;
170      }
171
172      /**
173       * Returns the request path info as a {@link UrlPath} bean.
174       *
175       * @return The request path info as a {@link UrlPath} bean.
176       */
177      public String getPathInfoUndecoded() {
178         if (pathInfoUndecoded == null)
179            pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req);
180         return pathInfoUndecoded;
181      }
182
183      /**
184       * Adds resolved <c><ja>@Resource</ja>(path)</c> variable values to this call.
185       *
186       * @param value The variables to add to this call.
187       * @return This object.
188       */
189      @SuppressWarnings("unchecked")
190      public Builder pathVars(Map<String,String> value) {
191         if (value != null && ! value.isEmpty()) {
192            Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR);
193            if (m == null) {
194               m = new TreeMap<>();
195               req.setAttribute(REST_PATHVARS_ATTR, m);
196            }
197            m.putAll(value);
198         }
199         return this;
200      }
201   }
202
203   //-----------------------------------------------------------------------------------------------------------------
204   // Static
205   //-----------------------------------------------------------------------------------------------------------------
206
207   private final Object resource;
208   private final RestContext context;
209   private HttpServletRequest req;
210   private HttpServletResponse res;
211
212   private CallLogger logger;
213   private UrlPath urlPath;
214   private String pathInfoUndecoded;
215   private long startTime = System.currentTimeMillis();
216   private BeanStore beanStore;
217   private Map<String,String[]> queryParams;
218   private String method;
219   private RestOpSession opSession;
220
221   private UrlPathMatch urlPathMatch;
222
223   /**
224    * Constructor.
225    *
226    * @param builder The builder for this object.
227    */
228   public RestSession(Builder builder) {
229      super(builder);
230      context = builder.ctx;
231      resource = builder.resource;
232      beanStore = BeanStore.of(context.getBeanStore(), resource).addBean(RestContext.class, context);
233
234      req = beanStore.add(HttpServletRequest.class, builder.req);
235      res = beanStore.add(HttpServletResponse.class, builder.res);
236      logger = beanStore.add(CallLogger.class, builder.logger);
237      urlPath = beanStore.add(UrlPath.class, builder.urlPath);
238      pathInfoUndecoded = builder.pathInfoUndecoded;
239   }
240
241   //------------------------------------------------------------------------------------------------------------------
242   // Fluent setters.
243   //------------------------------------------------------------------------------------------------------------------
244
245   /**
246    * Sets the logger to use when logging this call.
247    *
248    * @param value The new value for this setting.  Can be <jk>null</jk>.
249    * @return This object.
250    */
251   public RestSession logger(CallLogger value) {
252      logger = beanStore.add(CallLogger.class, value);
253      return this;
254   }
255
256   /**
257    * Enables or disabled debug mode on this call.
258    *
259    * @param value The new value for this setting.
260    * @return This object.
261    * @throws IOException Occurs if request content could not be cached into memory.
262    */
263   public RestSession debug(boolean value) throws IOException {
264      if (value) {
265         req = CachingHttpServletRequest.wrap(req);
266         res = CachingHttpServletResponse.wrap(res);
267         req.setAttribute("Debug", true);
268      } else {
269         req.removeAttribute("Debug");
270      }
271      return this;
272   }
273
274   /**
275    * Sets the HTTP status on this call.
276    *
277    * @param value The status code.
278    * @return This object.
279    */
280   public RestSession status(int value) {
281      res.setStatus(value);
282      return this;
283   }
284
285   /**
286    * Sets the HTTP status on this call.
287    *
288    * @param value The status code.
289    * @return This object.
290    */
291   public RestSession status(StatusLine value) {
292      if (value != null)
293         res.setStatus(value.getStatusCode());
294      return this;
295   }
296
297   /**
298    * Identifies that an exception occurred during this call.
299    *
300    * @param value The thrown exception.
301    * @return This object.
302    */
303   public RestSession exception(Throwable value) {
304      req.setAttribute("Exception", value);
305      beanStore.addBean(Throwable.class, value);
306      return this;
307   }
308
309   /**
310    * Sets the URL path pattern match on this call.
311    *
312    * @param value The match pattern.
313    * @return This object.
314    */
315   public RestSession urlPathMatch(UrlPathMatch value) {
316      urlPathMatch = beanStore.add(UrlPathMatch.class, value);
317      return this;
318   }
319
320   //------------------------------------------------------------------------------------------------------------------
321   // Getters
322   //------------------------------------------------------------------------------------------------------------------
323
324   /**
325    * Returns the HTTP servlet request of this REST call.
326    *
327    * @return the HTTP servlet request of this REST call.
328    */
329   public HttpServletRequest getRequest() {
330      return req;
331   }
332
333   /**
334    * Returns the HTTP servlet response of this REST call.
335    *
336    * @return the HTTP servlet response of this REST call.
337    */
338   public HttpServletResponse getResponse() {
339      return res;
340   }
341
342   /**
343    * Returns the bean store of this call.
344    *
345    * @return The bean store of this call.
346    */
347   public BeanStore getBeanStore() {
348      return beanStore;
349   }
350
351   /**
352    * Returns resolved <c><ja>@Resource</ja>(path)</c> variable values on this call.
353    *
354    * @return Resolved <c><ja>@Resource</ja>(path)</c> variable values on this call.
355    */
356   @SuppressWarnings("unchecked")
357   public Map<String,String> getPathVars() {
358      Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR);
359      return m == null ? Collections.emptyMap() : m;
360   }
361
362   /**
363    * Returns the URL path pattern match on this call.
364    *
365    * @return The URL path pattern match on this call.
366    */
367   public UrlPathMatch getUrlPathMatch() {
368      return urlPathMatch;
369   }
370
371   /**
372    * Returns the exception that occurred during this call.
373    *
374    * @return The exception that occurred during this call.
375    */
376   public Throwable getException() {
377      return (Throwable)req.getAttribute("Exception");
378   }
379
380   //------------------------------------------------------------------------------------------------------------------
381   // Lifecycle methods.
382   //------------------------------------------------------------------------------------------------------------------
383
384   /**
385    * Called at the end of a call to finish any remaining tasks such as flushing buffers and logging the response.
386    *
387    * @return This object.
388    */
389   public RestSession finish() {
390      try {
391         req.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
392         if (opSession != null)
393            opSession.finish();
394         else {
395            res.flushBuffer();
396         }
397      } catch (Exception e) {
398         exception(e);
399      }
400      if (logger != null)
401         logger.log(req, res);
402      return this;
403   }
404
405   //------------------------------------------------------------------------------------------------------------------
406   // Pass-through convenience methods.
407   //------------------------------------------------------------------------------------------------------------------
408
409   /**
410    * Shortcut for calling <c>getRequest().getServletPath()</c>.
411    *
412    * @return The request servlet path.
413    */
414   public String getServletPath() {
415      return req.getServletPath();
416   }
417
418   /**
419    * Returns the request path info as a {@link UrlPath} bean.
420    *
421    * @return The request path info as a {@link UrlPath} bean.
422    */
423   public UrlPath getUrlPath() {
424      if (urlPath == null)
425         urlPath = UrlPath.of(getPathInfoUndecoded());
426      return urlPath;
427   }
428
429   /**
430    * Shortcut for calling <c>getRequest().getPathInfo()</c>.
431    *
432    * @return The request servlet path info.
433    */
434   public String getPathInfo() {
435      return req.getPathInfo();
436   }
437
438   /**
439    * Same as {@link #getPathInfo()} but doesn't decode encoded characters.
440    *
441    * @return The undecoded request servlet path info.
442    */
443   public String getPathInfoUndecoded() {
444      if (pathInfoUndecoded == null)
445         pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req);
446      return pathInfoUndecoded;
447   }
448
449   /**
450    * Returns the query parameters on the request.
451    *
452    * <p>
453    * Unlike {@link HttpServletRequest#getParameterMap()}, this doesn't parse the content if it's a POST.
454    *
455    * @return The query parameters on the request.
456    */
457   public Map<String,String[]> getQueryParams() {
458      if (queryParams == null) {
459         if (req.getMethod().equalsIgnoreCase("POST"))
460            queryParams = RestUtils.parseQuery(req.getQueryString(), map());
461         else
462            queryParams = req.getParameterMap();
463      }
464      return queryParams;
465   }
466
467   /**
468    * Returns the HTTP method name.
469    *
470    * @return The HTTP method name, always uppercased.
471    */
472   public String getMethod() {
473      if (method == null) {
474
475         Set<String> s1 = context.getAllowedMethodParams();
476         Set<String> s2 = context.getAllowedMethodHeaders();
477
478         if (! s1.isEmpty()) {
479            String[] x = getQueryParams().get("method");
480            if (x != null && (s1.contains("*") || s1.contains(x[0])))
481               method = x[0];
482         }
483
484         if (method == null && ! s2.isEmpty()) {
485            String x = req.getHeader("X-Method");
486            if (x != null && (s2.contains("*") || s2.contains(x)))
487               method = x;
488         }
489
490         if (method == null)
491            method = req.getMethod();
492
493         method = method.toUpperCase(Locale.ENGLISH);
494      }
495
496      return method;
497   }
498
499   /**
500    * Shortcut for calling <c>getRequest().getStatus()</c>.
501    *
502    * @return The response status code.
503    */
504   public int getStatus() {
505      return res.getStatus();
506   }
507
508   /**
509    * Returns the context that created this call.
510    *
511    * @return The context that created this call.
512    */
513   @Override
514   public RestContext getContext() {
515      return context;
516   }
517
518   /**
519    * Returns the REST object.
520    *
521    * @return The rest object.
522    */
523   public Object getResource() {
524      return resource;
525   }
526
527   /**
528    * Returns the operation session of this REST session.
529    *
530    * <p>
531    * The operation session is created once the Java method to be invoked has been determined.
532    *
533    * @return The operation session of this REST session.
534    * @throws InternalServerError If operation session has not been created yet.
535    */
536   public RestOpSession getOpSession() throws InternalServerError {
537      if (opSession == null)
538         throw new InternalServerError("Op Session not created.");
539      return opSession;
540   }
541
542   /**
543    * Runs this session.
544    *
545    * <p>
546    * Does the following:
547    * <ol>
548    *    <li>Finds the Java method to invoke and creates a {@link RestOpSession} for it.
549    *    <li>Invokes {@link RestPreCall} methods by calling {@link RestContext#preCall(RestOpSession)}.
550    *    <li>Invokes Java method by calling {@link RestOpSession#run()}.
551    *    <li>Invokes {@link RestPostCall} methods by calling {@link RestContext#postCall(RestOpSession)}.
552    *    <li>If the Java method produced output, finds the response processor for it and runs it by calling {@link RestContext#processResponse(RestOpSession)}.
553    *    <li>If no Java method matched, generates a 404/405/412 by calling {@link RestContext#handleNotFound(RestSession)}.
554    * </ol>
555    *
556    * @throws Throwable Any throwable can be thrown.
557    */
558   public void run() throws Throwable {
559      try {
560         opSession = context.getRestOperations().findOperation(this).createSession(this).build();
561         context.preCall(opSession);
562         opSession.run();
563         context.postCall(opSession);
564         if (res.getStatus() == 0)
565            res.setStatus(200);
566         if (opSession.getResponse().hasContent()) {
567            // Now serialize the output if there was any.
568            // Some subclasses may write to the OutputStream or Writer directly.
569            context.processResponse(opSession);
570         }
571      } catch (NotFound e) {
572         if (getStatus() == 0)
573            status(404);
574         exception(e);
575         context.handleNotFound(this);
576      }
577   }
578}