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