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