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 }