001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk;
019
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import net.jcip.annotations.Immutable;
029import net.minidev.json.JSONObject;
030
031import com.nimbusds.common.contenttype.ContentType;
032import com.nimbusds.oauth2.sdk.http.HTTPResponse;
033import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
034import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
035
036
037/**
038 * Error object, used to encapsulate OAuth 2.0 and other errors.
039 *
040 * <p>Example error object as HTTP response:
041 *
042 * <pre>
043 * HTTP/1.1 400 Bad Request
044 * Content-Type: application/json;charset=UTF-8
045 * Cache-Control: no-store
046 * Pragma: no-cache
047 *
048 * {
049 *   "error" : "invalid_request"
050 * }
051 * </pre>
052 */
053@Immutable
054public class ErrorObject {
055        
056        
057        /**
058         * The error code, may not always be defined.
059         */
060        private final String code;
061
062
063        /**
064         * Optional error description.
065         */
066        private final String description;
067
068
069        /**
070         * Optional HTTP status code, 0 if not specified.
071         */
072        private final int httpStatusCode;
073
074
075        /**
076         * Optional URI of a web page that includes additional information 
077         * about the error.
078         */
079        private final URI uri;
080
081
082        /**
083         * Creates a new error with the specified code.
084         *
085         * @param code The error code, {@code null} if not specified.
086         */
087        public ErrorObject(final String code) {
088        
089                this(code, null, 0, null);
090        }
091        
092        
093        /**
094         * Creates a new error with the specified code and description.
095         *
096         * @param code        The error code, {@code null} if not specified.
097         * @param description The error description, {@code null} if not
098         *                    specified.
099         */
100        public ErrorObject(final String code, final String description) {
101        
102                this(code, description, 0, null);
103        }
104
105
106        /**
107         * Creates a new error with the specified code, description and HTTP 
108         * status code.
109         *
110         * @param code           The error code, {@code null} if not specified.
111         * @param description    The error description, {@code null} if not
112         *                       specified.
113         * @param httpStatusCode The HTTP status code, zero if not specified.
114         */
115        public ErrorObject(final String code, final String description, 
116                           final int httpStatusCode) {
117        
118                this(code, description, httpStatusCode, null);
119        }
120
121
122        /**
123         * Creates a new error with the specified code, description, HTTP 
124         * status code and page URI.
125         *
126         * @param code           The error code, {@code null} if not specified.
127         * @param description    The error description, {@code null} if not
128         *                       specified.
129         * @param httpStatusCode The HTTP status code, zero if not specified.
130         * @param uri            The error page URI, {@code null} if not
131         *                       specified.
132         */
133        public ErrorObject(final String code, final String description, 
134                           final int httpStatusCode, final URI uri) {
135        
136                this.code = code;
137                this.description = description;
138                this.httpStatusCode = httpStatusCode;
139                this.uri = uri;
140        }
141
142
143        /**
144         * Gets the error code.
145         *
146         * @return The error code, {@code null} if not specified.
147         */
148        public String getCode() {
149
150                return code;
151        }
152        
153        
154        /**
155         * Gets the error description.
156         *
157         * @return The error description, {@code null} if not specified.
158         */
159        public String getDescription() {
160        
161                return description;
162        }
163
164
165        /**
166         * Sets the error description.
167         *
168         * @param description The error description, {@code null} if not 
169         *                    specified.
170         *
171         * @return A copy of this error with the specified description.
172         */
173        public ErrorObject setDescription(final String description) {
174
175                return new ErrorObject(getCode(), description, getHTTPStatusCode(), getURI());
176        }
177
178
179        /**
180         * Appends the specified text to the error description.
181         *
182         * @param text The text to append to the error description, 
183         *             {@code null} if not specified.
184         *
185         * @return A copy of this error with the specified appended 
186         *         description.
187         */
188        public ErrorObject appendDescription(final String text) {
189
190                String newDescription;
191
192                if (getDescription() != null)
193                        newDescription = getDescription() + text;
194                else
195                        newDescription = text;
196
197                return new ErrorObject(getCode(), newDescription, getHTTPStatusCode(), getURI());
198        }
199
200
201        /**
202         * Gets the HTTP status code.
203         *
204         * @return The HTTP status code, zero if not specified.
205         */
206        public int getHTTPStatusCode() {
207
208                return httpStatusCode;
209        }
210
211
212        /**
213         * Sets the HTTP status code.
214         *
215         * @param httpStatusCode  The HTTP status code, zero if not specified.
216         *
217         * @return A copy of this error with the specified HTTP status code.
218         */
219        public ErrorObject setHTTPStatusCode(final int httpStatusCode) {
220
221                return new ErrorObject(getCode(), getDescription(), httpStatusCode, getURI());
222        }
223
224
225        /**
226         * Gets the error page URI.
227         *
228         * @return The error page URI, {@code null} if not specified.
229         */
230        public URI getURI() {
231
232                return uri;
233        }
234
235
236        /**
237         * Sets the error page URI.
238         *
239         * @param uri The error page URI, {@code null} if not specified.
240         *
241         * @return A copy of this error with the specified page URI.
242         */
243        public ErrorObject setURI(final URI uri) {
244
245                return new ErrorObject(getCode(), getDescription(), getHTTPStatusCode(), uri);
246        }
247
248
249        /**
250         * Returns a JSON object representation of this error object.
251         *
252         * <p>Example:
253         *
254         * <pre>
255         * {
256         *   "error"             : "invalid_grant",
257         *   "error_description" : "Invalid resource owner credentials"
258         * }
259         * </pre>
260         *
261         * @return The JSON object.
262         */
263        public JSONObject toJSONObject() {
264
265                JSONObject o = new JSONObject();
266
267                if (code != null) {
268                        o.put("error", code);
269                }
270
271                if (description != null) {
272                        o.put("error_description", description);
273                }
274
275                if (uri != null) {
276                        o.put("error_uri", uri.toString());
277                }
278
279                return o;
280        }
281        
282        
283        /**
284         * Returns a parameters representation of this error object. Suitable
285         * for URL-encoded error responses.
286         *
287         * @return The parameters.
288         */
289        public Map<String, List<String>> toParameters() {
290                
291                Map<String,List<String>> params = new HashMap<>();
292                
293                if (getCode() != null) {
294                        params.put("error", Collections.singletonList(getCode()));
295                }
296                
297                if (getDescription() != null) {
298                        params.put("error_description", Collections.singletonList(getDescription()));
299                }
300                
301                if (getURI() != null) {
302                        params.put("error_uri", Collections.singletonList(getURI().toString()));
303                }
304                
305                return params;
306        }
307        
308        
309        /**
310         * Returns an HTTP response for this error object. If no HTTP status
311         * code is specified it will be set to 400 (Bad Request). If an error
312         * code is specified the {@code Content-Type} header will be set to
313         * {@link ContentType#APPLICATION_JSON application/json; charset=UTF-8}
314         * and the error JSON object will be put in the entity body.
315         *
316         * @return The HTTP response.
317         */
318        public HTTPResponse toHTTPResponse() {
319                
320                int statusCode = (getHTTPStatusCode() > 0) ? getHTTPStatusCode() : HTTPResponse.SC_BAD_REQUEST;
321                HTTPResponse httpResponse = new HTTPResponse(statusCode);
322                httpResponse.setCacheControl("no-store");
323                httpResponse.setPragma("no-cache");
324                
325                if (getCode() != null) {
326                        httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
327                        httpResponse.setContent(toJSONObject().toJSONString());
328                }
329                
330                return httpResponse;
331        }
332
333
334        /**
335         * @see #getCode
336         */
337        @Override
338        public String toString() {
339
340                return code != null ? code : "null";
341        }
342
343
344        @Override
345        public int hashCode() {
346
347                return code != null ? code.hashCode() : "null".hashCode();
348        }
349
350
351        @Override
352        public boolean equals(final Object object) {
353        
354                return object instanceof ErrorObject &&
355                       this.toString().equals(object.toString());
356        }
357
358
359        /**
360         * Parses an error object from the specified JSON object.
361         *
362         * @param jsonObject The JSON object to parse. Must not be
363         *                   {@code null}.
364         *
365         * @return The error object.
366         */
367        public static ErrorObject parse(final JSONObject jsonObject) {
368
369                String code = null;
370                try {
371                        code = JSONObjectUtils.getString(jsonObject, "error", null);
372                } catch (ParseException e) {
373                        // ignore and continue
374                }
375                
376                String description = null;
377                try {
378                        description = JSONObjectUtils.getString(jsonObject, "error_description", null);
379                } catch (ParseException e) {
380                        // ignore and continue
381                }
382                
383                URI uri = null;
384                try {
385                        uri = JSONObjectUtils.getURI(jsonObject, "error_uri", null);
386                } catch (ParseException e) {
387                        // ignore and continue
388                }
389
390                return new ErrorObject(code, description, 0, uri);
391        }
392        
393        
394        /**
395         * Parses an error object from the specified parameters representation.
396         * Suitable for URL-encoded error responses.
397         *
398         * @param params The parameters. Must not be {@code null}.
399         *
400         * @return The error object.
401         */
402        public static ErrorObject parse(final Map<String, List<String>> params) {
403                
404                String code = MultivaluedMapUtils.getFirstValue(params, "error");
405                String description = MultivaluedMapUtils.getFirstValue(params, "error_description");
406                String uriString = MultivaluedMapUtils.getFirstValue(params, "error_uri");
407                
408                URI uri = null;
409                if (uriString != null) {
410                        try {
411                                uri = new URI(uriString);
412                        } catch (URISyntaxException e) {
413                                // ignore
414                        }
415                }
416                
417                return new ErrorObject(code, description, 0, uri);
418        }
419
420
421        /**
422         * Parses an error object from the specified HTTP response.
423         *
424         * @param httpResponse The HTTP response to parse. Must not be
425         *                     {@code null}.
426         *
427         * @return The error object.
428         */
429        public static ErrorObject parse(final HTTPResponse httpResponse) {
430
431                JSONObject jsonObject;
432                try {
433                        jsonObject = httpResponse.getContentAsJSONObject();
434                } catch (ParseException e) {
435                        return new ErrorObject(null, null, httpResponse.getStatusCode());
436                }
437
438                ErrorObject intermediary = parse(jsonObject);
439
440                return new ErrorObject(
441                        intermediary.getCode(),
442                        intermediary.description,
443                        httpResponse.getStatusCode(),
444                        intermediary.getURI());
445        }
446}