001package com.nimbusds.oauth2.sdk.http;
002
003
004import java.io.IOException;
005import java.io.PrintWriter;
006import java.net.URI;
007import java.net.URISyntaxException;
008import java.util.Arrays;
009import java.util.Map;
010
011import javax.servlet.http.HttpServletResponse;
012
013import net.jcip.annotations.ThreadSafe;
014
015import net.minidev.json.JSONObject;
016
017import com.nimbusds.jwt.JWT;
018import com.nimbusds.jwt.JWTParser;
019
020import com.nimbusds.oauth2.sdk.ParseException;
021import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
022
023
024/**
025 * HTTP response with support for the parameters required to construct an 
026 * {@link com.nimbusds.oauth2.sdk.Response OAuth 2.0 response message}.
027 *
028 * <p>Provided HTTP status code constants:
029 *
030 * <ul>
031 *     <li>{@link #SC_OK HTTP 200 OK}
032 *     <li>{@link #SC_CREATED HTTP 201 Created}
033 *     <li>{@link #SC_FOUND HTTP 302 Redirect}
034 *     <li>{@link #SC_BAD_REQUEST HTTP 400 Bad request}
035 *     <li>{@link #SC_UNAUTHORIZED HTTP 401 Unauthorized}
036 *     <li>{@link #SC_FORBIDDEN HTTP 403 Forbidden}
037 *     <li>{@link #SC_SERVER_ERROR HTTP 500 Server error}
038 * </ul>
039 *
040 * <p>Supported response headers:
041 *
042 * <ul>
043 *     <li>Location
044 *     <li>Content-Type
045 *     <li>Cache-Control
046 *     <li>Pragma
047 *     <li>Www-Authenticate
048 * </ul>
049 */
050@ThreadSafe
051public class HTTPResponse extends HTTPMessage {
052
053        
054        /**
055         * HTTP status code (200) indicating the request succeeded.
056         */
057        public static final int SC_OK = 200;
058
059
060        /**
061         * HTTP status code (201) indicating the request succeeded with a new
062         * resource being created.
063         */
064        public static final int SC_CREATED = 201;
065        
066        
067        /**
068         * HTTP status code (302) indicating that the resource resides
069         * temporarily under a different URI (redirect).
070         */
071        public static final int SC_FOUND = 302;
072        
073        
074        /**
075         * HTTP status code (400) indicating a bad request.
076         */
077        public static final int SC_BAD_REQUEST = 400;
078        
079        
080        /**
081         * HTTP status code (401) indicating that the request requires HTTP 
082         * authentication.
083         */
084        public static final int SC_UNAUTHORIZED = 401;
085        
086        
087        /**
088         * HTTP status code (403) indicating that access to the resource was
089         * forbidden.
090         */
091        public static final int SC_FORBIDDEN = 403;
092
093
094        /**
095         * HTTP status code (500) indicating an internal server error.
096         */
097        public static final int SC_SERVER_ERROR = 500;
098
099
100        /**
101         * HTTP status code (503) indicating the server is unavailable.
102         */
103        public static final int SC_SERVICE_UNAVAILABLE = 503;
104
105
106        /**
107         * The HTTP status code.
108         */
109        private final int statusCode;
110        
111        
112        /**
113         * The raw response content.
114         */
115        private String content = null;
116        
117        
118        /**
119         * Creates a new minimal HTTP response with the specified status code.
120         *
121         * @param statusCode The HTTP status code.
122         */
123        public HTTPResponse(final int statusCode) {
124        
125                this.statusCode = statusCode;
126        }
127        
128        
129        /**
130         * Gets the HTTP status code.
131         *
132         * @return The HTTP status code.
133         */
134        public int getStatusCode() {
135        
136                return statusCode;
137        }
138
139
140        /**
141         * Returns {@code true} if the HTTP status code indicates success
142         * (2xx).
143         *
144         * @return {@code true} if the HTTP status code indicates success, else
145         *         {@code false}.
146         */
147        public boolean indicatesSuccess() {
148
149                return statusCode >= 200 && statusCode < 300;
150        }
151        
152        
153        /**
154         * Ensures this HTTP response has the specified status code.
155         *
156         * @param expectedStatusCode The expected status code(s).
157         *
158         * @throws ParseException If the status code of this HTTP response 
159         *                        doesn't match the expected.
160         */ 
161        public void ensureStatusCode(final int ... expectedStatusCode)
162                throws ParseException {
163
164                for (int c: expectedStatusCode) {
165
166                        if (this.statusCode == c)
167                                return;
168                }
169
170                throw new ParseException("Unexpected HTTP status code " + 
171                        this.statusCode + 
172                        ", must be " +  
173                        Arrays.toString(expectedStatusCode));
174        }
175
176
177        /**
178         * Ensures this HTTP response does not have a {@link #SC_OK 200 OK} 
179         * status code.
180         *
181         * @throws ParseException If the status code of this HTTP response is
182         *                        200 OK.
183         */
184        public void ensureStatusCodeNotOK()
185                throws ParseException {
186
187                if (statusCode == SC_OK)
188                        throw new ParseException("Unexpected HTTP status code, must not be 200 (OK)");
189        }
190        
191        
192        /**
193         * Gets the {@code Location} header value (for redirects).
194         *
195         * @return The header value, {@code null} if not specified.
196         */
197        public URI getLocation() {
198
199                String value = getHeader("Location");
200
201                if (value == null) {
202                        return null;
203                }
204
205                try {
206                        return new URI(value);
207
208                } catch (URISyntaxException e) {
209                        return null;
210                }
211        }
212        
213        
214        /**
215         * Sets the {@code Location} header value (for redirects).
216         *
217         * @param location The header value, {@code null} if not specified.
218         */
219        public void setLocation(final URI location) {
220        
221                setHeader("Location", location != null ? location.toString() : null);
222        }
223        
224        
225        /**
226         * Gets the {@code Cache-Control} header value.
227         *
228         * @return The header value, {@code null} if not specified.
229         */
230        public String getCacheControl() {
231        
232                return getHeader("Cache-Control");
233        }
234
235
236        /**
237         * Sets the {@code Cache-Control} header value.
238         *
239         * @param cacheControl The header value, {@code null} if not specified.
240         */
241        public void setCacheControl(final String cacheControl) {
242        
243                setHeader("Cache-Control", cacheControl);
244        }
245        
246        
247        /**
248         * Gets the {@code Pragma} header value.
249         *
250         * @return The header value, {@code null} if not specified.
251         */
252        public String getPragma() {
253        
254                return getHeader("Pragma");
255        }
256        
257        
258        /**
259         * Sets the {@code Pragma} header value.
260         *
261         * @param pragma The header value, {@code null} if not specified.
262         */
263        public void setPragma(final String pragma) {
264        
265                setHeader("Pragma", pragma);
266        }
267        
268        
269        /**
270         * Gets the {@code WWW-Authenticate} header value.
271         *
272         * @return The header value, {@code null} if not specified.
273         */
274        public String getWWWAuthenticate() {
275        
276                return getHeader("WWW-Authenticate");
277        }
278        
279        
280        /**
281         * Sets the {@code WWW-Authenticate} header value.
282         *
283         * @param wwwAuthenticate The header value, {@code null} if not 
284         *                        specified.
285         */
286        public void setWWWAuthenticate(final String wwwAuthenticate) {
287        
288                setHeader("WWW-Authenticate", wwwAuthenticate);
289        }
290        
291        
292        /**
293         * Ensures this HTTP response has a specified content body.
294         *
295         * @throws ParseException If the content body is missing or empty.
296         */
297        private void ensureContent()
298                throws ParseException {
299                
300                if (content == null || content.isEmpty())
301                        throw new ParseException("Missing or empty HTTP response body");
302        }
303        
304        
305        /**
306         * Gets the raw response content.
307         *
308         * @return The raw response content, {@code null} if none.
309         */
310        public String getContent() {
311        
312                return content;
313        }
314        
315        
316        /**
317         * Gets the response content as a JSON object.
318         *
319         * @return The response content as a JSON object.
320         *
321         * @throws ParseException If the Content-Type header isn't 
322         *                        {@code application/json}, the response
323         *                        content is {@code null}, empty or couldn't be
324         *                        parsed to a valid JSON object.
325         */
326        public JSONObject getContentAsJSONObject()
327                throws ParseException {
328                
329                ensureContentType(CommonContentTypes.APPLICATION_JSON);
330                
331                ensureContent();
332                
333                return JSONObjectUtils.parseJSONObject(content);
334        }
335        
336        
337        /**
338         * Gets the response content as a JSON Web Token (JWT).
339         *
340         * @return The response content as a JSON Web Token (JWT).
341         *
342         * @throws ParseException If the Content-Type header isn't
343         *                        {@code application/jwt}, the response content 
344         *                        is {@code null}, empty or couldn't be parsed
345         *                        to a valid JSON Web Token (JWT).
346         */
347        public JWT getContentAsJWT()
348                throws ParseException {
349                
350                ensureContentType(CommonContentTypes.APPLICATION_JWT);
351                
352                ensureContent();
353                
354                try {
355                        return JWTParser.parse(content);
356                        
357                } catch (java.text.ParseException e) {
358                
359                        throw new ParseException(e.getMessage(), e);
360                }
361        }
362        
363        
364        /**
365         * Sets the raw response content.
366         *
367         * @param content The raw response content, {@code null} if none.
368         */
369        public void setContent(final String content) {
370        
371                this.content = content;
372        }
373        
374        
375        /**
376         * Applies the status code, headers and content of this HTTP response
377         * object to the specified HTTP servlet response.
378         *
379         * @param sr The HTTP servlet response to have the properties of this
380         *           HTTP request applied to. Must not be {@code null}.
381         *
382         * @throws IOException If the response content couldn't be written.
383         */
384        public void applyTo(final HttpServletResponse sr)
385                throws IOException {
386        
387                // Set the status code
388                sr.setStatus(statusCode);
389        
390        
391                // Set the headers, but only if explicitly specified
392                for (Map.Entry<String,String> header: getHeaders().entrySet()) {
393                        sr.setHeader(header.getKey(), header.getValue());
394                }
395                
396                if (getContentType() != null)
397                        sr.setContentType(getContentType().toString());
398        
399        
400                // Write out the content
401        
402                if (content != null) {
403                
404                        PrintWriter writer = sr.getWriter();
405                        writer.print(content);
406                        writer.close();
407                }
408        }
409}