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