001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2011-2015 ForgeRock AS.
015 */
016
017package org.forgerock.json.resource;
018
019import java.io.IOException;
020import java.util.LinkedHashMap;
021import java.util.Map;
022
023import org.forgerock.http.routing.Version;
024import org.forgerock.json.JsonValue;
025import org.forgerock.util.promise.Promise;
026import org.forgerock.util.promise.Promises;
027
028/**
029 * An exception that is thrown during the processing of a JSON resource request.
030 * Contains an integer exception code and short reason phrase. A longer
031 * description of the exception is provided in the exception's detail message.
032 * <p>
033 * Positive 3-digit integer exception codes are explicitly reserved for
034 * exceptions that correspond with HTTP status codes. For the sake of
035 * interoperability with HTTP, if an exception corresponds with an HTTP error
036 * status, use the matching HTTP status code.
037 */
038public class ResourceException extends IOException implements Response {
039
040    /**
041     * The name of the JSON field used for the detail.
042     *
043     * @see #getDetail()
044     * @see #toJsonValue()
045     */
046    public static final String FIELD_DETAIL = "detail";
047
048    /**
049     * The name of the JSON field used for the message.
050     *
051     * @see #getMessage()
052     * @see #toJsonValue()
053     */
054    public static final String FIELD_MESSAGE = "message";
055
056    /**
057     * The name of the JSON field used for the reason.
058     *
059     * @see #getReason()
060     * @see #toJsonValue()
061     */
062    public static final String FIELD_REASON = "reason";
063
064    /**
065     * The name of the JSON field used for the code.
066     *
067     * @see #getCode()
068     * @see #toJsonValue()
069     */
070    public static final String FIELD_CODE = "code";
071
072    /**
073     * The name of the JSON field used for the cause message.
074     *
075     * @see #getCause()
076     * @see #toJsonValue()
077     */
078    public static final String FIELD_CAUSE = "cause";
079
080    /**
081     * Indicates that the request could not be understood by the resource due to
082     * malformed syntax. Equivalent to HTTP status: 400 Bad Request.
083     */
084    public static final int BAD_REQUEST = 400;
085
086    /**
087     * Indicates the request could not be completed due to a conflict with the
088     * current state of the resource. Equivalent to HTTP status: 409 Conflict.
089     */
090    public static final int CONFLICT = 409;
091
092    /**
093     * Indicates that the resource understood the request, but is refusing to
094     * fulfill it. Equivalent to HTTP status: 403 Forbidden.
095     */
096    public static final int FORBIDDEN = 403;
097
098    /**
099     * 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}