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 §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}