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&nbsp;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}