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>"<p>This is my REST interface</p>"</js> 217 * }, 218 * aside={ 219 * <js>"<p>Custom aside content</p>"</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>"<p>This is my REST interface</p>"</js>) 228 * .aside(<js>"<p>Custom aside content</p>"</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}