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