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