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