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 org.apache.juneau.internal.StringUtils.*;
016
017import java.io.*;
018import java.nio.charset.*;
019import java.util.*;
020
021import javax.servlet.*;
022import javax.servlet.http.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.encoders.*;
026import org.apache.juneau.http.*;
027import org.apache.juneau.httppart.*;
028import org.apache.juneau.rest.annotation.*;
029import org.apache.juneau.serializer.*;
030
031/**
032 * Represents an HTTP response for a REST resource.
033 * 
034 * <p>
035 * Essentially an extended {@link HttpServletResponse} with some special convenience methods that allow you to easily
036 * output POJOs as responses.
037 * 
038 * <p>
039 * Since this class extends {@link HttpServletResponse}, developers are free to use these convenience methods, or
040 * revert to using lower level methods like any other servlet response.
041 * 
042 * <h5 class='section'>Example:</h5>
043 * <p class='bcode'>
044 *    <ja>@RestMethod</ja>(name=<jsf>GET</jsf>)
045 *    <jk>public void</jk> doGet(RestRequest req, RestResponse res) {
046 *       res.setOutput(<js>"Simple string response"</js>);
047 *    }
048 * </p>
049 * 
050 * <h5 class='section'>See Also:</h5>
051 * <ul>
052 *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.RestResponse">Overview &gt; juneau-rest-server &gt; RestResponse</a>
053 * </ul>
054 */
055public final class RestResponse extends HttpServletResponseWrapper {
056
057   private final RestRequest request;
058   private RestJavaMethod restJavaMethod;
059   private Object output;                       // The POJO being sent to the output.
060   private boolean isNullOutput;                // The output is null (as opposed to not being set at all)
061   private RequestProperties properties;                // Response properties
062   private ServletOutputStream sos;
063   private FinishableServletOutputStream os;
064   private FinishablePrintWriter w;
065   private HtmlDocBuilder htmlDocBuilder;
066
067   /**
068    * Constructor.
069    */
070   RestResponse(RestContext context, RestRequest req, HttpServletResponse res) {
071      super(res);
072      this.request = req;
073
074      for (Map.Entry<String,Object> e : context.getDefaultResponseHeaders().entrySet())
075         setHeader(e.getKey(), asString(e.getValue()));
076
077      try {
078         String passThroughHeaders = req.getHeader("x-response-headers");
079         if (passThroughHeaders != null) {
080            HttpPartParser p = context.getPartParser();
081            ObjectMap m = p.parse(HttpPartType.HEADER, passThroughHeaders, context.getBeanContext().getClassMeta(ObjectMap.class));
082            for (Map.Entry<String,Object> e : m.entrySet())
083               setHeader(e.getKey(), e.getValue().toString());
084         }
085      } catch (Exception e1) {
086         throw new RestException(SC_BAD_REQUEST, "Invalid format for header 'x-response-headers'.  Must be in URL-encoded format.").initCause(e1);
087      }
088   }
089
090   /*
091    * Called from RestServlet after a match has been made but before the guard or method invocation.
092    */
093   final void init(RestJavaMethod rjm, RequestProperties properties) {
094      this.restJavaMethod = rjm;
095      this.properties = properties;
096
097      // Find acceptable charset
098      String h = request.getHeader("accept-charset");
099      String charset = null;
100      if (h == null)
101         charset = rjm.defaultCharset;
102      else for (MediaTypeRange r : MediaTypeRange.parse(h)) {
103         if (r.getQValue() > 0) {
104            MediaType mt = r.getMediaType();
105            if (mt.getType().equals("*"))
106               charset = rjm.defaultCharset;
107            else if (Charset.isSupported(mt.getType()))
108               charset = mt.getType();
109            if (charset != null)
110               break;
111         }
112      }
113
114      if (charset == null)
115         throw new RestException(SC_NOT_ACCEPTABLE, "No supported charsets in header ''Accept-Charset'': ''{0}''", request.getHeader("Accept-Charset"));
116      super.setCharacterEncoding(charset);
117   }
118
119   /**
120    * Gets the serializer group for the response.
121    * 
122    * <h5 class='section'>See Also:</h5>
123    * <ul>
124    *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.Serializers">Overview &gt; juneau-rest-server &gt; Serializers</a>
125    * </ul>
126    * 
127    * @return The serializer group for the response.
128    */
129   public SerializerGroup getSerializers() {
130      return restJavaMethod.serializers;
131   }
132
133   /**
134    * Returns the media types that are valid for <code>Accept</code> headers on the request.
135    * 
136    * @return The set of media types registered in the parser group of this request.
137    */
138   public List<MediaType> getSupportedMediaTypes() {
139      return restJavaMethod.supportedAcceptTypes;
140   }
141
142   /**
143    * Returns the codings that are valid for <code>Accept-Encoding</code> and <code>Content-Encoding</code> headers on
144    * the request.
145    * 
146    * @return The set of media types registered in the parser group of this request.
147    * @throws RestServletException
148    */
149   public List<String> getSupportedEncodings() throws RestServletException {
150      return restJavaMethod.encoders.getSupportedEncodings();
151   }
152
153   /**
154    * Sets the HTTP output on the response.
155    * 
156    * <p>
157    * The object type can be anything allowed by the registered response handlers.
158    * 
159    * <p>
160    * Calling this method is functionally equivalent to returning the object in the REST Java method.
161    * 
162    * <h5 class='section'>Example:</h5>
163    * <p class='bcode'>
164    *    <ja>@RestMethod</ja>(..., path=<js>"/example2/{personId}"</js>)
165    *    <jk>public void</jk> doGet2(RestResponse res, <ja>@Path</ja> UUID personId) {
166    *       Person p = getPersonById(personId);
167    *       res.setOutput(p);
168    *    }
169    * </p>
170    * 
171    * <h5 class='section'>Notes:</h5>
172    * <ul class='spaced-list'>
173    *    <li>
174    *       Calling this method with a <jk>null</jk> value is NOT the same as not calling this method at all.
175    *       <br>A <jk>null</jk> output value means we want to serialize <jk>null</jk> as a response (e.g. as a JSON <code>null</code>).
176    *       <br>Not calling this method or returning a value means you're handing the response yourself via the underlying stream or writer.
177    *       <br>This distinction affects the {@link #hasOutput()} method behavior.
178    * </ul>
179    * 
180    * <h5 class='section'>See Also:</h5>
181    * <ul>
182    *    <li class='jf'>{@link RestContext#REST_responseHandlers}
183    *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.MethodReturnTypes">Overview &gt; juneau-rest-server &gt; Method Return Types</a>
184    * </ul>
185    * 
186    * @param output The output to serialize to the connection.
187    * @return This object (for method chaining).
188    */
189   public RestResponse setOutput(Object output) {
190      this.output = output;
191      this.isNullOutput = output == null;
192      return this;
193   }
194
195   /**
196    * Returns a programmatic interface for setting properties for the HTML doc view.
197    * 
198    * <p>
199    * This is the programmatic equivalent to the {@link RestMethod#htmldoc() @RestMethod.htmldoc()} annotation.
200    * 
201    * <h5 class='section'>Example:</h5>
202    * <p class='bcode'>
203    *    <jc>// Declarative approach.</jc>
204    *    <ja>@RestMethod</ja>(
205    *       htmldoc=<ja>@HtmlDoc</ja>(
206    *          header={
207    *             <js>"&lt;p&gt;This is my REST interface&lt;/p&gt;"</js>
208    *          },
209    *          aside={
210    *             <js>"&lt;p&gt;Custom aside content&lt;/p&gt;"</js>
211    *          }
212    *       )
213    *    )
214    *    <jk>public</jk> Object doGet(RestResponse res) {
215    *       
216    *       <jc>// Equivalent programmatic approach.</jc>
217    *       res.getHtmlDocBuilder()
218    *          .header(<js>"&lt;p&gt;This is my REST interface&lt;/p&gt;"</js>)
219    *          .aside(<js>"&lt;p&gt;Custom aside content&lt;/p&gt;"</js>);
220    *    }
221    * </p>
222    * 
223    * <h5 class='section'>See Also:</h5>
224    * <ul>
225    *    <li class='ja'>{@link RestMethod#htmldoc()}
226    *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.HtmlDocAnnotation">Overview &gt; juneau-rest-server &gt; @HtmlDoc</a>
227    * </ul>
228    * 
229    * @return A new programmatic interface for setting properties for the HTML doc view.
230    */
231   public HtmlDocBuilder getHtmlDocBuilder() {
232      if (htmlDocBuilder == null)
233         htmlDocBuilder = new HtmlDocBuilder(properties);
234      return htmlDocBuilder;
235   }
236
237   /**
238    * Retrieve the properties active for this request.
239    * 
240    * <p>
241    * This contains all resource and method level properties from the following:
242    * <ul>
243    *    <li class='ja'>{@link RestResource#properties()}
244    *    <li class='ja'>{@link RestMethod#properties()}
245    *    <li class='jm'>{@link RestContextBuilder#set(String, Object)}
246    * </ul>
247    * 
248    * <p>
249    * The returned object is modifiable and allows you to override session-level properties before
250    * they get passed to the serializers.
251    * <br>However, properties are open-ended, and can be used for any purpose.
252    * 
253    * <h5 class='section'>Example:</h5>
254    * <p class='bcode'>
255    *    <ja>@RestMethod</ja>(
256    *       properties={
257    *          <ja>@Property</ja>(name=<jsf>SERIALIZER_sortMaps</jsf>, value=<js>"false"</js>)
258    *       }
259    *    )
260    *    <jk>public</jk> Map doGet(RestResponse res, <ja>@Query</ja>(<js>"sortMaps"</js>) Boolean sortMaps) {
261    *       
262    *       <jc>// Override value if specified through query parameter.</jc>
263    *       <jk>if</jk> (sortMaps != <jk>null</jk>)
264    *          res.getProperties().put(<jsf>SERIALIZER_sortMaps</jsf>, sortMaps);
265    * 
266    *       <jk>return</jk> <jsm>getMyMap</jsm>();
267    *    }
268    * </p>
269    * 
270    * <h5 class='section'>See Also:</h5>
271    * <ul>
272    *    <li class='jm'>{@link #prop(String, Object)}
273    *    <li class='link'><a class="doclink" href="../../../../overview-summary.html#juneau-rest-server.Properties">Overview &gt; juneau-rest-server &gt; Properties</a>
274    * </ul>
275    * 
276    * @return The properties active for this request.
277    */
278   public RequestProperties getProperties() {
279      return properties;
280   }
281
282   /**
283    * Shortcut for calling <code>getProperties().append(name, value);</code> fluently.
284    * 
285    * @param name The property name.
286    * @param value The property value.
287    * @return This object (for method chaining).
288    */
289   public RestResponse prop(String name, Object value) {
290      this.properties.append(name, value);
291      return this;
292   }
293
294   /**
295    * Shortcut method that allows you to use var-args to simplify setting array output.
296    * 
297    * <h5 class='section'>Example:</h5>
298    * <p class='bcode'>
299    *    <jc>// Instead of...</jc>
300    *    response.setOutput(<jk>new</jk> Object[]{x,y,z});
301    * 
302    *    <jc>// ...call this...</jc>
303    *    response.setOutput(x,y,z);
304    * </p>
305    * 
306    * @param output The output to serialize to the connection.
307    * @return This object (for method chaining).
308    */
309   public RestResponse setOutputs(Object...output) {
310      this.output = output;
311      return this;
312   }
313
314   /**
315    * Returns the output that was set by calling {@link #setOutput(Object)}.
316    * 
317    * @return The output object.
318    */
319   public Object getOutput() {
320      return output;
321   }
322
323   /**
324    * Returns <jk>true</jk> if this response has any output associated with it.
325    * 
326    * @return <jk>true</jk> if {@link #setOutput(Object)} has been called, even if the value passed was <jk>null</jk>.
327    */
328   public boolean hasOutput() {
329      return output != null || isNullOutput;
330   }
331
332   /**
333    * Sets the output to a plain-text message regardless of the content type.
334    * 
335    * @param text The output text to send.
336    * @return This object (for method chaining).
337    * @throws IOException If a problem occurred trying to write to the writer.
338    */
339   public RestResponse sendPlainText(String text) throws IOException {
340      setContentType("text/plain");
341      getNegotiatedWriter().write(text);
342      return this;
343   }
344
345   /**
346    * Equivalent to {@link HttpServletResponse#getOutputStream()}, except wraps the output stream if an {@link Encoder}
347    * was found that matched the <code>Accept-Encoding</code> header.
348    * 
349    * @return A negotiated output stream.
350    * @throws IOException
351    */
352   public FinishableServletOutputStream getNegotiatedOutputStream() throws IOException {
353      if (os == null) {
354         Encoder encoder = null;
355         EncoderGroup encoders = restJavaMethod.encoders;
356         
357         String ae = request.getHeader("Accept-Encoding");
358         if (! (ae == null || ae.isEmpty())) {
359            EncoderMatch match = encoders.getEncoderMatch(ae);
360            if (match == null) {
361               // Identity should always match unless "identity;q=0" or "*;q=0" is specified.
362               if (ae.matches(".*(identity|\\*)\\s*;\\s*q\\s*=\\s*(0(?!\\.)|0\\.0).*")) {
363                  throw new RestException(SC_NOT_ACCEPTABLE,
364                     "Unsupported encoding in request header ''Accept-Encoding'': ''{0}''\n\tSupported codings: {1}",
365                     ae, encoders.getSupportedEncodings()
366                  );
367               }
368            } else {
369               encoder = match.getEncoder();
370               String encoding = match.getEncoding().toString();
371
372               // Some clients don't recognize identity as an encoding, so don't set it.
373               if (! encoding.equals("identity"))
374                  setHeader("content-encoding", encoding);
375            }
376         }
377         @SuppressWarnings("resource")
378         ServletOutputStream sos = getOutputStream();
379         os = new FinishableServletOutputStream(encoder == null ? sos : encoder.getOutputStream(sos)); 
380      }
381      return os;
382   }
383
384   @Override /* ServletResponse */
385   public ServletOutputStream getOutputStream() throws IOException {
386      if (sos == null)
387         sos = super.getOutputStream();
388      return sos;
389   }
390
391   /**
392    * Returns <jk>true</jk> if {@link #getOutputStream()} has been called.
393    * 
394    * @return <jk>true</jk> if {@link #getOutputStream()} has been called.
395    */
396   public boolean getOutputStreamCalled() {
397      return sos != null;
398   }
399
400   /**
401    * Returns the writer to the response body.
402    * 
403    * <p>
404    * This methods bypasses any specified encoders and returns a regular unbuffered writer.
405    * Use the {@link #getNegotiatedWriter()} method if you want to use the matched encoder (if any).
406    */
407   @Override /* ServletResponse */
408   public PrintWriter getWriter() throws IOException {
409      return getWriter(true);
410   }
411
412   /**
413    * Convenience method meant to be used when rendering directly to a browser with no buffering.
414    * 
415    * <p>
416    * Sets the header <js>"x-content-type-options=nosniff"</js> so that output is rendered immediately on IE and Chrome
417    * without any buffering for content-type sniffing.
418    * 
419    * <p>
420    * This can be useful if you want to render a streaming 'console' on a web page.
421    * 
422    * @param contentType The value to set as the <code>Content-Type</code> on the response.
423    * @return The raw writer.
424    * @throws IOException
425    */
426   public PrintWriter getDirectWriter(String contentType) throws IOException {
427      setContentType(contentType);
428      setHeader("x-content-type-options", "nosniff");
429      return getWriter();
430   }
431
432   /**
433    * Equivalent to {@link HttpServletResponse#getWriter()}, except wraps the output stream if an {@link Encoder} was
434    * found that matched the <code>Accept-Encoding</code> header and sets the <code>Content-Encoding</code>
435    * header to the appropriate value.
436    * 
437    * @return The negotiated writer.
438    * @throws IOException
439    */
440   public FinishablePrintWriter getNegotiatedWriter() throws IOException {
441      return getWriter(false);
442   }
443
444   @SuppressWarnings("resource")
445   private FinishablePrintWriter getWriter(boolean raw) throws IOException {
446      if (w != null)
447         return w;
448
449      // If plain text requested, override it now.
450      if (request.isPlainText())
451         setHeader("Content-Type", "text/plain");
452
453      try {
454         OutputStream out = (raw ? getOutputStream() : getNegotiatedOutputStream());
455         w = new FinishablePrintWriter(out, getCharacterEncoding());
456         return w;
457      } catch (UnsupportedEncodingException e) {
458         String ce = getCharacterEncoding();
459         setCharacterEncoding("UTF-8");
460         throw new RestException(SC_NOT_ACCEPTABLE, "Unsupported charset in request header ''Accept-Charset'': ''{0}''", ce);
461      }
462   }
463
464   /**
465    * Returns the <code>Content-Type</code> header stripped of the charset attribute if present.
466    * 
467    * @return The <code>media-type</code> portion of the <code>Content-Type</code> header.
468    */
469   public MediaType getMediaType() {
470      return MediaType.forString(getContentType());
471   }
472
473   /**
474    * Redirects to the specified URI.
475    * 
476    * <p>
477    * Relative URIs are always interpreted as relative to the context root.
478    * This is similar to how WAS handles redirect requests, and is different from how Tomcat handles redirect requests.
479    */
480   @Override /* ServletResponse */
481   public void sendRedirect(String uri) throws IOException {
482      char c = (uri.length() > 0 ? uri.charAt(0) : 0);
483      if (c != '/' && uri.indexOf("://") == -1)
484         uri = request.getContextPath() + '/' + uri;
485      super.sendRedirect(uri);
486   }
487
488   /**
489    * Returns the HTTP-part serializer associated with this response.
490    * 
491    * @return The HTTP-part serializer associated with this response.
492    */
493   public HttpPartSerializer getPartSerializer() {
494      return restJavaMethod.partSerializer;
495   }
496
497   @Override /* ServletResponse */
498   public void setHeader(String name, String value) {
499      // Jetty doesn't set the content type correctly if set through this method.
500      // Tomcat/WAS does.
501      if (name.equalsIgnoreCase("Content-Type"))
502         super.setContentType(value);
503      else
504         super.setHeader(name, value);
505   }
506
507
508   @Override /* ServletResponse */
509   public void flushBuffer() throws IOException {
510      if (w != null)
511         w.flush();
512      if (os != null)
513         os.flush();
514      super.flushBuffer();
515   }
516}