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.server; 018 019import static java.time.format.DateTimeFormatter.*; 020import static java.time.temporal.ChronoUnit.*; 021import static org.apache.juneau.commons.httppart.HttpPartType.*; 022import static org.apache.juneau.commons.utils.Shorts.*; 023import static org.apache.juneau.commons.utils.StringUtils.*; 024 025import java.io.*; 026import java.nio.charset.*; 027import java.time.*; 028import java.util.*; 029import java.util.regex.*; 030 031import org.apache.juneau.commons.http.*; 032import org.apache.juneau.commons.httppart.*; 033import org.apache.juneau.http.*; 034import org.apache.juneau.http.header.*; 035import org.apache.juneau.http.response.*; 036import org.apache.juneau.httppart.bean.*; 037import org.apache.juneau.marshall.*; 038import org.apache.juneau.marshall.collections.*; 039import org.apache.juneau.marshall.encoders.*; 040import org.apache.juneau.marshall.httppart.*; 041import org.apache.juneau.marshall.marshaller.*; 042import org.apache.juneau.marshall.oapi.*; 043import org.apache.juneau.marshall.serializer.*; 044import org.apache.juneau.rest.server.httppart.*; 045import org.apache.juneau.rest.server.sse.*; 046import org.apache.juneau.rest.server.util.*; 047 048import jakarta.servlet.*; 049import jakarta.servlet.http.*; 050 051/** 052 * Represents an HTTP response for a REST resource. 053 * 054 * <p> 055 * The {@link RestResponse} object is an extension of the <l>HttpServletResponse</l> class 056 * with various built-in convenience methods for use in building REST interfaces. 057 * It can be accessed by passing it as a parameter on your REST Java method: 058 * </p> 059 * 060 * <p class='bjava'> 061 * <ja>@RestPost</ja>(...) 062 * <jk>public</jk> Object myMethod(RestResponse <jv>res</jv>) {...} 063 * </p> 064 * 065 * <p> 066 * The primary methods on this class are: 067 * </p> 068 * <ul class='javatree'> 069 * <li class='jc'>{@link RestResponse} 070 * <ul class='spaced-list'> 071 * <li>Methods for setting response headers: 072 * <ul class='javatreec'> 073 * <li class='jm'>{@link RestResponse#addHeader(HttpHeader) addHeader(HttpHeader)} 074 * <li class='jm'>{@link RestResponse#addHeader(String,String) addHeader(String,String)} 075 * <li class='jm'>{@link RestResponse#containsHeader(String) containsHeader(String)} 076 * <li class='jm'>{@link RestResponse#getHeader(String) getHeader(String)} 077 * <li class='jm'>{@link RestResponse#setCharacterEncoding(String) setCharacterEncoding(String)} 078 * <li class='jm'>{@link RestResponse#setContentType(String) setContentType(String)} 079 * <li class='jm'>{@link RestResponse#setHeader(HttpHeader) setHeader(HttpHeader)} 080 * <li class='jm'>{@link RestResponse#setHeader(HttpPartSchema,String,Object) setHeader(HttpPartSchema,String,Object)} 081 * <li class='jm'>{@link RestResponse#setHeader(String,Object) setHeader(String,Object)} 082 * <li class='jm'>{@link RestResponse#setHeader(String,String) setHeader(String,String)} 083 * <li class='jm'>{@link RestResponse#setMaxHeaderLength(int) setMaxHeaderLength(int)} 084 * <li class='jm'>{@link RestResponse#setSafeHeaders() setSafeHeaders()} 085 * <li class='jm'>{@link RestResponse#setUnsafeHeaders() setUnsafeHeaders()} 086 * <li class='jm'>{@link RestResponse#downloadAs(String) downloadAs(String)} 087 * <li class='jm'>{@link RestResponse#eTag(String) eTag(String)} 088 * <li class='jm'>{@link RestResponse#eTag(EntityTag) eTag(EntityTag)} 089 * <li class='jm'>{@link RestResponse#lastModified(Instant) lastModified(Instant)} 090 * <li class='jm'>{@link RestResponse#lastModified(ZonedDateTime) lastModified(ZonedDateTime)} 091 * <li class='jm'>{@link RestResponse#cacheControl(String) cacheControl(String)} 092 * <li class='jm'>{@link RestResponse#cacheControl(CacheControl.Builder) cacheControl(CacheControl.Builder)} 093 * </ul> 094 * <li>Methods for setting response bodies: 095 * <ul class='javatreec'> 096 * <li class='jm'>{@link RestResponse#flushBuffer() flushBuffer()} 097 * <li class='jm'>{@link RestResponse#getDirectWriter(String) getDirectWriter(String)} 098 * <li class='jm'>{@link RestResponse#getNegotiatedOutputStream() getNegotiatedOutputStream()} 099 * <li class='jm'>{@link RestResponse#getNegotiatedWriter() getNegotiatedWriter()} 100 * <li class='jm'>{@link RestResponse#getSerializerMatch() getSerializerMatch()} 101 * <li class='jm'>{@link RestResponse#getWriter() getWriter()} 102 * <li class='jm'>{@link RestResponse#sendPlainText(String) sendPlainText(String)} 103 * <li class='jm'>{@link RestResponse#sendRedirect(String) sendRedirect(String)} 104 * <li class='jm'>{@link RestResponse#sendSafeRedirect(String,String...) sendSafeRedirect(String,String...)} 105 * <li class='jm'>{@link RestResponse#setContentSchema(HttpPartSchema) setContentSchema(HttpPartSchema)} 106 * <li class='jm'>{@link RestResponse#setContent(Object) setContent(Object)} 107 * <li class='jm'>{@link RestResponse#setSerializer(Serializer) setSerializer(Serializer)} 108 * <li class='jm'>{@link RestResponse#setResponseBeanMeta(ResponseBeanMeta) setResponseBeanMeta(ResponseBeanMeta)} 109 * <li class='jm'>{@link RestResponse#setException(Throwable) setException(Throwable)} 110 * </ul> 111 * <li>Other: 112 * <ul class='javatreec'> 113 * <li class='jm'>{@link RestResponse#getAttributes() getAttributes()} 114 * <li class='jm'>{@link RestResponse#getContext() getContext()} 115 * <li class='jm'>{@link RestResponse#getOpContext() getOpContext()} 116 * <li class='jm'>{@link RestResponse#setAttribute(String,Object) setAttribute(String,Object)} 117 * <li class='jm'>{@link RestResponse#setNoTrace() setNoTrace()} 118 * <li class='jm'>{@link RestResponse#setStatus(int) setStatus(int)} 119 * </ul> 120 * </ul> 121 * </ul> 122 * 123 */ 124@SuppressWarnings({ 125 "java:S115", // Constants use UPPER_snakeCase convention (e.g., HEADER_ContentType) 126 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 127 "java:S2789", // The Optional content field is null until setContent() is called, and null (not Optional.empty()) means 'never set'; getRawOutput() null-checks it. 128 "java:S3776", // Mainly getNegotiatedOutputStream(), whose Accept-Encoding negotiation nests several conditions. 129 "resource" // HttpServletResponseWrapper is managed by servlet container 130}) 131public class RestResponse extends HttpServletResponseWrapper { 132 133 private static final String HEADER_ContentType = "Content-Type"; 134 private static final Pattern ENCODING_DISABLED_PATTERN = Pattern.compile("(identity|\\*)\\s*;\\s*q\\s*=\\s*(0(?!\\.)|0\\.0)"); 135 136 private final HttpServletResponse inner; 137 private final RestRequest request; 138 139 private Optional<Object> content; // The POJO being sent to the output. 140 private ServletOutputStream sos; 141 private FinishableServletOutputStream os; 142 private FinishablePrintWriter w; 143 private ResponseBeanMeta responseBeanMeta; 144 private final RestOpContext opContext; 145 private Optional<HttpPartSchema> contentSchema; 146 private Serializer serializer; 147 private Optional<SerializerMatch> serializerMatch; 148 private boolean safeHeaders = true; 149 private int maxHeaderLength = 8096; 150 151 /** 152 * Constructor. 153 */ 154 @SuppressWarnings({ 155 "java:S112" // throws Exception intentional - callback/lifecycle method 156 }) 157 RestResponse(RestOpContext opContext, RestSession session, RestRequest req) throws Exception { 158 super(session.getResponse()); 159 160 inner = session.getResponse(); 161 request = req; 162 163 this.opContext = opContext; 164 responseBeanMeta = opContext.getResponseMeta(); 165 166 var context = session.getContext(); 167 168 try { 169 var passThroughHeaders = request.getHeaderParam("x-response-headers").orElse(null); 170 if (nn(passThroughHeaders)) { 171 var m = context.getPartParser().getPartSession().read(HEADER, null, passThroughHeaders, MarshallingContext.DEFAULT.getClassMeta(JsonMap.class)); 172 for (var e : m.entrySet()) 173 addHeader(e.getKey(), resolveUris(e.getValue())); 174 } 175 } catch (Exception e1) { 176 throw new BadRequest(e1, "Invalid format for header 'x-response-headers'. Must be in URL-encoded format."); 177 } 178 179 // Find acceptable charset 180 var h = request.getHeaderParam("accept-charset").orElse(null); 181 Charset charset = null; 182 if (h == null) 183 charset = opContext.getDefaultCharset(); 184 else 185 for (var r : StringRanges.of(h).toList()) { 186 if (r.getQValue() > 0) { 187 if (eq(r.getName(), "*")) 188 charset = opContext.getDefaultCharset(); 189 else if (Charset.isSupported(r.getName())) 190 charset = Charset.forName(r.getName()); 191 if (nn(charset)) 192 break; 193 } 194 } 195 196 request.getContext().getDefaultResponseHeaders().forEach(x -> addHeader(x.getName(), resolveCspNonce(maybeResolveUris(x.getName(), x.getValue())))); // Done this way to avoid list/array copy. 197 198 opContext.getDefaultResponseHeaders().forEach(x -> addHeader(x.getName(), resolveCspNonce(maybeResolveUris(x.getName(), x.getValue())))); 199 200 if (charset == null) 201 throw new NotAcceptable("No supported charsets in header 'Accept-Charset': '%s'", request.getHeaderParam("Accept-Charset").orElse(null)); 202 inner.setCharacterEncoding(charset.name()); 203 204 } 205 206 /** 207 * Adds a response header. 208 * 209 * <p> 210 * Any previous header values are preserved. 211 * 212 * <p> 213 * Value is added at the end of the headers. 214 * 215 * <p> 216 * If the header is a {@link HttpUriHeader}, the URI will be resolved using the {@link RestRequest#getUriResolver()} object. 217 * 218 * <p> 219 * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, 220 * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. 221 * 222 * @param header The header. 223 * <br>Can be <jk>null</jk> (ignored). 224 * @return This object. 225 */ 226 public RestResponse addHeader(HttpHeader header) { 227 if (header == null) { 228 // Do nothing. 229 } else if (header instanceof HttpUriHeader header2) { 230 addHeader(header2.getName(), resolveUris(header2.getValue())); 231 } else { 232 addHeader(header.getName(), header.getValue()); 233 } 234 return this; 235 } 236 237 /** 238 * Adds a response header with the given name and value. 239 * 240 * <p> 241 * This method allows response headers to have multiple values. 242 * 243 * <p> 244 * A no-op of either the name or value is <jk>null</jk>. 245 * 246 * <p> 247 * Note that per <a class='doclink' href='https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2'>RFC2616</a>, 248 * only headers defined as comma-delimited lists [i.e., #(values)] should be defined as multiple message header fields. 249 * 250 * @param name The header name. 251 * @param value The header value. 252 */ 253 @Override 254 public void addHeader(String name, String value) { 255 if (nn(name) && nn(value)) { 256 if (eqic(name, HEADER_ContentType)) 257 setHeader(name, value); 258 else { 259 if (safeHeaders) 260 value = stripInvalidHttpHeaderChars(value); 261 value = abbreviate(value, maxHeaderLength); 262 inner.addHeader(name, value); 263 } 264 } 265 } 266 267 /** 268 * Forces any content in the buffer to be written to the client. 269 * 270 * <p> 271 * A call to this method automatically commits the response, meaning the status code and headers will be written. 272 * 273 * @throws IOException If an I/O error occurred. 274 */ 275 @Override 276 public void flushBuffer() throws IOException { 277 if (nn(w)) 278 w.flush(); 279 if (nn(os)) 280 os.flush(); 281 inner.flushBuffer(); 282 } 283 284 /** 285 * Shortcut for calling <c>getRequest().getAttributes()</c>. 286 * 287 * @return The request attributes object. 288 */ 289 public RequestAttributes getAttributes() { return request.getAttributes(); } 290 291 /** 292 * Wrapper around {@link #getCharacterEncoding()} that converts the value to a {@link Charset}. 293 * 294 * @return The request character encoding converted to a {@link Charset}, or <jk>null</jk> if no character encoding is set. 295 */ 296 public Charset getCharset() { 297 var s = getCharacterEncoding(); 298 return s == null ? null : Charset.forName(s); 299 } 300 301 /** 302 * Returns the output that was set by calling {@link #setContent(Object)}. 303 * 304 * <p> 305 * If it's null, then {@link #setContent(Object)} wasn't called. 306 * <br>If it contains an empty, then <c>setObject(<jk>null</jk>)</c> was called. 307 * <br>Otherwise, {@link #setContent(Object)} was called with a non-null value. 308 * 309 * @return The output object, or <jk>null</jk> if {@link #setContent(Object)} was never called. 310 */ 311 public Optional<Object> getContent() { return content; } 312 313 /** 314 * Returns this value cast to the specified class. 315 * 316 * @param <T> The class to cast to. 317 * @param c The class to cast to. 318 * <br>Must not be <jk>null</jk>. 319 * @return This value cast to the specified class, or <jk>null</jk> if the object doesn't exist or isn't the specified type. 320 */ 321 @SuppressWarnings({ 322 "unchecked" // Type erasure requires unchecked cast 323 }) 324 public <T> T getContent(Class<T> c) { 325 if (isContentOfType(c)) 326 return (T)getRawOutput(); 327 return null; 328 } 329 330 /** 331 * Returns the schema of the response content. 332 * 333 * @return The schema of the response content, never <jk>null</jk>. 334 */ 335 public Optional<HttpPartSchema> getContentSchema() { 336 if (nn(contentSchema)) 337 return contentSchema; 338 if (nn(responseBeanMeta)) 339 contentSchema = o(responseBeanMeta.getSchema()); 340 else { 341 var rbm = opContext.getResponseBeanMeta(getContent(Object.class)); 342 if (nn(rbm)) 343 contentSchema = o(rbm.getSchema()); 344 else 345 contentSchema = oe(); 346 } 347 return contentSchema; 348 } 349 350 /** 351 * Returns access to the inner {@link RestContext} of the class of this method. 352 * 353 * @return The {@link RestContext} of this class. Never <jk>null</jk>. 354 */ 355 public RestContext getContext() { return request.getContext(); } 356 357 /** 358 * Convenience method meant to be used when rendering directly to a browser with no buffering. 359 * 360 * <p> 361 * Sets the header <js>"x-content-type-options=nosniff"</js> so that output is rendered immediately on IE and Chrome 362 * without any buffering for content-type sniffing. 363 * 364 * <p> 365 * This can be useful if you want to render a streaming 'console' on a web page. 366 * 367 * @param contentType The value to set as the <c>Content-Type</c> on the response. 368 * @return The raw writer. 369 * @throws IOException Thrown by underlying stream. 370 */ 371 public PrintWriter getDirectWriter(String contentType) throws IOException { 372 setContentType(contentType); 373 setHeader("X-Content-Type-Options", "nosniff"); 374 setHeader("Content-Encoding", "identity"); 375 return getWriter(true, true); 376 } 377 378 /** 379 * Returns the wrapped servlet response. 380 * 381 * @return The wrapped servlet response. 382 */ 383 public HttpServletResponse getHttpServletResponse() { return inner; } 384 385 /** 386 * Returns the <c>Content-Type</c> header stripped of the charset attribute if present. 387 * 388 * @return The <c>media-type</c> portion of the <c>Content-Type</c> header. 389 */ 390 public MediaType getMediaType() { return MediaType.of(getContentType()); } 391 392 /** 393 * Equivalent to {@link HttpServletResponse#getOutputStream()}, except wraps the output stream if an {@link Encoder} 394 * was found that matched the <c>Accept-Encoding</c> header. 395 * 396 * @return A negotiated output stream. 397 * @throws NotAcceptable If unsupported Accept-Encoding value specified. 398 * @throws IOException Thrown by underlying stream. 399 */ 400 public FinishableServletOutputStream getNegotiatedOutputStream() throws NotAcceptable, IOException { 401 if (os == null) { 402 Encoder encoder = null; 403 var encoders = request.getOpContext().getEncoders(); 404 405 var ae = request.getHeaderParam("Accept-Encoding").orElse(null); 406 if (ine(ae)) { 407 var match = encoders.getEncoderMatch(ae); 408 if (match == null) { 409 // Identity should always match unless "identity;q=0" or "*;q=0" is specified. 410 if (ENCODING_DISABLED_PATTERN.matcher(ae).find()) { 411 throw new NotAcceptable("Unsupported encoding in request header 'Accept-Encoding': '%s'\n\tSupported codings: %s", ae, Json5.of(encoders.getSupportedEncodings())); 412 } 413 } else { 414 encoder = match.getEncoder(); 415 var encoding = match.getEncoding(); 416 417 // Some clients don't recognize identity as an encoding, so don't set it. 418 if (neq(encoding, "identity")) 419 setHeader("content-encoding", encoding); 420 } 421 } 422 var sos2 = getOutputStream(); 423 os = new FinishableServletOutputStream(encoder == null ? sos2 : encoder.getOutputStream(sos2)); 424 } 425 return os; 426 } 427 428 /** 429 * Equivalent to {@link HttpServletResponse#getWriter()}, except wraps the output stream if an {@link Encoder} was 430 * found that matched the <c>Accept-Encoding</c> header and sets the <c>Content-Encoding</c> 431 * header to the appropriate value. 432 * 433 * @return The negotiated writer. 434 * @throws NotAcceptable If unsupported charset in request header Accept-Charset. 435 * @throws IOException Thrown by underlying stream. 436 */ 437 public FinishablePrintWriter getNegotiatedWriter() throws NotAcceptable, IOException { return getWriter(false, false); } 438 439 /** 440 * Returns access to the inner {@link RestOpContext} of this method. 441 * 442 * @return The {@link RestOpContext} of this method. Never <jk>null</jk>. 443 */ 444 public RestOpContext getOpContext() { return request.getOpContext(); } 445 446 /** 447 * Returns a ServletOutputStream suitable for writing binary data in the response. 448 * 449 * <p> 450 * The servlet container does not encode the binary data. 451 * 452 * <p> 453 * Calling <c>flush()</c> on the ServletOutputStream commits the response. 454 * Either this method or <c>getWriter</c> may be called to write the content, not both, except when reset has been called. 455 * 456 * @return The stream. 457 * @throws IOException If stream could not be accessed. 458 */ 459 @Override 460 public ServletOutputStream getOutputStream() throws IOException { 461 if (sos == null) 462 sos = inner.getOutputStream(); 463 return sos; 464 } 465 466 /** 467 * Returns <jk>true</jk> if {@link #getOutputStream()} has been called. 468 * 469 * @return <jk>true</jk> if {@link #getOutputStream()} has been called. 470 */ 471 public boolean getOutputStreamCalled() { return nn(sos); } 472 473 /** 474 * Returns the metadata about this response. 475 * 476 * @return 477 * The metadata about this response. 478 * <br>Can be <jk>null</jk> if the response method has no {@link Response @Response} metadata (or was explicitly cleared). 479 */ 480 public ResponseBeanMeta getResponseBeanMeta() { return responseBeanMeta; } 481 482 /** 483 * Returns the matching serializer and media type for this response. 484 * 485 * @return The matching serializer, never <jk>null</jk>. 486 */ 487 public Optional<SerializerMatch> getSerializerMatch() { 488 if (nn(serializerMatch)) 489 return serializerMatch; 490 if (nn(serializer)) { 491 serializerMatch = o(new SerializerMatch(getMediaType(), serializer)); 492 } else { 493 serializerMatch = opContext.getSerializers().getSerializerMatch(request.getHeaderParam("Accept").orElse("*/*")); 494 } 495 return serializerMatch; 496 } 497 498 /** 499 * Returns the writer to the response content. 500 * 501 * <p> 502 * This methods bypasses any specified encoders and returns a regular unbuffered writer. 503 * Use the {@link #getNegotiatedWriter()} method if you want to use the matched encoder (if any). 504 * 505 * @return The writer. 506 * @throws IOException If writer could not be accessed. 507 */ 508 @Override 509 public PrintWriter getWriter() throws IOException { return getWriter(true, false); } 510 511 /** 512 * Returns <jk>true</jk> if the response contains output. 513 * 514 * <p> 515 * This implies {@link #setContent(Object)} has been called on this object. 516 * 517 * <p> 518 * Note that this also returns <jk>true</jk> even if {@link #setContent(Object)} was called with a <jk>null</jk> 519 * value as this means the response contains an output value of <jk>null</jk> as opposed to no value at all. 520 * 521 * @return <jk>true</jk> if the response contains output. 522 */ 523 public boolean hasContent() { 524 return nn(content); 525 } 526 527 /** 528 * Returns <jk>true</jk> if this response object is of the specified type. 529 * 530 * @param c The type to check against. 531 * <br>Must not be <jk>null</jk>. 532 * @return <jk>true</jk> if this response object is of the specified type. 533 */ 534 public boolean isContentOfType(Class<?> c) { 535 return c.isInstance(getRawOutput()); 536 } 537 538 /** 539 * Sets the output to a plain-text message regardless of the content type. 540 * 541 * @param text The output text to send. 542 * <br>Must not be <jk>null</jk>. 543 * @return This object. 544 * @throws IOException If a problem occurred trying to write to the writer. 545 */ 546 public RestResponse sendPlainText(String text) throws IOException { 547 setContentType("text/plain"); 548 getNegotiatedWriter().write(text); 549 return this; 550 } 551 552 /** 553 * Creates a fluent Server-Sent Events helper for this response. 554 * 555 * @return The SSE response helper. 556 * @throws IOException If the writer could not be created. 557 */ 558 public SseResponseSupport sse() throws IOException { 559 return new SseResponseSupport(this); 560 } 561 562 /** 563 * Redirects to the specified URI. 564 * 565 * <p> 566 * Relative URIs are always interpreted as relative to the context root. 567 * This is similar to how WAS handles redirect requests, and is different from how Tomcat handles redirect requests. 568 * 569 * <p> 570 * This method forwards <c>uri</c> to the container as-is. If the target is built from request input (e.g. 571 * a query parameter or header value), a caller can steer users to an arbitrary external site; use 572 * {@link #sendSafeRedirect(String,String...) sendSafeRedirect(String,String...)} instead in that case, which 573 * validates the target is either relative or points at an allow-listed host. 574 * 575 * @param uri The redirection URL. 576 * @throws IOException If an input or output exception occurs 577 */ 578 @Override 579 public void sendRedirect(String uri) throws IOException { 580 var c = (uri.isEmpty() ? 0 : uri.charAt(0)); 581 if (c != '/' && uri.indexOf("://") == -1) 582 uri = request.getContextPath() + '/' + uri; 583 inner.sendRedirect(uri); 584 } 585 586 /** 587 * Redirects to the specified URI, but only if it's a relative path (interpreted per {@link #sendRedirect(String) 588 * sendRedirect(String)}) or an absolute/protocol-relative URI whose host matches one of <c>allowedHosts</c>. 589 * 590 * <p> 591 * Intended for use when the redirect target is derived from request input rather than a fixed, trusted 592 * literal, so that input can't be used to redirect a user to an arbitrary external site. 593 * 594 * @param uri The redirection URL. 595 * @param allowedHosts The hosts (case-insensitive) an absolute or protocol-relative <c>uri</c> is allowed to target. Ignored for relative URIs. 596 * @throws IOException If an input or output exception occurs. 597 * @throws BadRequest If <c>uri</c> is absolute or protocol-relative and its host isn't in <c>allowedHosts</c>. 598 */ 599 public void sendSafeRedirect(String uri, String...allowedHosts) throws IOException { 600 if (! isSafeRedirectUri(uri, allowedHosts)) 601 throw new BadRequest("Redirect target '%s' is not a relative path or an allowed host.", uri); 602 sendRedirect(uri); 603 } 604 605 /** 606 * Returns <jk>true</jk> if <c>uri</c> is safe to pass to {@link #sendRedirect(String) sendRedirect(String)} 607 * without risk of redirecting to an unintended external site: either a relative path, or an absolute/ 608 * protocol-relative URI whose host is in <c>allowedHosts</c>. 609 * 610 * @param uri The candidate redirection URL. 611 * @param allowedHosts The hosts (case-insensitive) an absolute or protocol-relative <c>uri</c> is allowed to target. 612 * @return <jk>true</jk> if the URI is safe to redirect to. 613 */ 614 static boolean isSafeRedirectUri(String uri, String...allowedHosts) { 615 if (ie(uri)) 616 return true; 617 if (uri.charAt(0) == '/' && ! uri.startsWith("//")) 618 return true; 619 if (uri.indexOf("://") == -1 && ! uri.startsWith("//")) 620 return true; 621 String host; 622 try { 623 host = new java.net.URI(uri.startsWith("//") ? "http:" + uri : uri).getHost(); 624 } catch (java.net.URISyntaxException e) { 625 return false; 626 } 627 if (host == null || allowedHosts == null) 628 return false; 629 for (var h : allowedHosts) 630 if (eqic(host, h)) 631 return true; 632 return false; 633 } 634 635 /** 636 * Shortcut for calling <c>getRequest().setAttribute(String,Object)</c>. 637 * 638 * @param name The property name. 639 * @param value The property value. 640 * @return This object. 641 */ 642 public RestResponse setAttribute(String name, Object value) { 643 request.setAttribute(name, value); 644 return this; 645 } 646 647 /** 648 * Sets the HTTP output on the response. 649 * 650 * <p> 651 * The object type can be anything allowed by the registered response handlers. 652 * 653 * <p> 654 * Calling this method is functionally equivalent to returning the object in the REST Java method. 655 * 656 * <h5 class='section'>Example:</h5> 657 * <p class='bjava'> 658 * <ja>@RestGet</ja>(<js>"/example2/{personId}"</js>) 659 * <jk>public void</jk> doGet(RestResponse <jv>res</jv>, <ja>@Path</ja> UUID <jv>personId</jv>) { 660 * Person <jv>person</jv> = getPersonById(<jv>personId</jv>); 661 * <jv>res</jv>.setContent(<jv>person</jv>); 662 * } 663 * </p> 664 * 665 * <h5 class='section'>Notes:</h5><ul> 666 * <li class='note'> 667 * Calling this method with a <jk>null</jk> value is NOT the same as not calling this method at all. 668 * <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>). 669 * <br>Not calling this method or returning a value means you're handing the response yourself via the underlying stream or writer. 670 * </ul> 671 * 672 * <h5 class='section'>See Also:</h5><ul> 673 * <li class='ja'>{@link Rest#responseProcessors()} 674 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestOpAnnotatedMethods">@RestOp-Annotated Method</a> 675 * </ul> 676 * 677 * @param output The output to serialize to the connection. 678 * @return This object. 679 */ 680 public RestResponse setContent(Object output) { 681 this.content = o(output); 682 return this; 683 } 684 685 /** 686 * Specifies the schema for the response content. 687 * 688 * <p> 689 * Used by schema-aware serializers such as {@link OpenApiSerializer}. Ignored by other serializers. 690 * 691 * @param schema The content schema 692 * <br>Can be <jk>null</jk> (clears any previously-set schema). 693 * @return This object. 694 */ 695 public RestResponse setContentSchema(HttpPartSchema schema) { 696 this.contentSchema = o(schema); 697 return this; 698 } 699 700 /** 701 * Forces the {@link Serializer} used to serialize the response body set via {@link #setContent(Object)} (or an 702 * equivalent return from the REST method), instead of choosing one from the <c>Accept</c> header. 703 * 704 * <p> 705 * {@link org.apache.juneau.rest.server.processor.SerializedPojoProcessor} uses {@link #getSerializerMatch()}, which 706 * returns a {@link SerializerMatch} built from this serializer when non-<jk>null</jk>. 707 * 708 * <p> 709 * <c>Content-Type</c> is still applied by the response processor: it prefers {@link #getMediaType()} if set, then 710 * the match media type, then {@link Serializer#getResponseContentType()}. Call {@link #setContentType(String)} (or 711 * another header setter) if you need a specific type before serialization runs. 712 * 713 * @param value The serializer to use. Can be <jk>null</jk> to clear the override and use normal <c>Accept</c> 714 * negotiation on the next {@link #getSerializerMatch()} call. 715 * @return This object. 716 */ 717 public RestResponse setSerializer(Serializer value) { 718 serializer = value; 719 serializerMatch = null; 720 return this; 721 } 722 723 /** 724 * Returns the captured (bounded) response body bytes for debug logging. 725 * 726 * <p> 727 * Populated by the two-phase debug pipeline only when the resolved logger is loggable at 728 * {@link java.util.logging.Level#FINEST FINEST}; otherwise returns <jk>null</jk>. The captured copy is capped at the 729 * formatter's body cap (default 8 KB); use {@link #getCachedContentLength()} for the full byte count. 730 * 731 * @return The captured response body bytes, or <jk>null</jk> if body caching was not installed. 732 */ 733 public byte[] getCachedContent() { 734 return inner instanceof CachingHttpServletResponse c ? c.getContent() : null; 735 } 736 737 /** 738 * Returns the total number of response body bytes written, including any beyond the capture cap. 739 * 740 * @return The total response body length in bytes, or {@code -1} if body caching was not installed. 741 */ 742 public long getCachedContentLength() { 743 return inner instanceof CachingHttpServletResponse c ? c.getTotalLength() : -1; 744 } 745 746 /** 747 * Sets the <js>"Exception"</js> attribute to the specified throwable. 748 * 749 * <p> 750 * This exception is surfaced by the debug pipeline as the {@code thrown} of the emitted log record. 751 * 752 * @param t The attribute value. 753 * @return This object. 754 */ 755 public RestResponse setException(Throwable t) { 756 request.setException(t); 757 return this; 758 } 759 760 /** 761 * Sets a response header. 762 * 763 * <p> 764 * Any previous header values are removed. 765 * 766 * <p> 767 * Value is added at the end of the headers. 768 * 769 * @param header The header. 770 * <br>Can be <jk>null</jk> (ignored). 771 * @return This object. 772 */ 773 public RestResponse setHeader(HttpHeader header) { 774 if (header == null) { 775 // Do nothing. 776 } else if (header instanceof HttpUriHeader header2) { 777 setHeader(header2.getName(), resolveUris(header2.getValue())); 778 } else { 779 var v = header.getValue(); 780 if (nn(v) && v.indexOf("://") != -1) 781 v = resolveUris(v); 782 setHeader(header.getName(), v); 783 } 784 return this; 785 } 786 787 /** 788 * Sets <l>Content-Disposition</l> to <js>attachment</js> so browsers typically download the body, using the given 789 * suggested file name (quoted {@code filename} parameter). 790 * 791 * <p> 792 * Shorthand for {@link #setHeader(HttpHeader) setHeader}({@link ContentDisposition#attachment(String) 793 * ContentDisposition.attachment(filename)}). 794 * </p> 795 * 796 * <h5 class='figure'>Example</h5> 797 * <p class='bjava'> 798 * <jv>res</jv>.downloadAs(<js>"example.pdf"</js>).setContent(<jv>pdfBytes</jv>); 799 * </p> 800 * 801 * @param filename Suggested download name. Must not be <jk>null</jk>, blank, or contain CR/LF. 802 * @return This object. 803 * @throws IllegalArgumentException If {@code filename} is invalid. 804 * @see ContentDisposition#attachment(String) 805 */ 806 public RestResponse downloadAs(String filename) { 807 return setHeader(ContentDisposition.attachment(filename)); 808 } 809 810 /** 811 * Sets the <c>ETag</c> response header to the supplied wire value. 812 * 813 * <p> 814 * The value is validated through {@link EntityTag#of(Object)} — strong tags must be quoted 815 * (<c>"v1"</c>), weak tags must be prefixed with <c>W/</c> (<c>W/"v1"</c>), and the wildcard 816 * <c>*</c> is accepted. The header is serialized in canonical form (re-quoted for strong tags, 817 * <c>W/</c>-prefixed for weak tags). 818 * 819 * <h5 class='section'>Example:</h5> 820 * <p class='bjava'> 821 * <jv>res</jv>.eTag(<js>"\"v42\""</js>); <jc>// ETag: "v42"</jc> 822 * <jv>res</jv>.eTag(<js>"W/\"v42\""</js>); <jc>// ETag: W/"v42"</jc> 823 * </p> 824 * 825 * @param value The entity-tag wire value. Must not be <jk>null</jk>; must match the 826 * {@code entity-tag} grammar per RFC 7232 §2.3. 827 * @return This object. 828 * @throws IllegalArgumentException If {@code value} is malformed. 829 * @see RestRequest#checkPreconditions(RestResponse) 830 */ 831 public RestResponse eTag(String value) { 832 return eTag(EntityTag.of(value)); 833 } 834 835 /** 836 * Sets the <c>ETag</c> response header to the canonical wire form of the supplied {@link EntityTag}. 837 * 838 * @param value The entity tag. Must not be <jk>null</jk>. 839 * @return This object. 840 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>. 841 */ 842 public RestResponse eTag(EntityTag value) { 843 reqnn("value", value); 844 return setHeader(ETag.of(value.toString())); 845 } 846 847 /** 848 * Sets the <c>Last-Modified</c> response header to the supplied instant formatted as an 849 * <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1'>RFC 7231 IMF-fixdate</a>. 850 * 851 * <p> 852 * The instant is rendered in GMT regardless of the JVM's default timezone. The fractional-second 853 * portion is truncated, matching the per-spec one-second resolution of HTTP-date timestamps. 854 * 855 * <h5 class='section'>Example:</h5> 856 * <p class='bjava'> 857 * <jv>res</jv>.lastModified(Instant.<jsm>parse</jsm>(<js>"2026-05-22T00:00:00Z"</js>)); 858 * <jc>// Last-Modified: Fri, 22 May 2026 00:00:00 GMT</jc> 859 * </p> 860 * 861 * @param value The last-modified timestamp. Must not be <jk>null</jk>. 862 * @return This object. 863 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>. 864 * @see RestRequest#checkPreconditions(RestResponse) 865 */ 866 public RestResponse lastModified(Instant value) { 867 reqnn("value", value); 868 return lastModified(value.atZone(ZoneOffset.UTC)); 869 } 870 871 /** 872 * Sets the <c>Last-Modified</c> response header to the supplied date-time formatted as an 873 * <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1'>RFC 7231 IMF-fixdate</a>. 874 * 875 * <p> 876 * The supplied date-time is converted to GMT and truncated to whole seconds before formatting. 877 * 878 * @param value The last-modified timestamp. Must not be <jk>null</jk>. 879 * @return This object. 880 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>. 881 */ 882 public RestResponse lastModified(ZonedDateTime value) { 883 reqnn("value", value); 884 var z = value.withZoneSameInstant(ZoneOffset.UTC).truncatedTo(SECONDS); 885 return setHeader(LastModified.of(RFC_1123_DATE_TIME.format(z))); 886 } 887 888 /** 889 * Sets the <c>Cache-Control</c> response header to the supplied directive string. 890 * 891 * <p> 892 * The value is written verbatim — the caller is responsible for supplying a comma-separated 893 * directive list per <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc9111#name-cache-control'>RFC 894 * 9111 §5.2</a>. 895 * 896 * @param value The directive list (e.g. <js>"public, max-age=3600"</js>). Must not be <jk>null</jk>. 897 * @return This object. 898 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>. 899 * @see #cacheControl(CacheControl.Builder) 900 */ 901 public RestResponse cacheControl(String value) { 902 reqnn("value", value); 903 return setHeader(CacheControl.of(value)); 904 } 905 906 /** 907 * Sets the <c>Cache-Control</c> response header from a {@link org.apache.juneau.http.header.CacheControl.Builder CacheControl.Builder}. 908 * 909 * <h5 class='section'>Example:</h5> 910 * <p class='bjava'> 911 * <jv>res</jv>.cacheControl(CacheControl.<jsm>create</jsm>().publicCache().maxAge(3600)); 912 * <jc>// Cache-Control: public, max-age=3600</jc> 913 * </p> 914 * 915 * @param value The builder. Must not be <jk>null</jk>. 916 * @return This object. 917 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>. 918 */ 919 public RestResponse cacheControl(CacheControl.Builder value) { 920 reqnn("value", value); 921 return cacheControl(value.build()); 922 } 923 924 /** 925 * Sets a header on the request. 926 * 927 * @param schema 928 * The schema to use to serialize the header, or <jk>null</jk> to use the default schema. 929 * @param name The header name. 930 * @param value The header value. 931 * <ul> 932 * <li>Can be any POJO. 933 * <li>Converted to a string using the specified part serializer. 934 * </ul> 935 * @return This object. 936 * @throws SchemaValidationException Header failed schema validation. 937 * @throws SerializeException Header could not be serialized. 938 */ 939 public RestResponse setHeader(HttpPartSchema schema, String name, Object value) throws SchemaValidationException, SerializeException { 940 setHeader(name, request.getPartSerializerSession().write(HEADER, schema, value)); 941 return this; 942 } 943 944 /** 945 * Sets a header on the request. 946 * 947 * @param name The header name. 948 * @param value The header value. 949 * <ul> 950 * <li>Can be any POJO. 951 * <li>Converted to a string using the specified part serializer. 952 * </ul> 953 * @return This object. 954 * @throws SchemaValidationException Header failed schema validation. 955 * @throws SerializeException Header could not be serialized. 956 */ 957 public RestResponse setHeader(String name, Object value) throws SchemaValidationException, SerializeException { 958 setHeader(name, request.getPartSerializerSession().write(HEADER, null, value)); 959 return this; 960 } 961 962 /** 963 * Sets a response header with the given name and value. 964 * 965 * <p> 966 * If the header had already been set, the new value overwrites the previous one. 967 * 968 * <p> 969 * The {@link #containsHeader(String)} method can be used to test for the presence of a header before setting its value. 970 * 971 * @param name The header name. 972 * @param value The header value. 973 */ 974 @Override 975 public void setHeader(String name, String value) { 976 977 // Jetty doesn't set the content type correctly if set through this method. 978 // Tomcat/WAS does. 979 if (eqic(name, HEADER_ContentType)) { 980 inner.setContentType(value); 981 ContentType ct = ContentType.of(value); 982 if (nn(ct) && nn(ct.getParameter("charset"))) 983 inner.setCharacterEncoding(ct.getParameter("charset")); 984 } else { 985 if (safeHeaders) 986 value = stripInvalidHttpHeaderChars(value); 987 value = abbreviate(value, maxHeaderLength); 988 inner.setHeader(name, value); 989 } 990 } 991 992 /** 993 * Specifies the maximum length for header values. 994 * 995 * <p> 996 * Header values that exceed this length will get truncated. 997 * 998 * @param value The new value for this setting. The default is <c>8096</c>. 999 * @return This object. 1000 */ 1001 public RestResponse setMaxHeaderLength(int value) { 1002 maxHeaderLength = value; 1003 return this; 1004 } 1005 1006 /** 1007 * Shortcut for calling <c>setNoTrace(<jk>true</jk>)</c>. 1008 * 1009 * @return This object. 1010 */ 1011 public RestResponse setNoTrace() { 1012 return setNoTrace(true); 1013 } 1014 1015 /** 1016 * Sets the <js>"NoTrace"</js> attribute to the specified boolean. 1017 * 1018 * <p> 1019 * This flag tells the framework not to trace the current request: the access-log record for the call is still 1020 * emitted, but without the exception's stack trace. A <c>No-Trace: true</c> request header has the same effect; when both are present, the attribute wins. 1021 * 1022 * @param b The attribute value. 1023 * @return This object. 1024 */ 1025 public RestResponse setNoTrace(Boolean b) { 1026 request.setNoTrace(b); 1027 return this; 1028 } 1029 1030 /** 1031 * Sets metadata about this response. 1032 * 1033 * @param rbm The metadata about this response. 1034 * <br>Can be <jk>null</jk> (clears the metadata). 1035 * @return This object. 1036 */ 1037 public RestResponse setResponseBeanMeta(ResponseBeanMeta rbm) { 1038 this.responseBeanMeta = rbm; 1039 return this; 1040 } 1041 1042 /** 1043 * Enables safe-header mode. 1044 * 1045 * <p> 1046 * When enabled (the default), invalid characters such as CTRL characters (including a raw CR or LF that 1047 * could otherwise be used to inject an additional header or split the response) are stripped from header 1048 * values via {@link org.apache.juneau.commons.utils.StringUtils#stripInvalidHttpHeaderChars(String) 1049 * StringUtils.stripInvalidHttpHeaderChars(String)} before they get set. This is a no-op call retained for 1050 * apps that called it explicitly before this became the default; see {@link #setUnsafeHeaders()} to opt 1051 * back out. 1052 * 1053 * @return This object. 1054 */ 1055 public RestResponse setSafeHeaders() { 1056 this.safeHeaders = true; 1057 return this; 1058 } 1059 1060 /** 1061 * Disables safe-header mode, restoring the pre-hardening behavior of passing header values through to the 1062 * underlying {@link HttpServletResponse} verbatim (aside from the usual {@link #setMaxHeaderLength(int)} 1063 * truncation). 1064 * 1065 * <p> 1066 * Only call this for a header value the application has already validated itself; most servlet containers 1067 * reject a raw CR/LF in a header value outright regardless of this setting, but other control characters may 1068 * still pass through once safe-header mode is off. 1069 * 1070 * @return This object. 1071 */ 1072 public RestResponse setUnsafeHeaders() { 1073 this.safeHeaders = false; 1074 return this; 1075 } 1076 1077 private Object getRawOutput() { return content == null ? null : content.orElse(null); } 1078 1079 private FinishablePrintWriter getWriter(boolean raw, boolean autoflush) throws NotAcceptable, IOException { 1080 if (nn(w)) 1081 return w; 1082 1083 // If plain text requested, override it now. 1084 if (request.isPlainText()) 1085 setHeader(HEADER_ContentType, "text/plain"); 1086 1087 try { 1088 OutputStream out = (raw ? getOutputStream() : getNegotiatedOutputStream()); 1089 w = new FinishablePrintWriter(out, getCharacterEncoding(), autoflush); 1090 return w; 1091 } catch (@SuppressWarnings("unused") UnsupportedEncodingException e) { 1092 var ce = getCharacterEncoding(); 1093 setCharacterEncoding("UTF-8"); 1094 throw new NotAcceptable("Unsupported charset in request header 'Accept-Charset': '%s'", ce); 1095 } 1096 } 1097 1098 private String resolveUris(Object value) { 1099 var s = s(value); 1100 return request.getUriResolver().resolve(s); 1101 } 1102 1103 private String maybeResolveUris(String name, Object value) { 1104 return isUriBearingHeader(name) ? resolveUris(value) : s(value); 1105 } 1106 1107 /** 1108 * If a (Content-Security-Policy) header value contains a nonce placeholder, mints a single per-response nonce 1109 * (cached as a request attribute so the serializer stamps the SAME value onto inline {@code <script>}/{@code <style>} 1110 * tags) and substitutes it into the value. Returns the value unchanged when no placeholder is present, so this is 1111 * a no-op for every non-CSP header and for CSP headers that don't use a nonce. 1112 * 1113 * @param value The already-URI-resolved header value. May be <jk>null</jk>. 1114 * @return The header value with any nonce placeholder resolved. 1115 */ 1116 private String resolveCspNonce(String value) { 1117 if (value == null || ! value.contains(ContentSecurityPolicy.Builder.NONCE_PLACEHOLDER)) 1118 return value; 1119 var nonce = request.getAttribute(RestRequest.CSP_NONCE_ATTR).as(String.class).orElse(null); 1120 if (nonce == null) { 1121 nonce = ContentSecurityPolicy.Builder.generateNonce(); 1122 request.setAttribute(RestRequest.CSP_NONCE_ATTR, nonce); 1123 } 1124 return ContentSecurityPolicy.Builder.resolveNonce(value, nonce); 1125 } 1126 1127 private static boolean isUriBearingHeader(String name) { 1128 if (name == null) 1129 return false; 1130 return eqic(name, "Location") 1131 || eqic(name, "Content-Location") 1132 || eqic(name, "Referer"); 1133 } 1134}