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