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.CollectionUtils.*; 016import static org.apache.juneau.common.internal.StringUtils.*; 017import static org.apache.juneau.http.HttpHeaders.*; 018import static org.apache.juneau.httppart.HttpPartType.*; 019 020import java.io.*; 021import java.nio.charset.*; 022import java.util.*; 023 024import jakarta.servlet.*; 025import jakarta.servlet.http.*; 026 027import org.apache.http.*; 028import org.apache.juneau.*; 029import org.apache.juneau.collections.*; 030import org.apache.juneau.encoders.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.httppart.bean.*; 033import org.apache.juneau.marshaller.*; 034import org.apache.juneau.oapi.*; 035import org.apache.juneau.http.header.*; 036import org.apache.juneau.http.response.*; 037import org.apache.juneau.rest.httppart.*; 038import org.apache.juneau.rest.logger.*; 039import org.apache.juneau.rest.util.*; 040import org.apache.juneau.serializer.*; 041 042/** 043 * Represents an HTTP response for a REST resource. 044 * 045 * <p> 046 * The {@link RestResponse} object is an extension of the <l>HttpServletResponse</l> class 047 * with various built-in convenience methods for use in building REST interfaces. 048 * It can be accessed by passing it as a parameter on your REST Java method: 049 * </p> 050 * 051 * <p class='bjava'> 052 * <ja>@RestPost</ja>(...) 053 * <jk>public</jk> Object myMethod(RestResponse <jv>res</jv>) {...} 054 * </p> 055 * 056 * <p> 057 * The primary methods on this class are: 058 * </p> 059 * <ul class='javatree'> 060 * <li class='jc'>{@link RestResponse} 061 * <ul class='spaced-list'> 062 * <li>Methods for setting response headers: 063 * <ul class='javatreec'> 064 * <li class='jm'>{@link RestResponse#addHeader(Header) addHeader(Header)} 065 * <li class='jm'>{@link RestResponse#addHeader(String,String) addHeader(String,String)} 066 * <li class='jm'>{@link RestResponse#containsHeader(String) containsHeader(String)} 067 * <li class='jm'>{@link RestResponse#getHeader(String) getHeader(String)} 068 * <li class='jm'>{@link RestResponse#setCharacterEncoding(String) setCharacterEncoding(String)} 069 * <li class='jm'>{@link RestResponse#setContentType(String) setContentType(String)} 070 * <li class='jm'>{@link RestResponse#setHeader(Header) setHeader(Header)} 071 * <li class='jm'>{@link RestResponse#setHeader(HttpPartSchema,String,Object) setHeader(HttpPartSchema,String,Object)} 072 * <li class='jm'>{@link RestResponse#setHeader(String,Object) setHeader(String,Object)} 073 * <li class='jm'>{@link RestResponse#setHeader(String,String) setHeader(String,String)} 074 * <li class='jm'>{@link RestResponse#setMaxHeaderLength(int) setMaxHeaderLength(int)} 075 * <li class='jm'>{@link RestResponse#setSafeHeaders() setSafeHeaders()} 076 * </ul> 077 * <li>Methods for setting response bodies: 078 * <ul class='javatreec'> 079 * <li class='jm'>{@link RestResponse#flushBuffer() flushBuffer()} 080 * <li class='jm'>{@link RestResponse#getDirectWriter(String) getDirectWriter(String)} 081 * <li class='jm'>{@link RestResponse#getNegotiatedOutputStream() getNegotiatedOutputStream()} 082 * <li class='jm'>{@link RestResponse#getNegotiatedWriter() getNegotiatedWriter()} 083 * <li class='jm'>{@link RestResponse#getSerializerMatch() getSerializerMatch()} 084 * <li class='jm'>{@link RestResponse#getWriter() getWriter()} 085 * <li class='jm'>{@link RestResponse#sendPlainText(String) sendPlainText(String)} 086 * <li class='jm'>{@link RestResponse#sendRedirect(String) sendRedirect(String)} 087 * <li class='jm'>{@link RestResponse#setContentSchema(HttpPartSchema) setContentSchema(HttpPartSchema)} 088 * <li class='jm'>{@link RestResponse#setContent(Object) setOutput(Object)} 089 * <li class='jm'>{@link RestResponse#setResponseBeanMeta(ResponseBeanMeta) setResponseBeanMeta(ResponseBeanMeta)} 090 * <li class='jm'>{@link RestResponse#setException(Throwable) setException(Throwable)} 091 * </ul> 092 * <li>Other: 093 * <ul class='javatreec'> 094 * <li class='jm'>{@link RestResponse#getAttributes() getAttributes()} 095 * <li class='jm'>{@link RestResponse#getContext() getContext()} 096 * <li class='jm'>{@link RestResponse#getOpContext() getOpContext()} 097 * <li class='jm'>{@link RestResponse#setAttribute(String,Object) setAttribute(String,Object)} 098 * <li class='jm'>{@link RestResponse#setDebug() setDebug()} 099 * <li class='jm'>{@link RestResponse#setNoTrace() setNoTrace()} 100 * <li class='jm'>{@link RestResponse#setStatus(int) setStatus(int)} 101 * </ul> 102 * </ul> 103 * </ul> 104 * 105 * <h5 class='section'>See Also:</h5><ul> 106 107 * </ul> 108 */ 109public final class RestResponse extends HttpServletResponseWrapper { 110 111 private HttpServletResponse inner; 112 private final RestRequest request; 113 114 private Optional<Object> content; // The POJO being sent to the output. 115 private ServletOutputStream sos; 116 private FinishableServletOutputStream os; 117 private FinishablePrintWriter w; 118 private ResponseBeanMeta responseBeanMeta; 119 private RestOpContext opContext; 120 private Optional<HttpPartSchema> contentSchema; 121 private Serializer serializer; 122 private Optional<SerializerMatch> serializerMatch; 123 private boolean safeHeaders; 124 private int maxHeaderLength = 8096; 125 126 /** 127 * Constructor. 128 */ 129 RestResponse(RestOpContext opContext, RestSession session, RestRequest req) throws Exception { 130 super(session.getResponse()); 131 132 inner = session.getResponse(); 133 request = req; 134 135 this.opContext = opContext; 136 responseBeanMeta = opContext.getResponseMeta(); 137 138 RestContext context = session.getContext(); 139 140 try { 141 String passThroughHeaders = request.getHeaderParam("x-response-headers").orElse(null); 142 if (passThroughHeaders != null) { 143 JsonMap m = context.getPartParser().getPartSession().parse(HEADER, null, passThroughHeaders, BeanContext.DEFAULT.getClassMeta(JsonMap.class)); 144 for (Map.Entry<String,Object> e : m.entrySet()) 145 addHeader(e.getKey(), resolveUris(e.getValue())); 146 } 147 } catch (Exception e1) { 148 throw new BadRequest(e1, "Invalid format for header 'x-response-headers'. Must be in URL-encoded format."); 149 } 150 151 // Find acceptable charset 152 String h = request.getHeaderParam("accept-charset").orElse(null); 153 Charset charset = null; 154 if (h == null) 155 charset = opContext.getDefaultCharset(); 156 else for (StringRange r : StringRanges.of(h).toList()) { 157 if (r.getQValue() > 0) { 158 if (r.getName().equals("*")) 159 charset = opContext.getDefaultCharset(); 160 else if (Charset.isSupported(r.getName())) 161 charset = Charset.forName(r.getName()); 162 if (charset != null) 163 break; 164 } 165 } 166 167 request.getContext().getDefaultResponseHeaders().forEach(x->addHeader(x.getValue(), resolveUris(x.getValue()))); // Done this way to avoid list/array copy. 168 169 opContext.getDefaultResponseHeaders().forEach(x->addHeader(x.getName(), resolveUris(x.getValue()))); 170 171 if (charset == null) 172 throw new NotAcceptable("No supported charsets in header ''Accept-Charset'': ''{0}''", request.getHeaderParam("Accept-Charset").orElse(null)); 173 inner.setCharacterEncoding(charset.name()); 174 175 } 176 177 /** 178 * Returns access to the inner {@link RestContext} of the class of this method. 179 * 180 * @return The {@link RestContext} of this class. Never <jk>null</jk>. 181 */ 182 public RestContext getContext() { 183 return request.getContext(); 184 } 185 186 /** 187 * Returns access to the inner {@link RestOpContext} of this method. 188 * 189 * @return The {@link RestOpContext} of this method. Never <jk>null</jk>. 190 */ 191 public RestOpContext getOpContext() { 192 return request.getOpContext(); 193 } 194 195 /** 196 * Sets the HTTP output on the response. 197 * 198 * <p> 199 * The object type can be anything allowed by the registered response handlers. 200 * 201 * <p> 202 * Calling this method is functionally equivalent to returning the object in the REST Java method. 203 * 204 * <h5 class='section'>Example:</h5> 205 * <p class='bjava'> 206 * <ja>@RestGet</ja>(<js>"/example2/{personId}"</js>) 207 * <jk>public void</jk> doGet(RestResponse <jv>res</jv>, <ja>@Path</ja> UUID <jv>personId</jv>) { 208 * Person <jv>person</jv> = getPersonById(<jv>personId</jv>); 209 * <jv>res</jv>.setOutput(<jv>person</jv>); 210 * } 211 * </p> 212 * 213 * <h5 class='section'>Notes:</h5><ul> 214 * <li class='note'> 215 * Calling this method with a <jk>null</jk> value is NOT the same as not calling this method at all. 216 * <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>). 217 * <br>Not calling this method or returning a value means you're handing the response yourself via the underlying stream or writer. 218 * </ul> 219 * 220 * <h5 class='section'>See Also:</h5><ul> 221 * <li class='jm'>{@link RestContext.Builder#responseProcessors()} 222 * <li class='link'><a class="doclink" href="../../../../index.html#jrs.RestOpAnnotatedMethods">@RestOp-Annotated Methods</a> 223 * </ul> 224 * 225 * @param output The output to serialize to the connection. 226 * @return This object. 227 */ 228 public RestResponse setContent(Object output) { 229 this.content = optional(output); 230 return this; 231 } 232 233 /** 234 * Shortcut for calling <c>getRequest().getAttributes()</c>. 235 * 236 * @return The request attributes object. 237 */ 238 public RequestAttributes getAttributes() { 239 return request.getAttributes(); 240 } 241 242 /** 243 * Shortcut for calling <c>getRequest().setAttribute(String,Object)</c>. 244 * 245 * @param name The property name. 246 * @param value The property value. 247 * @return This object. 248 */ 249 public RestResponse setAttribute(String name, Object value) { 250 request.setAttribute(name, value); 251 return this; 252 } 253 254 /** 255 * Returns the output that was set by calling {@link #setContent(Object)}. 256 * 257 * <p> 258 * If it's null, then {@link #setContent(Object)} wasn't called. 259 * <br>If it contains an empty, then <c>setObject(<jk>null</jk>)</c> was called. 260 * <br>Otherwise, {@link #setContent(Object)} was called with a non-null value. 261 * 262 * @return The output object, or <jk>null</jk> if {@link #setContent(Object)} was never called. 263 */ 264 public Optional<Object> getContent() { 265 return content; 266 } 267 268 /** 269 * Returns <jk>true</jk> if the response contains output. 270 * 271 * <p> 272 * This implies {@link #setContent(Object)} has been called on this object. 273 * 274 * <p> 275 * Note that this also returns <jk>true</jk> even if {@link #setContent(Object)} was called with a <jk>null</jk> 276 * value as this means the response contains an output value of <jk>null</jk> as opposed to no value at all. 277 * 278 * @return <jk>true</jk> if the response contains output. 279 */ 280 public boolean hasContent() { 281 return content != null; 282 } 283 284 /** 285 * Sets the output to a plain-text message regardless of the content type. 286 * 287 * @param text The output text to send. 288 * @return This object. 289 * @throws IOException If a problem occurred trying to write to the writer. 290 */ 291 public RestResponse sendPlainText(String text) throws IOException { 292 setContentType("text/plain"); 293 getNegotiatedWriter().write(text); 294 return this; 295 } 296 297 /** 298 * Equivalent to {@link HttpServletResponse#getOutputStream()}, except wraps the output stream if an {@link Encoder} 299 * was found that matched the <c>Accept-Encoding</c> header. 300 * 301 * @return A negotiated output stream. 302 * @throws NotAcceptable If unsupported Accept-Encoding value specified. 303 * @throws IOException Thrown by underlying stream. 304 */ 305 public FinishableServletOutputStream getNegotiatedOutputStream() throws NotAcceptable, IOException { 306 if (os == null) { 307 Encoder encoder = null; 308 EncoderSet encoders = request.getOpContext().getEncoders(); 309 310 String ae = request.getHeaderParam("Accept-Encoding").orElse(null); 311 if (! (ae == null || ae.isEmpty())) { 312 EncoderMatch match = encoders.getEncoderMatch(ae); 313 if (match == null) { 314 // Identity should always match unless "identity;q=0" or "*;q=0" is specified. 315 if (ae.matches(".*(identity|\\*)\\s*;\\s*q\\s*=\\s*(0(?!\\.)|0\\.0).*")) { 316 throw new NotAcceptable( 317 "Unsupported encoding in request header ''Accept-Encoding'': ''{0}''\n\tSupported codings: {1}", 318 ae, Json5.of(encoders.getSupportedEncodings()) 319 ); 320 } 321 } else { 322 encoder = match.getEncoder(); 323 String encoding = match.getEncoding().toString(); 324 325 // Some clients don't recognize identity as an encoding, so don't set it. 326 if (! encoding.equals("identity")) 327 setHeader("content-encoding", encoding); 328 } 329 } 330 @SuppressWarnings("resource") 331 ServletOutputStream sos = getOutputStream(); 332 os = new FinishableServletOutputStream(encoder == null ? sos : encoder.getOutputStream(sos)); 333 } 334 return os; 335 } 336 337 /** 338 * Returns a ServletOutputStream suitable for writing binary data in the response. 339 * 340 * <p> 341 * The servlet container does not encode the binary data. 342 * 343 * <p> 344 * Calling <c>flush()</c> on the ServletOutputStream commits the response. 345 * Either this method or <c>getWriter</c> may be called to write the content, not both, except when reset has been called. 346 * 347 * @return The stream. 348 * @throws IOException If stream could not be accessed. 349 */ 350 @Override 351 public ServletOutputStream getOutputStream() throws IOException { 352 if (sos == null) 353 sos = inner.getOutputStream(); 354 return sos; 355 } 356 357 /** 358 * Returns <jk>true</jk> if {@link #getOutputStream()} has been called. 359 * 360 * @return <jk>true</jk> if {@link #getOutputStream()} has been called. 361 */ 362 public boolean getOutputStreamCalled() { 363 return sos != null; 364 } 365 366 /** 367 * Returns the writer to the response content. 368 * 369 * <p> 370 * This methods bypasses any specified encoders and returns a regular unbuffered writer. 371 * Use the {@link #getNegotiatedWriter()} method if you want to use the matched encoder (if any). 372 * 373 * @return The writer. 374 * @throws IOException If writer could not be accessed. 375 */ 376 @Override 377 public PrintWriter getWriter() throws IOException { 378 return getWriter(true, false); 379 } 380 381 /** 382 * Convenience method meant to be used when rendering directly to a browser with no buffering. 383 * 384 * <p> 385 * Sets the header <js>"x-content-type-options=nosniff"</js> so that output is rendered immediately on IE and Chrome 386 * without any buffering for content-type sniffing. 387 * 388 * <p> 389 * This can be useful if you want to render a streaming 'console' on a web page. 390 * 391 * @param contentType The value to set as the <c>Content-Type</c> on the response. 392 * @return The raw writer. 393 * @throws IOException Thrown by underlying stream. 394 */ 395 public PrintWriter getDirectWriter(String contentType) throws IOException { 396 setContentType(contentType); 397 setHeader("X-Content-Type-Options", "nosniff"); 398 setHeader("Content-Encoding", "identity"); 399 return getWriter(true, true); 400 } 401 402 /** 403 * Equivalent to {@link HttpServletResponse#getWriter()}, except wraps the output stream if an {@link Encoder} was 404 * found that matched the <c>Accept-Encoding</c> header and sets the <c>Content-Encoding</c> 405 * header to the appropriate value. 406 * 407 * @return The negotiated writer. 408 * @throws NotAcceptable If unsupported charset in request header Accept-Charset. 409 * @throws IOException Thrown by underlying stream. 410 */ 411 public FinishablePrintWriter getNegotiatedWriter() throws NotAcceptable, IOException { 412 return getWriter(false, false); 413 } 414 415 @SuppressWarnings("resource") 416 private FinishablePrintWriter getWriter(boolean raw, boolean autoflush) throws NotAcceptable, IOException { 417 if (w != null) 418 return w; 419 420 // If plain text requested, override it now. 421 if (request.isPlainText()) 422 setHeader("Content-Type", "text/plain"); 423 424 try { 425 OutputStream out = (raw ? getOutputStream() : getNegotiatedOutputStream()); 426 w = new FinishablePrintWriter(out, getCharacterEncoding(), autoflush); 427 return w; 428 } catch (UnsupportedEncodingException e) { 429 String ce = getCharacterEncoding(); 430 setCharacterEncoding("UTF-8"); 431 throw new NotAcceptable("Unsupported charset in request header ''Accept-Charset'': ''{0}''", ce); 432 } 433 } 434 435 /** 436 * Returns the <c>Content-Type</c> header stripped of the charset attribute if present. 437 * 438 * @return The <c>media-type</c> portion of the <c>Content-Type</c> header. 439 */ 440 public MediaType getMediaType() { 441 return MediaType.of(getContentType()); 442 } 443 444 /** 445 * Wrapper around {@link #getCharacterEncoding()} that converts the value to a {@link Charset}. 446 * 447 * @return The request character encoding converted to a {@link Charset}. 448 */ 449 public Charset getCharset() { 450 String s = getCharacterEncoding(); 451 return s == null ? null : Charset.forName(s); 452 } 453 454 /** 455 * Redirects to the specified URI. 456 * 457 * <p> 458 * Relative URIs are always interpreted as relative to the context root. 459 * This is similar to how WAS handles redirect requests, and is different from how Tomcat handles redirect requests. 460 * 461 * @param uri The redirection URL. 462 * @throws IOException If an input or output exception occurs 463 */ 464 @Override 465 public void sendRedirect(String uri) throws IOException { 466 char c = (uri.length() > 0 ? uri.charAt(0) : 0); 467 if (c != '/' && uri.indexOf("://") == -1) 468 uri = request.getContextPath() + '/' + uri; 469 inner.sendRedirect(uri); 470 } 471 472 /** 473 * Sets a response header with the given name and value. 474 * 475 * <p> 476 * If the header had already been set, the new value overwrites the previous one. 477 * 478 * <p> 479 * The {@link #containsHeader(String)} method can be used to test for the presence of a header before setting its value. 480 * 481 * @param name The header name. 482 * @param value The header value. 483 */ 484 @Override 485 public void setHeader(String name, String value) { 486 487 // Jetty doesn't set the content type correctly if set through this method. 488 // Tomcat/WAS does. 489 if (name.equalsIgnoreCase("Content-Type")) { 490 inner.setContentType(value); 491 ContentType ct = contentType(value); 492 if (ct != null && ct.getParameter("charset") != null) 493 inner.setCharacterEncoding(ct.getParameter("charset")); 494 } else { 495 if (safeHeaders) 496 value = stripInvalidHttpHeaderChars(value); 497 value = abbreviate(value, maxHeaderLength); 498 inner.setHeader(name, value); 499 } 500 } 501 502 /** 503 * Sets a header on the request. 504 * 505 * @param name The header name. 506 * @param value The header value. 507 * <ul> 508 * <li>Can be any POJO. 509 * <li>Converted to a string using the specified part serializer. 510 * </ul> 511 * @return This object. 512 * @throws SchemaValidationException Header failed schema validation. 513 * @throws SerializeException Header could not be serialized. 514 */ 515 public RestResponse setHeader(String name, Object value) throws SchemaValidationException, SerializeException { 516 setHeader(name, request.getPartSerializerSession().serialize(HEADER, null, value)); 517 return this; 518 } 519 520 /** 521 * Sets a header on the request. 522 * 523 * @param schema 524 * The schema to use to serialize the header, or <jk>null</jk> to use the default schema. 525 * @param name The header name. 526 * @param value The header value. 527 * <ul> 528 * <li>Can be any POJO. 529 * <li>Converted to a string using the specified part serializer. 530 * </ul> 531 * @return This object. 532 * @throws SchemaValidationException Header failed schema validation. 533 * @throws SerializeException Header could not be serialized. 534 */ 535 public RestResponse setHeader(HttpPartSchema schema, String name, Object value) throws SchemaValidationException, SerializeException { 536 setHeader(name, request.getPartSerializerSession().serialize(HEADER, schema, value)); 537 return this; 538 } 539 540 /** 541 * Specifies the schema for the response content. 542 * 543 * <p> 544 * Used by schema-aware serializers such as {@link OpenApiSerializer}. Ignored by other serializers. 545 * 546 * @param schema The content schema 547 * @return This object. 548 */ 549 public RestResponse setContentSchema(HttpPartSchema schema) { 550 this.contentSchema = optional(schema); 551 return this; 552 } 553 554 /** 555 * Sets the <js>"Exception"</js> attribute to the specified throwable. 556 * 557 * <p> 558 * This exception is used by {@link CallLogger} for logging purposes. 559 * 560 * @param t The attribute value. 561 * @return This object. 562 */ 563 public RestResponse setException(Throwable t) { 564 request.setException(t); 565 return this; 566 } 567 568 /** 569 * Sets the <js>"NoTrace"</js> attribute to the specified boolean. 570 * 571 * <p> 572 * This flag is used by {@link CallLogger} and tells it not to log the current request. 573 * 574 * @param b The attribute value. 575 * @return This object. 576 */ 577 public RestResponse setNoTrace(Boolean b) { 578 request.setNoTrace(b); 579 return this; 580 } 581 582 /** 583 * Shortcut for calling <c>setNoTrace(<jk>true</jk>)</c>. 584 * 585 * @return This object. 586 */ 587 public RestResponse setNoTrace() { 588 return setNoTrace(true); 589 } 590 591 /** 592 * Sets the <js>"Debug"</js> attribute to the specified boolean. 593 * 594 * <p> 595 * This flag is used by {@link CallLogger} to help determine how a request should be logged. 596 * 597 * @param b The attribute value. 598 * @return This object. 599 * @throws IOException If bodies could not be cached. 600 */ 601 public RestResponse setDebug(Boolean b) throws IOException { 602 request.setDebug(b); 603 if (b) 604 inner = CachingHttpServletResponse.wrap(inner); 605 return this; 606 } 607 608 /** 609 * Shortcut for calling <c>setDebug(<jk>true</jk>)</c>. 610 * 611 * @return This object. 612 * @throws IOException If bodies could not be cached. 613 */ 614 public RestResponse setDebug() throws IOException { 615 return setDebug(true); 616 } 617 618 /** 619 * Returns the metadata about this response. 620 * 621 * @return 622 * The metadata about this response. 623 * <br>Never <jk>null</jk>. 624 */ 625 public ResponseBeanMeta getResponseBeanMeta() { 626 return responseBeanMeta; 627 } 628 629 /** 630 * Sets metadata about this response. 631 * 632 * @param rbm The metadata about this response. 633 * @return This object. 634 */ 635 public RestResponse setResponseBeanMeta(ResponseBeanMeta rbm) { 636 this.responseBeanMeta = rbm; 637 return this; 638 } 639 640 /** 641 * Returns <jk>true</jk> if this response object is of the specified type. 642 * 643 * @param c The type to check against. 644 * @return <jk>true</jk> if this response object is of the specified type. 645 */ 646 public boolean isContentOfType(Class<?> c) { 647 return c.isInstance(getRawOutput()); 648 } 649 650 /** 651 * Returns this value cast to the specified class. 652 * 653 * @param <T> The class to cast to. 654 * @param c The class to cast to. 655 * @return This value cast to the specified class, or <jk>null</jk> if the object doesn't exist or isn't the specified type. 656 */ 657 @SuppressWarnings("unchecked") 658 public <T> T getContent(Class<T> c) { 659 if (isContentOfType(c)) 660 return (T)getRawOutput(); 661 return null; 662 } 663 664 /** 665 * Returns the wrapped servlet request. 666 * 667 * @return The wrapped servlet request. 668 */ 669 public HttpServletResponse getHttpServletResponse() { 670 return inner; 671 } 672 673 /** 674 * Forces any content in the buffer to be written to the client. 675 * 676 * <p> 677 * A call to this method automatically commits the response, meaning the status code and headers will be written. 678 * 679 * @throws IOException If an I/O error occurred. 680 */ 681 @Override 682 public void flushBuffer() throws IOException { 683 if (w != null) 684 w.flush(); 685 if (os != null) 686 os.flush(); 687 inner.flushBuffer(); 688 } 689 690 private Object getRawOutput() { 691 return content == null ? null : content.orElse(null); 692 } 693 694 /** 695 * Enabled safe-header mode. 696 * 697 * <p> 698 * When enabled, invalid characters such as CTRL characters will be stripped from header values 699 * before they get set. 700 * 701 * @return This object. 702 */ 703 public RestResponse setSafeHeaders() { 704 this.safeHeaders = true; 705 return this; 706 } 707 708 /** 709 * Specifies the maximum length for header values. 710 * 711 * <p> 712 * Header values that exceed this length will get truncated. 713 * 714 * @param value The new value for this setting. The default is <c>8096</c>. 715 * @return This object. 716 */ 717 public RestResponse setMaxHeaderLength(int value) { 718 this.maxHeaderLength = value; 719 return this; 720 } 721 722 /** 723 * Adds a response header with the given name and value. 724 * 725 * <p> 726 * This method allows response headers to have multiple values. 727 * 728 * <p> 729 * A no-op of either the name or value is <jk>null</jk>. 730 * 731 * <p> 732 * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, 733 * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. 734 * 735 * @param name The header name. 736 * @param value The header value. 737 */ 738 @Override 739 public void addHeader(String name, String value) { 740 if (name != null && value != null) { 741 if (name.equalsIgnoreCase("Content-Type")) 742 setHeader(name, value); 743 else { 744 if (safeHeaders) 745 value = stripInvalidHttpHeaderChars(value); 746 value = abbreviate(value, maxHeaderLength); 747 inner.addHeader(name, value); 748 } 749 } 750 } 751 752 /** 753 * Sets a response header. 754 * 755 * <p> 756 * Any previous header values are removed. 757 * 758 * <p> 759 * Value is added at the end of the headers. 760 * 761 * @param header The header. 762 * @return This object. 763 */ 764 public RestResponse setHeader(Header header) { 765 if (header == null) { 766 // Do nothing. 767 } else if (header instanceof BasicUriHeader) { 768 BasicUriHeader x = (BasicUriHeader)header; 769 setHeader(x.getName(), resolveUris(x.getValue())); 770 } else if (header instanceof SerializedHeader) { 771 SerializedHeader x = ((SerializedHeader)header).copyWith(request.getPartSerializerSession(), null); 772 String v = x.getValue(); 773 if (v != null && v.indexOf("://") != -1) 774 v = resolveUris(v); 775 setHeader(x.getName(), v); 776 } else { 777 setHeader(header.getName(), header.getValue()); 778 } 779 return this; 780 } 781 782 /** 783 * Adds a response header. 784 * 785 * <p> 786 * Any previous header values are preserved. 787 * 788 * <p> 789 * Value is added at the end of the headers. 790 * 791 * <p> 792 * If the header is a {@link BasicUriHeader}, the URI will be resolved using the {@link RestRequest#getUriResolver()} object. 793 * 794 * <p> 795 * If the header is a {@link SerializedHeader} and the serializer session is not set, it will be set to the one returned by {@link RestRequest#getPartSerializerSession()} before serialization. 796 * 797 * <p> 798 * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, 799 * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. 800 * 801 * @param header The header. 802 * @return This object. 803 */ 804 public RestResponse addHeader(Header header) { 805 if (header == null) { 806 // Do nothing. 807 } else if (header instanceof BasicUriHeader) { 808 BasicUriHeader x = (BasicUriHeader)header; 809 addHeader(x.getName(), resolveUris(x.getValue())); 810 } else if (header instanceof SerializedHeader) { 811 SerializedHeader x = ((SerializedHeader)header).copyWith(request.getPartSerializerSession(), null); 812 addHeader(x.getName(), resolveUris(x.getValue())); 813 } else { 814 addHeader(header.getName(), header.getValue()); 815 } 816 return this; 817 } 818 819 private String resolveUris(Object value) { 820 String s = stringify(value); 821 return request.getUriResolver().resolve(s); 822 } 823 824 /** 825 * Returns the matching serializer and media type for this response. 826 * 827 * @return The matching serializer, never <jk>null</jk>. 828 */ 829 public Optional<SerializerMatch> getSerializerMatch() { 830 if (serializerMatch != null) 831 return serializerMatch; 832 if (serializer != null) { 833 serializerMatch = optional(new SerializerMatch(getMediaType(), serializer)); 834 } else { 835 serializerMatch = optional(opContext.getSerializers().getSerializerMatch(request.getHeaderParam("Accept").orElse("*/*"))); 836 } 837 return serializerMatch; 838 } 839 840 /** 841 * Returns the schema of the response content. 842 * 843 * @return The schema of the response content, never <jk>null</jk>. 844 */ 845 public Optional<HttpPartSchema> getContentSchema() { 846 if (contentSchema != null) 847 return contentSchema; 848 if (responseBeanMeta != null) 849 contentSchema = optional(responseBeanMeta.getSchema()); 850 else { 851 ResponseBeanMeta rbm = opContext.getResponseBeanMeta(getContent(Object.class)); 852 if (rbm != null) 853 contentSchema = optional(rbm.getSchema()); 854 else 855 contentSchema = empty(); 856 } 857 return contentSchema; 858 } 859}