1 /* 2 * The contents of this file are subject to the terms of the Common Development and 3 * Distribution License (the License). You may not use this file except in compliance with the 4 * License. 5 * 6 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 7 * specific language governing permission and limitations under the License. 8 * 9 * When distributing Covered Software, include this CDDL Header Notice in each file and include 10 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 11 * Header, with the fields enclosed by brackets [] replaced by your own identifying 12 * information: "Portions Copyrighted [year] [name of copyright owner]". 13 * 14 * Copyright 2011-2015 ForgeRock AS. 15 */ 16 17 package org.forgerock.json.resource; 18 19 import java.io.IOException; 20 import java.util.LinkedHashMap; 21 import java.util.Map; 22 23 import org.forgerock.http.routing.Version; 24 import org.forgerock.json.JsonValue; 25 import org.forgerock.util.promise.Promise; 26 import org.forgerock.util.promise.Promises; 27 28 /** 29 * An exception that is thrown during the processing of a JSON resource request. 30 * Contains an integer exception code and short reason phrase. A longer 31 * description of the exception is provided in the exception's detail message. 32 * <p> 33 * Positive 3-digit integer exception codes are explicitly reserved for 34 * exceptions that correspond with HTTP status codes. For the sake of 35 * interoperability with HTTP, if an exception corresponds with an HTTP error 36 * status, use the matching HTTP status code. 37 */ 38 public class ResourceException extends IOException implements Response { 39 40 /** 41 * The name of the JSON field used for the detail. 42 * 43 * @see #getDetail() 44 * @see #toJsonValue() 45 */ 46 public static final String FIELD_DETAIL = "detail"; 47 48 /** 49 * The name of the JSON field used for the message. 50 * 51 * @see #getMessage() 52 * @see #toJsonValue() 53 */ 54 public static final String FIELD_MESSAGE = "message"; 55 56 /** 57 * The name of the JSON field used for the reason. 58 * 59 * @see #getReason() 60 * @see #toJsonValue() 61 */ 62 public static final String FIELD_REASON = "reason"; 63 64 /** 65 * The name of the JSON field used for the code. 66 * 67 * @see #getCode() 68 * @see #toJsonValue() 69 */ 70 public static final String FIELD_CODE = "code"; 71 72 /** 73 * The name of the JSON field used for the cause message. 74 * 75 * @see #getCause() 76 * @see #toJsonValue() 77 */ 78 public static final String FIELD_CAUSE = "cause"; 79 80 /** 81 * Indicates that the request could not be understood by the resource due to 82 * malformed syntax. Equivalent to HTTP status: 400 Bad Request. 83 */ 84 public static final int BAD_REQUEST = 400; 85 86 /** 87 * Indicates the request could not be completed due to a conflict with the 88 * current state of the resource. Equivalent to HTTP status: 409 Conflict. 89 */ 90 public static final int CONFLICT = 409; 91 92 /** 93 * Indicates that the resource understood the request, but is refusing to 94 * fulfill it. Equivalent to HTTP status: 403 Forbidden. 95 */ 96 public static final int FORBIDDEN = 403; 97 98 /** 99 * Indicates that a resource encountered an unexpected condition which 100 * prevented it from fulfilling the request. Equivalent to HTTP status: 500 101 * Internal Server Error. 102 */ 103 public static final int INTERNAL_ERROR = 500; 104 105 /** 106 * Indicates that the resource could not be found. Equivalent to HTTP 107 * status: 404 Not Found. 108 */ 109 public static final int NOT_FOUND = 404; 110 111 /** 112 * Indicates that the resource does not implement/support the feature to 113 * fulfill the request HTTP status: 501 Not Implemented. 114 */ 115 public static final int NOT_SUPPORTED = 501; 116 117 /** 118 * Indicates that the resource is temporarily unable to handle the request. 119 * Equivalent to HTTP status: 503 Service Unavailable. 120 */ 121 public static final int UNAVAILABLE = 503; 122 123 /** 124 * Indicates that the resource's current version does not match the version 125 * provided. Equivalent to HTTP status: 412 Precondition Failed. 126 */ 127 public static final int VERSION_MISMATCH = 412; 128 129 /** 130 * Indicates that the resource requires a version, but no version was 131 * supplied in the request. Equivalent to 132 * draft-nottingham-http-new-status-03 HTTP status: 428 Precondition 133 * Required. 134 */ 135 public static final int VERSION_REQUIRED = 428; 136 137 /** Serializable class a version number. */ 138 private static final long serialVersionUID = 1L; 139 140 /** flag to indicate whether to include the cause. */ 141 private boolean includeCause = false; 142 143 /** 144 * Returns an exception with the specified HTTP error code, but no detail 145 * message or cause, and a default reason phrase. Useful for translating 146 * HTTP status codes to the relevant Java exception type. The type of the 147 * returned exception will be a sub-type of {@code ResourceException}. 148 * 149 * <p>If the type of the expected exception is known in advance, prefer to 150 * directly instantiate the exception type as usual: 151 * 152 * <pre> 153 * {@code 154 * throw new InternalServerErrorException("Server failed"); 155 * } 156 * </pre> 157 * 158 * @param code 159 * The HTTP error code. 160 * @return A resource exception having the provided HTTP error code. 161 */ 162 public static ResourceException newResourceException(final int code) { 163 return newResourceException(code, null); 164 } 165 166 /** 167 * Returns an exception with the specified HTTP error code and detail 168 * message, but no cause, and a default reason phrase. Useful for 169 * translating HTTP status codes to the relevant Java exception type. The 170 * type of the returned exception will be a sub-type of 171 * {@code ResourceException}. 172 * 173 * <p>If the type of the expected exception is known in advance, prefer to 174 * directly instantiate the exception type as usual: 175 * 176 * <pre> 177 * {@code 178 * throw new InternalServerErrorException("Server failed"); 179 * } 180 * </pre> 181 * 182 * @param code 183 * The HTTP error code. 184 * @param message 185 * The detail message. 186 * @return A resource exception having the provided HTTP error code. 187 */ 188 public static ResourceException newResourceException(final int code, final String message) { 189 return newResourceException(code, message, null); 190 } 191 192 /** 193 * Returns an exception with the specified HTTP error code, detail message, 194 * and cause, and a default reason phrase. Useful for translating HTTP 195 * status codes to the relevant Java exception type. The type of the 196 * returned exception will be a sub-type of {@code ResourceException}. 197 * 198 * <p>If the type of the expected exception is known in advance, prefer to 199 * directly instantiate the exception type as usual: 200 * 201 * <pre> 202 * {@code 203 * throw new InternalServerErrorException("Server failed"); 204 * } 205 * </pre> 206 * 207 * @param code 208 * The HTTP error code. 209 * @param message 210 * The detail message. 211 * @param cause 212 * The exception which caused this exception to be thrown. 213 * @return A resource exception having the provided HTTP error code. 214 */ 215 public static ResourceException newResourceException(final int code, 216 final String message, 217 final Throwable cause) { 218 final ResourceException ex; 219 switch (code) { 220 case BAD_REQUEST: 221 ex = new BadRequestException(message, cause); 222 break; 223 case FORBIDDEN: 224 ex = new ForbiddenException(message, cause); 225 break; // Authorization exceptions 226 case NOT_FOUND: 227 ex = new NotFoundException(message, cause); 228 break; 229 case CONFLICT: 230 ex = new ConflictException(message, cause); 231 break; 232 case VERSION_MISMATCH: 233 ex = new PreconditionFailedException(message, cause); 234 break; 235 case VERSION_REQUIRED: 236 ex = new PreconditionRequiredException(message, cause); 237 break; // draft-nottingham-http-new-status-03 238 case INTERNAL_ERROR: 239 ex = new InternalServerErrorException(message, cause); 240 break; 241 case NOT_SUPPORTED: 242 ex = new NotSupportedException(message, cause); 243 break; // Not Implemented 244 case UNAVAILABLE: 245 ex = new ServiceUnavailableException(message, cause); 246 break; 247 248 // Temporary failures without specific exception classes 249 case 408: // Request Time-out 250 case 504: // Gateway Time-out 251 ex = new RetryableException(code, message, cause); 252 break; 253 254 // Permanent Failures without specific exception classes 255 case 401: // Unauthorized - Missing or bad authentication 256 case 402: // Payment Required 257 case 405: // Method Not Allowed 258 case 406: // Not Acceptable 259 case 407: // Proxy Authentication Required 260 case 410: // Gone 261 case 411: // Length Required 262 case 413: // Request Entity Too Large 263 case 414: // Request-URI Too Large 264 case 415: // Unsupported Media Type 265 case 416: // Requested range not satisfiable 266 case 417: // Expectation Failed 267 case 502: // Bad Gateway 268 case 505: // HTTP Version not supported 269 ex = new PermanentException(code, message, cause); 270 break; 271 default: 272 ex = new UncategorizedException(code, message, cause); 273 } 274 return ex; 275 } 276 277 /** 278 * Returns an exception with the specified HTTP error code, but no detail 279 * message or cause, and a default reason phrase. Useful for translating 280 * HTTP status codes to the relevant Java exception type. The type of the 281 * returned exception will be a sub-type of {@code ResourceException}. 282 * 283 * @param code 284 * The HTTP error code. 285 * @return A resource exception having the provided HTTP error code. 286 * @deprecated in favor of {@link #newResourceException(int)} 287 */ 288 @Deprecated 289 public static ResourceException getException(final int code) { 290 return newResourceException(code, null); 291 } 292 293 /** 294 * Returns an exception with the specified HTTP error code and detail 295 * message, but no cause, and a default reason phrase. Useful for 296 * translating HTTP status codes to the relevant Java exception type. The 297 * type of the returned exception will be a sub-type of 298 * {@code ResourceException}. 299 * 300 * @param code 301 * The HTTP error code. 302 * @param message 303 * The detail message. 304 * @return A resource exception having the provided HTTP error code. 305 * @deprecated in favor of {@link #newResourceException(int, String)} 306 */ 307 @Deprecated 308 public static ResourceException getException(final int code, final String message) { 309 return newResourceException(code, message, null); 310 } 311 312 /** 313 * Returns an exception with the specified HTTP error code, detail message, 314 * and cause, and a default reason phrase. Useful for translating HTTP 315 * status codes to the relevant Java exception type. The type of the 316 * returned exception will be a sub-type of {@code ResourceException}. 317 * 318 * @param code 319 * The HTTP error code. 320 * @param message 321 * The detail message. 322 * @param cause 323 * The exception which caused this exception to be thrown. 324 * @return A resource exception having the provided HTTP error code. 325 * @deprecated in favor of {@link #newResourceException(int, String, Throwable)} 326 */ 327 @Deprecated 328 public static ResourceException getException(final int code, final String message, 329 final Throwable cause) { 330 return newResourceException(code, message, cause); 331 } 332 333 /** 334 * Returns the reason phrase for an HTTP error status code, per RFC 2616 and 335 * draft-nottingham-http-new-status-03. If no match is found, then a generic 336 * reason {@code "Resource Exception"} is returned. 337 */ 338 private static String reason(final int code) { 339 String result = "Resource Exception"; // default 340 switch (code) { 341 case BAD_REQUEST: 342 result = "Bad Request"; 343 break; 344 case 401: 345 result = "Unauthorized"; 346 break; // Missing or bad authentication (despite the name) 347 case 402: 348 result = "Payment Required"; 349 break; 350 case FORBIDDEN: 351 result = "Forbidden"; 352 break; // Authorization exceptions 353 case NOT_FOUND: 354 result = "Not Found"; 355 break; 356 case 405: 357 result = "Method Not Allowed"; 358 break; 359 case 406: 360 result = "Not Acceptable"; 361 break; 362 case 407: 363 result = "Proxy Authentication Required"; 364 break; 365 case 408: 366 result = "Request Time-out"; 367 break; 368 case CONFLICT: 369 result = "Conflict"; 370 break; 371 case 410: 372 result = "Gone"; 373 break; 374 case 411: 375 result = "Length Required"; 376 break; 377 case VERSION_MISMATCH: 378 result = "Precondition Failed"; 379 break; 380 case 413: 381 result = "Request Entity Too Large"; 382 break; 383 case 414: 384 result = "Request-URI Too Large"; 385 break; 386 case 415: 387 result = "Unsupported Media Type"; 388 break; 389 case 416: 390 result = "Requested range not satisfiable"; 391 break; 392 case 417: 393 result = "Expectation Failed"; 394 break; 395 case VERSION_REQUIRED: 396 result = "Precondition Required"; 397 break; // draft-nottingham-http-new-status-03 398 case INTERNAL_ERROR: 399 result = "Internal Server Error"; 400 break; 401 case NOT_SUPPORTED: 402 result = "Not Implemented"; 403 break; 404 case 502: 405 result = "Bad Gateway"; 406 break; 407 case UNAVAILABLE: 408 result = "Service Unavailable"; 409 break; 410 case 504: 411 result = "Gateway Time-out"; 412 break; 413 case 505: 414 result = "HTTP Version not supported"; 415 break; 416 } 417 return result; 418 } 419 420 /** 421 * Returns the message which should be returned by {@link #getMessage()}. 422 */ 423 private static String message(final int code, final String message, final Throwable cause) { 424 if (message != null) { 425 return message; 426 } else if (cause != null && cause.getMessage() != null) { 427 return cause.getMessage(); 428 } else { 429 return reason(code); 430 } 431 } 432 433 /** The numeric code of the exception. */ 434 private final int code; 435 436 /** The short reason phrase of the exception. */ 437 private String reason; 438 439 /** Additional detail which can be evaluated by applications. */ 440 private JsonValue detail = new JsonValue(null); 441 442 /** Resource API Version. */ 443 private Version resourceApiVersion; 444 445 /** 446 * Constructs a new exception with the specified exception code, and 447 * {@code null} as its detail message. If the error code corresponds with a 448 * known HTTP error status code, then the reason phrase is set to a 449 * corresponding reason phrase, otherwise is set to a generic value 450 * {@code "Resource Exception"}. 451 * 452 * @param code 453 * The numeric code of the exception. 454 */ 455 protected ResourceException(final int code) { 456 this(code, null, null); 457 } 458 459 /** 460 * Constructs a new exception with the specified exception code and detail 461 * message. 462 * 463 * @param code 464 * The numeric code of the exception. 465 * @param message 466 * The detail message. 467 */ 468 protected ResourceException(final int code, final String message) { 469 this(code, message, null); 470 } 471 472 /** 473 * Constructs a new exception with the specified exception code and detail 474 * message. 475 * 476 * @param code 477 * The numeric code of the exception. 478 * @param cause 479 * The exception which caused this exception to be thrown. 480 */ 481 protected ResourceException(final int code, final Throwable cause) { 482 this(code, null, cause); 483 } 484 485 /** 486 * Constructs a new exception with the specified exception code, reason 487 * phrase, detail message and cause. 488 * 489 * @param code 490 * The numeric code of the exception. 491 * @param message 492 * The detail message. 493 * @param cause 494 * The exception which caused this exception to be thrown. 495 */ 496 protected ResourceException(final int code, final String message, final Throwable cause) { 497 super(message(code, message, cause), cause); 498 this.code = code; 499 this.reason = reason(code); 500 } 501 502 /** 503 * Returns the numeric code of the exception. 504 * 505 * @return The numeric code of the exception. 506 */ 507 public final int getCode() { 508 return code; 509 } 510 511 /** 512 * Returns true if the HTTP error code is in the 500 range. 513 * 514 * @return <code>true</code> if HTTP error code is in the 500 range. 515 */ 516 public boolean isServerError() { 517 return code >= 500 && code <= 599; 518 } 519 520 /** 521 * Returns the additional detail which can be evaluated by applications. By 522 * default there is no additional detail ( 523 * {@code getDetail().isNull() == true}), and it is the responsibility of 524 * the resource provider to add it if needed. 525 * 526 * @return The additional detail which can be evaluated by applications 527 * (never {@code null}). 528 */ 529 public final JsonValue getDetail() { 530 return detail; 531 } 532 533 /** 534 * Returns the short reason phrase of the exception. 535 * 536 * @return The short reason phrase of the exception. 537 */ 538 public final String getReason() { 539 return reason; 540 } 541 542 /** 543 * Sets the additional detail which can be evaluated by applications. By 544 * default there is no additional detail ( 545 * {@code getDetail().isNull() == true}), and it is the responsibility of 546 * the resource provider to add it if needed. 547 * 548 * @param detail 549 * The additional detail which can be evaluated by applications. 550 * @return This resource exception. 551 */ 552 public final ResourceException setDetail(JsonValue detail) { 553 this.detail = detail != null ? detail : new JsonValue(null); 554 return this; 555 } 556 557 /** 558 * Sets/overrides the short reason phrase of the exception. 559 * 560 * @param reason 561 * The short reason phrase of the exception. 562 * @return This resource exception. 563 */ 564 public final ResourceException setReason(final String reason) { 565 this.reason = reason; 566 return this; 567 } 568 569 /** 570 * Returns this ResourceException with the includeCause flag set to true 571 * so that toJsonValue() method will include the cause if there is 572 * one supplied. 573 * 574 * @return the exception where this flag has been set 575 */ 576 public final ResourceException includeCauseInJsonValue() { 577 includeCause = true; 578 return this; 579 } 580 581 /** 582 * Returns the exception in a JSON object structure, suitable for inclusion 583 * in the entity of an HTTP error response. The JSON representation looks 584 * like this: 585 * 586 * <pre> 587 * { 588 * "code" : 404, 589 * "reason" : "...", // optional 590 * "message" : "...", // required 591 * "detail" : { ... } // optional 592 * "cause" : { ... } // optional iff includeCause is set to true 593 * } 594 * </pre> 595 * 596 * @return The exception in a JSON object structure, suitable for inclusion 597 * in the entity of an HTTP error response. 598 */ 599 public final JsonValue toJsonValue() { 600 final Map<String, Object> result = new LinkedHashMap<>(4); 601 result.put(FIELD_CODE, code); // required 602 if (reason != null) { // optional 603 result.put(FIELD_REASON, reason); 604 } 605 final String message = getMessage(); 606 if (message != null) { // should always be present 607 result.put(FIELD_MESSAGE, message); 608 } 609 if (!detail.isNull()) { 610 result.put(FIELD_DETAIL, detail.getObject()); 611 } 612 if (includeCause && getCause() != null && getCause().getMessage() != null) { 613 final Map<String, Object> cause = new LinkedHashMap<>(2); 614 cause.put("message", getCause().getMessage()); 615 result.put(FIELD_CAUSE, cause); 616 } 617 return new JsonValue(result); 618 } 619 620 @Override 621 public void setResourceApiVersion(Version version) { 622 this.resourceApiVersion = version; 623 } 624 625 @Override 626 public Version getResourceApiVersion() { 627 return resourceApiVersion; 628 } 629 630 /** 631 * Return this ResourceException as a Promise. 632 * 633 * @param <V> the result value type of the promise 634 * @return an Exception promise of type ResourceException 635 */ 636 public <V> Promise<V, ResourceException> asPromise() { 637 return Promises.newExceptionPromise(this); 638 } 639 }