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