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.http.response;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.lang.reflect.*;
022import java.util.*;
023
024import org.apache.juneau.http.*;
025import org.apache.juneau.http.entity.*;
026import org.apache.juneau.http.header.*;
027
028/**
029 * Base class for HTTP error response exceptions (4xx and 5xx) in the {@code org.apache.juneau.http} stack.
030 *
031 * <p>
032 * Extends {@link RuntimeException} while also implementing {@link HttpResponseMessage}, so that HTTP
033 * error responses can be both thrown as exceptions and used as response objects. Provides classic-style
034 * fluent setters that mutate state and return {@code this}.
035 *
036 * <p>
037 * Immutability is expressed with the "funnel + nested {@code Unmodifiable} snapshot" paradigm: every mutator routes
038 * through the single protected {@link #modify(Runnable)} choke-point, and each concrete leaf gets a nested
039 * {@code X.Unmodifiable} whose only behavioral override is a throwing {@code modify(...)}.  A leaf's
040 * {@link #unmodifiable()} factory returns a point-in-time snapshot; any attempt to set a property on it throws an
041 * {@link UnsupportedOperationException}.
042 *
043 * <p>
044 * This is a {@link Throwable}-rooted hierarchy, so (unlike {@link BasicHttpResponse}) it cannot be a self-typed (CRTP)
045 * root — JLS &sect;8.1.2 forbids a generic {@code Throwable}.  The funnel therefore returns the base type and each leaf
046 * keeps its covariant setter overrides.
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
050 * </ul>
051 *
052 * @since 9.2.1
053 */
054@SuppressWarnings({
055   "java:S1165", // Mutable exception fields are intentional: HTTP exceptions support builder-style fluent setters.
056   "java:S1948", // HttpHeaderList is not Java-serializable; HTTP exceptions are not designed for Java serialization transport.
057   "java:S3008" // Concrete subclasses use UPPER_CASE singleton instance constants (e.g., INSTANCE fields).
058})
059public class BasicHttpException extends RuntimeException implements HttpResponseMessage {
060
061   private static final long serialVersionUID = 1L;
062
063   private HttpStatusLine statusLine;
064   private HttpHeaderList headers;
065   private HttpBody body;
066   private Locale locale;
067
068   /**
069    * Constructor with no message body.
070    *
071    * @param statusCode The HTTP status code.
072    * @param reasonPhrase The reason phrase (e.g. {@code "Internal Server Error"}). May be <jk>null</jk>.
073    */
074   public BasicHttpException(int statusCode, String reasonPhrase) {
075      this(statusCode, reasonPhrase, (Throwable)null, (String)null);
076   }
077
078   /**
079    * Constructor with a status code and a cause.
080    *
081    * @param statusCode The HTTP status code.
082    * @param reasonPhrase The reason phrase. May be <jk>null</jk>.
083    * @param cause The cause. May be <jk>null</jk>.
084    */
085   public BasicHttpException(int statusCode, String reasonPhrase, Throwable cause) {
086      this(statusCode, reasonPhrase, cause, cause != null ? cause.getMessage() : null);
087   }
088
089   /**
090    * Constructor with a {@link String#format(String, Object...) String.format}-style message body.
091    *
092    * @param statusCode The HTTP status code.
093    * @param reasonPhrase The reason phrase. May be <jk>null</jk>.
094    * @param msg The exception message and response body. May be <jk>null</jk>.
095    *    Treated as a {@link String#format(String, Object...) String.format}-style format string (<c>%s</c> placeholders) when {@code args} is non-empty.
096    * @param args Optional message arguments.
097    */
098   public BasicHttpException(int statusCode, String reasonPhrase, String msg, Object...args) {
099      this(statusCode, reasonPhrase, (Throwable)null, msg, args);
100   }
101
102   /**
103    * Constructor with a cause and a {@link String#format(String, Object...) String.format}-style message body.
104    *
105    * @param statusCode The HTTP status code.
106    * @param reasonPhrase The reason phrase. May be <jk>null</jk>.
107    * @param cause The cause. May be <jk>null</jk>.
108    * @param msg The exception message. May be <jk>null</jk>.
109    *    Treated as a {@link String#format(String, Object...) String.format}-style format string (<c>%s</c> placeholders) when {@code args} is non-empty.
110    * @param args Optional message arguments.
111    */
112   public BasicHttpException(int statusCode, String reasonPhrase, Throwable cause, String msg, Object...args) {
113      super(formatMessage(msg, args), cause);
114      this.statusLine = HttpStatusLineBean.of(statusCode, reasonPhrase);
115      this.headers = HttpHeaderList.create();
116      var m = formatMessage(msg, args);
117      this.body = m != null ? StringBody.of(m) : null;
118   }
119
120   private static String formatMessage(String msg, Object...args) {
121      if (msg == null)
122         return null;
123      if (args == null || args.length == 0)
124         return msg;
125      return f(msg, args);
126   }
127
128   /**
129    * Copy constructor.
130    *
131    * <p>
132    * The mutable sub-beans are deep-copied by <b>direct field assignment</b>, never through a funneled setter.  This is
133    * the snapshot path used by {@link #unmodifiable()}: the constructor runs inside an {@code Unmodifiable} instance
134    * whose {@link #modify(Runnable)} already throws, so a setter-based copy would blow up during construction.
135    *
136    * @param copyFrom The instance to copy. Must not be <jk>null</jk>.
137    */
138   protected BasicHttpException(BasicHttpException copyFrom) {
139      super(reqnn("copyFrom", copyFrom).getMessage(), copyFrom.getCause());
140      this.statusLine = copyFrom.statusLine;
141      this.headers = copyFrom.headers.copy();
142      this.body = copyFrom.body;
143      this.locale = copyFrom.locale;
144   }
145
146   // ------------------------------------------------------------------------------------------------------------------
147   // Read accessors
148   // ------------------------------------------------------------------------------------------------------------------
149
150   @Override /* HttpResponseMessage */
151   public HttpStatusLine getStatusLine() {
152      return statusLine;
153   }
154
155   @Override /* HttpResponseMessage */
156   public List<HttpHeader> getHeaders() {
157      return u(headers);
158   }
159
160   @Override /* HttpResponseMessage */
161   public HttpBody getBody() {
162      return body;
163   }
164
165   @Override /* Throwable */
166   public String getMessage() {
167      var m = super.getMessage();
168      if (m == null && getCause() != null)
169         m = getCause().getMessage();
170      if (m == null)
171         m = statusLine.getReasonPhrase();
172      return m;
173   }
174
175   /**
176    * Returns the HTTP status code.
177    *
178    * @return The status code.
179    */
180   public int getStatusCode() {
181      return statusLine.getStatusCode();
182   }
183
184   /**
185    * Returns the locale associated with this response, if any.
186    *
187    * @return The locale, possibly <jk>null</jk>.
188    */
189   public Locale getLocale() {
190      return locale;
191   }
192
193   // ------------------------------------------------------------------------------------------------------------------
194   // Fluent setters (mutate this instance)
195   // ------------------------------------------------------------------------------------------------------------------
196
197   /**
198    * Sets the status line.
199    *
200    * @param value New status line. Must not be <jk>null</jk>.
201    * @return This object.
202    */
203   public BasicHttpException setStatusLine(HttpStatusLine value) {
204      return modify(() -> statusLine = reqnn("value", value));
205   }
206
207   /**
208    * Sets the status code, preserving the existing protocol version and reason phrase.
209    *
210    * @param value New status code.
211    * @return This object.
212    */
213   public BasicHttpException setStatusCode(int value) {
214      return modify(() -> statusLine = HttpStatusLineBean.of(statusLine.getProtocolVersion(), value, statusLine.getReasonPhrase()));
215   }
216
217   /**
218    * Sets the reason phrase, preserving the existing protocol version and status code.
219    *
220    * @param value New reason phrase. May be <jk>null</jk>.
221    * @return This object.
222    */
223   public BasicHttpException setReasonPhrase(String value) {
224      return modify(() -> statusLine = HttpStatusLineBean.of(statusLine.getProtocolVersion(), statusLine.getStatusCode(), value));
225   }
226
227   /**
228    * Sets the protocol version, preserving the existing status code and reason phrase.
229    *
230    * @param value New protocol version. Must not be <jk>null</jk>.
231    * @return This object.
232    */
233   public BasicHttpException setProtocolVersion(HttpProtocolVersion value) {
234      return modify(() -> {
235         reqnn("value", value);
236         statusLine = HttpStatusLineBean.of(value, statusLine.getStatusCode(), statusLine.getReasonPhrase());
237      });
238   }
239
240   /**
241    * Replaces all headers with the given list.
242    *
243    * @param values New headers. {@code null} is treated as an empty list.
244    * @return This object.
245    */
246   public BasicHttpException setHeaders(List<HttpHeader> values) {
247      return modify(() -> {
248         headers.clear();
249         if (values != null)
250            headers.append(values);
251      });
252   }
253
254   /**
255    * Replaces all headers with the given array.
256    *
257    * @param values New headers. {@code null} is treated as an empty array.
258    * @return This object.
259    */
260   public BasicHttpException setHeaders(HttpHeader...values) {
261      return modify(() -> {
262         headers.clear();
263         if (values != null)
264            headers.append(values);
265      });
266   }
267
268   /**
269    * Sets a single header, replacing any existing header with the same name.
270    *
271    * @param value The header to set. {@code null} is ignored.
272    * @return This object.
273    */
274   public BasicHttpException setHeader(HttpHeader value) {
275      return modify(() -> headers.set(value));
276   }
277
278   /**
279    * Sets a single header, replacing any existing header with the same name.
280    *
281    * @param name Header name. Must not be <jk>null</jk>.
282    * @param value Header value. May be <jk>null</jk>.
283    * @return This object.
284    */
285   public BasicHttpException setHeader(String name, String value) {
286      return modify(() -> headers.set(name, value));
287   }
288
289   /**
290    * Appends a header to the end of the header list.
291    *
292    * @param value The header to add. {@code null} is ignored.
293    * @return This object.
294    */
295   public BasicHttpException addHeader(HttpHeader value) {
296      return modify(() -> headers.append(value));
297   }
298
299   /**
300    * Appends a header to the end of the header list.
301    *
302    * @param name Header name. Must not be <jk>null</jk>.
303    * @param value Header value. May be <jk>null</jk>.
304    * @return This object.
305    */
306   public BasicHttpException addHeader(String name, String value) {
307      return modify(() -> headers.append(name, value));
308   }
309
310   /**
311    * Sets the response body.
312    *
313    * @param value New body. May be <jk>null</jk>.
314    * @return This object.
315    */
316   public BasicHttpException setBody(HttpBody value) {
317      return modify(() -> body = value);
318   }
319
320   /**
321    * Sets the response body to a plain-text string body (UTF-8, {@code text/plain}).
322    *
323    * @param value Body text. May be <jk>null</jk>.
324    * @return This object.
325    */
326   public BasicHttpException setContent(String value) {
327      return setBody(value != null ? StringBody.of(value) : null);
328   }
329
330   /**
331    * Sets the response body.
332    *
333    * @param value New body. May be <jk>null</jk>.
334    * @return This object.
335    */
336   public BasicHttpException setContent(HttpBody value) {
337      return setBody(value);
338   }
339
340   /**
341    * Sets the locale associated with this response.
342    *
343    * @param value New locale. May be <jk>null</jk>.
344    * @return This object.
345    */
346   public BasicHttpException setLocale(Locale value) {
347      return modify(() -> locale = value);
348   }
349
350   /**
351    * Returns whether this instance is an unmodifiable snapshot.
352    *
353    * @return <jk>true</jk> if this bean is an {@link UnmodifiableBean} snapshot.
354    */
355   public boolean isUnmodifiable() {
356      return this instanceof UnmodifiableBean;
357   }
358
359   /**
360    * Returns an unmodifiable snapshot of this bean.
361    *
362    * <p>
363    * The returned instance is a point-in-time copy of the leaf's nested {@code Unmodifiable} type; any attempt to set a
364    * property on it throws an {@link UnsupportedOperationException}.  This method is idempotent: if this bean is already
365    * unmodifiable it returns itself rather than taking another snapshot.  The receiver is left unchanged (and still
366    * modifiable unless it was already unmodifiable).
367    *
368    * @return An unmodifiable snapshot of this bean, or this bean if it is already unmodifiable.
369    */
370   public BasicHttpException unmodifiable() {
371      return this instanceof Unmodifiable ? this : new Unmodifiable(this);
372   }
373
374   // ------------------------------------------------------------------------------------------------------------------
375   // Stack/cause helpers
376   // ------------------------------------------------------------------------------------------------------------------
377
378   /**
379    * Returns the root cause of this exception.
380    *
381    * <p>
382    * The root cause is the first exception in the init-cause parent chain that's not one of the following:
383    * <ul>
384    *    <li>{@link BasicHttpException}
385    *    <li>{@link InvocationTargetException}
386    * </ul>
387    *
388    * @return The root cause of this exception, or <jk>null</jk> if no root cause was found.
389    */
390   public Throwable getRootCause() {
391      Throwable t = this;
392      while (nn(t)) {
393         if (! (t instanceof BasicHttpException || t instanceof InvocationTargetException))
394            return t;
395         t = t.getCause();
396      }
397      return null;
398   }
399
400   /**
401    * Returns all error messages from all errors in this exception's cause chain joined into a single string.
402    *
403    * @param scrubForXssVulnerabilities
404    *    If {@code true}, replaces {@code <}, {@code >}, and {@code &} characters with spaces.
405    * @return A multi-line string of messages. Never <jk>null</jk>.
406    */
407   public String getFullStackMessage(boolean scrubForXssVulnerabilities) {
408      var msg = getMessage();
409      var sb = new StringBuilder();
410      if (nn(msg)) {
411         if (scrubForXssVulnerabilities)
412            msg = msg.replace('<', ' ').replace('>', ' ').replace('&', ' ');
413         sb.append(msg);
414      }
415      var e = getCause();
416      while (nn(e)) {
417         msg = e.getMessage();
418         if (nn(msg) && scrubForXssVulnerabilities)
419            msg = msg.replace('<', ' ').replace('>', ' ').replace('&', ' ');
420         var cls = cns(e);
421         if (msg == null)
422            sb.append(f("\nCaused by (%s)", cls));
423         else
424            sb.append(f("\nCaused by (%s): %s", cls, msg));
425         e = e.getCause();
426      }
427      return sb.toString();
428   }
429
430   // ------------------------------------------------------------------------------------------------------------------
431   // Immutability paradigm plumbing
432   // ------------------------------------------------------------------------------------------------------------------
433
434   /**
435    * Single mutation funnel — the only choke-point through which all state changes on this bean flow.
436    *
437    * <p>
438    * Every fluent setter routes through this method.  The nested {@code Unmodifiable} snapshot overrides only this
439    * method to throw, which freezes the entire mutation surface with a single override.  Because this is a
440    * {@link Throwable}-rooted hierarchy (no CRTP self-type), the funnel returns the base type.
441    *
442    * @param mutation The state change to apply.
443    * @return This object.
444    */
445   protected BasicHttpException modify(Runnable mutation) {
446      mutation.run();
447      return this;
448   }
449
450   /**
451    * Deep-freeze hook invoked from the nested {@code Unmodifiable} constructor after the snapshot copy completes.
452    *
453    * <p>
454    * The mutable sub-beans are frozen here by <b>direct field assignment</b> (never via a setter, which would route
455    * through the throwing {@link #modify(Runnable)} and blow up during construction).  A leaf that adds its own mutable
456    * sub-bean overrides this method (calling {@code super.freeze()} first).
457    */
458   protected void freeze() {
459      headers = headers.unmodifiable();   // DIRECT field write — snapshot the copied HttpHeaderList.
460   }
461
462   @Override /* Object */
463   public boolean equals(Object o) {
464      // Content equality.  The 'body' HttpBody is deliberately excluded: it is a wrapper without value semantics
465      // and is derived from the message (getMessage()), which is already compared here.
466      return o instanceof BasicHttpException other && eq(this, other, (x, y) ->
467         eq(x.statusLine, y.statusLine) && eq(x.headers, y.headers) && eq(x.getMessage(), y.getMessage()));
468   }
469
470   @Override /* Object */
471   public int hashCode() {
472      return h(statusLine, headers, getMessage());
473   }
474
475   @Override /* Object */
476   public String toString() {
477      return statusLine.toString();
478   }
479
480   /**
481    * Unmodifiable point-in-time snapshot of the enclosing {@link BasicHttpException}.
482    *
483    * <p>
484    * Its only behavioral override is {@link #modify(Runnable)}, which throws — because all mutation is funneled through
485    * {@code modify(...)}, this single override freezes the entire mutation surface.
486    */
487   public static class Unmodifiable extends BasicHttpException implements UnmodifiableBean {
488
489      private static final long serialVersionUID = 1L;
490
491      /**
492       * Constructor.
493       *
494       * @param copyFrom The exception to snapshot.  Must not be <jk>null</jk>.
495       */
496      @SuppressWarnings({
497         "java:S1699" // Paradigm intentionally calls the overridable freeze() from the ctor to deep-freeze sub-beans.
498      })
499      protected Unmodifiable(BasicHttpException copyFrom) {
500         super(copyFrom);
501         freeze();
502      }
503
504      @Override /* Overridden from BasicHttpException */
505      protected BasicHttpException modify(Runnable mutation) {
506         throw uoex("Bean is unmodifiable.");
507      }
508   }
509}