View Javadoc
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 }