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 > juneau-rest-server > 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 > juneau-rest-server > 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 > juneau-rest-server > 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>"<p>This is my REST interface</p>"</js> 208 * }, 209 * aside={ 210 * <js>"<p>Custom aside content</p>"</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>"<p>This is my REST interface</p>"</js>) 219 * .aside(<js>"<p>Custom aside content</p>"</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 > juneau-rest-server > @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 > juneau-rest-server > 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}