001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019package org.apache.isis.viewer.restfulobjects.applib.client;
020
021import java.io.IOException;
022import java.util.Date;
023import java.util.Map;
024
025import javax.ws.rs.core.CacheControl;
026import javax.ws.rs.core.MediaType;
027import javax.ws.rs.core.MultivaluedMap;
028import javax.ws.rs.core.Response;
029import javax.ws.rs.core.Response.Status;
030import javax.ws.rs.core.Response.Status.Family;
031import javax.ws.rs.core.Response.StatusType;
032
033import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
034import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
035import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
036import org.apache.isis.viewer.restfulobjects.applib.util.Parser;
037import org.codehaus.jackson.JsonParseException;
038import org.codehaus.jackson.map.JsonMappingException;
039
040import com.google.common.collect.Maps;
041
042public class RestfulResponse<T> {
043
044    public final static class HttpStatusCode {
045
046        private final static Map<Status, HttpStatusCode> statii = Maps.newHashMap();
047        private final static Map<Integer, HttpStatusCode> statusCodes = Maps.newHashMap();
048
049        private static class StatusTypeImpl implements StatusType {
050
051            private final int statusCode;
052            private final Family family;
053            private final String reasonPhrase;
054
055            private StatusTypeImpl(final int statusCode, final Family family, final String reasonPhrase) {
056                this.statusCode = statusCode;
057                this.family = family;
058                this.reasonPhrase = reasonPhrase;
059            }
060
061            @Override
062            public int getStatusCode() {
063                return statusCode;
064            }
065
066            @Override
067            public Family getFamily() {
068                return family;
069            }
070
071            @Override
072            public String getReasonPhrase() {
073                return reasonPhrase;
074            }
075        }
076
077        public static HttpStatusCode lookup(final int status) {
078            return statusCodes.get(status);
079        }
080
081        public static Family lookupFamily(final int statusCode) {
082            switch (statusCode / 100) {
083            case 1:
084                return Family.INFORMATIONAL;
085            case 2:
086                return Family.SUCCESSFUL;
087            case 3:
088                return Family.REDIRECTION;
089            case 4:
090                return Family.CLIENT_ERROR;
091            case 5:
092                return Family.SERVER_ERROR;
093            default:
094                return Family.OTHER;
095            }
096        }
097
098        // public static final int SC_CONTINUE = 100;
099        // public static final int SC_SWITCHING_PROTOCOLS = 101;
100        // public static final int SC_PROCESSING = 102;
101
102        public final static HttpStatusCode OK = new HttpStatusCode(200, Status.OK);
103        public final static HttpStatusCode CREATED = new HttpStatusCode(201, Status.CREATED);
104
105        // public static final int SC_ACCEPTED = 202;
106        // public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
107
108        public static final HttpStatusCode NO_CONTENT = new HttpStatusCode(204, Status.NO_CONTENT);
109
110        // public static final int SC_RESET_CONTENT = 205;
111        // public static final int SC_PARTIAL_CONTENT = 206;
112        // public static final int SC_MULTI_STATUS = 207;
113        // public static final int SC_MULTIPLE_CHOICES = 300;
114        // public static final int SC_MOVED_PERMANENTLY = 301;
115        // public static final int SC_MOVED_TEMPORARILY = 302;
116        // public static final int SC_SEE_OTHER = 303;
117        public final static HttpStatusCode NOT_MODIFIED = new HttpStatusCode(304, Status.BAD_REQUEST);
118
119        // public static final int SC_NOT_MODIFIED = 304;
120        // public static final int SC_USE_PROXY = 305;
121        // public static final int SC_TEMPORARY_REDIRECT = 307;
122
123        public final static HttpStatusCode BAD_REQUEST = new HttpStatusCode(400, Status.BAD_REQUEST);
124        public final static HttpStatusCode UNAUTHORIZED = new HttpStatusCode(401, Status.UNAUTHORIZED);
125
126        // public static final int SC_PAYMENT_REQUIRED = 402;
127        public static final HttpStatusCode FORBIDDEN = new HttpStatusCode(403, Status.FORBIDDEN);
128
129        public final static HttpStatusCode NOT_FOUND = new HttpStatusCode(404, Status.NOT_FOUND);
130        public final static HttpStatusCode METHOD_NOT_ALLOWED = new HttpStatusCode(405, new StatusTypeImpl(405, Family.CLIENT_ERROR, "Method not allowed"));
131        public final static HttpStatusCode NOT_ACCEPTABLE = new HttpStatusCode(406, Status.NOT_ACCEPTABLE);
132
133        // public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
134        // public static final int SC_REQUEST_TIMEOUT = 408;
135
136        public final static HttpStatusCode CONFLICT = new HttpStatusCode(409, Status.CONFLICT);
137
138        // public static final int SC_GONE = 410;
139        // public static final int SC_LENGTH_REQUIRED = 411;
140        // public static final int SC_PRECONDITION_FAILED = 412;
141        // public static final int SC_REQUEST_TOO_LONG = 413;
142        // public static final int SC_REQUEST_URI_TOO_LONG = 414;
143        // public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
144
145        public final static HttpStatusCode UNSUPPORTED_MEDIA_TYPE = new HttpStatusCode(415, Status.UNSUPPORTED_MEDIA_TYPE);
146
147        // public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
148        // public static final int SC_EXPECTATION_FAILED = 417;
149        // public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
150
151        public final static HttpStatusCode METHOD_FAILURE = new HttpStatusCode(420, new StatusTypeImpl(420, Family.CLIENT_ERROR, "Method failure"));
152
153        // public static final int SC_UNPROCESSABLE_ENTITY = 422;
154        public final static HttpStatusCode VALIDATION_FAILED = new HttpStatusCode(422, new StatusTypeImpl(422, Family.CLIENT_ERROR, "Validation failed"));
155        
156        // public static final int SC_LOCKED = 423;
157        // public static final int SC_FAILED_DEPENDENCY = 424;
158        
159        public final static HttpStatusCode PRECONDITION_HEADER_MISSING = new HttpStatusCode(428, new StatusTypeImpl(428, Family.CLIENT_ERROR, "Precondition header missing"));
160
161        public final static HttpStatusCode INTERNAL_SERVER_ERROR = new HttpStatusCode(500, Status.INTERNAL_SERVER_ERROR);
162        public final static HttpStatusCode NOT_IMPLEMENTED = new HttpStatusCode(501, new StatusTypeImpl(501, Family.SERVER_ERROR, "Not implemented"));
163
164        // public static final int SC_BAD_GATEWAY = 502;
165        // public static final int SC_SERVICE_UNAVAILABLE = 503;
166        // public static final int SC_GATEWAY_TIMEOUT = 504;
167        // public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
168        // public static final int SC_INSUFFICIENT_STORAGE = 507;
169
170        public final static HttpStatusCode statusFor(final int statusCode) {
171            final HttpStatusCode httpStatusCode = statusCodes.get(statusCode);
172            if (httpStatusCode != null) {
173                return httpStatusCode;
174            }
175            return syncStatusFor(statusCode);
176        }
177
178        public final static HttpStatusCode statusFor(final Status status) {
179            return statii.get(status);
180        }
181
182        private final static synchronized HttpStatusCode syncStatusFor(final int statusCode) {
183            HttpStatusCode httpStatusCode = statusCodes.get(statusCode);
184            if (httpStatusCode == null) {
185                httpStatusCode = new HttpStatusCode(statusCode, null);
186                statusCodes.put(statusCode, httpStatusCode);
187            }
188            return httpStatusCode;
189        }
190
191        private final int statusCode;
192        private final Family family;
193        private final StatusType jaxrsStatusType;
194
195        private HttpStatusCode(final int statusCode, final StatusType status) {
196            this.statusCode = statusCode;
197            this.jaxrsStatusType = status;
198            family = lookupFamily(statusCode);
199            statusCodes.put(statusCode, this);
200        }
201
202        public int getStatusCode() {
203            return statusCode;
204        }
205
206        public StatusType getJaxrsStatusType() {
207            return jaxrsStatusType;
208        }
209
210        public Family getFamily() {
211            return family;
212        }
213
214        @Override
215        public int hashCode() {
216            return statusCode;
217        }
218
219        @Override
220        public boolean equals(final Object obj) {
221            if (this == obj) {
222                return true;
223            }
224            if (obj == null) {
225                return false;
226            }
227            if (getClass() != obj.getClass()) {
228                return false;
229            }
230            final HttpStatusCode other = (HttpStatusCode) obj;
231            if (statusCode != other.statusCode) {
232                return false;
233            }
234            return true;
235        }
236
237        @Override
238        public String toString() {
239            return "HttpStatusCode " + statusCode + ", " + family;
240        }
241
242    }
243
244    public static class Header<X> {
245
246        public final static Header<String> WARNING = new Header<String>("Warning", warningParser());
247        public final static Header<Date> LAST_MODIFIED = new Header<Date>("Last-Modified", Parser.forDate());
248        public final static Header<CacheControl> CACHE_CONTROL = new Header<CacheControl>("Cache-Control", Parser.forCacheControl());
249        public final static Header<MediaType> CONTENT_TYPE = new Header<MediaType>("Content-Type", Parser.forJaxRsMediaType());
250        public final static Header<Integer> CONTENT_LENGTH = new Header<Integer>("Content-Length", Parser.forInteger());
251        public final static Header<String> ETAG = new Header<String>("ETag", Parser.forETag());
252
253        private final String name;
254        private final Parser<X> parser;
255
256        private Header(final String name, final Parser<X> parser) {
257            this.name = name;
258            this.parser = parser;
259        }
260
261        public String getName() {
262            return name;
263        }
264
265        public X parse(final String value) {
266            return value != null? parser.valueOf(value): null;
267        }
268
269        public String render(X message) {
270            return parser.asString(message);
271        }
272
273        private static Parser<String> warningParser() {
274            return new Parser<String>(){
275                private static final String PREFIX = "199 RestfulObjects ";
276
277                @Override
278                public String valueOf(String str) {
279                    return stripPrefix(str, PREFIX);
280                }
281
282                @Override
283                public String asString(String str) {
284                    return PREFIX + str;
285                }
286                private String stripPrefix(String str, String prefix) {
287                    return str.startsWith(prefix) ? str.substring(prefix.length()) : str;
288                }
289            };
290        }
291
292    }
293
294    private final Response response;
295    private final HttpStatusCode httpStatusCode;
296    private final Class<T> returnType;
297    private T entity;
298
299    @SuppressWarnings({ "rawtypes", "unchecked" })
300    public static RestfulResponse<JsonRepresentation> of(final Response response) {
301        final MediaType jaxRsMediaType = getHeader(response, Header.CONTENT_TYPE);
302        final RepresentationType representationType = RepresentationType.lookup(jaxRsMediaType);
303        final Class<? extends JsonRepresentation> returnType = representationType.getRepresentationClass();
304        return new RestfulResponse(response, returnType);
305    }
306
307    @SuppressWarnings("unchecked")
308    public static <T extends JsonRepresentation> RestfulResponse<T> ofT(final Response response) {
309        return (RestfulResponse<T>) of(response);
310    }
311
312    private RestfulResponse(final Response response, final Class<T> returnType) {
313        this.response = response;
314        this.httpStatusCode = HttpStatusCode.statusFor(response.getStatus());
315        this.returnType = returnType;
316    }
317
318    public HttpStatusCode getStatus() {
319        return httpStatusCode;
320    }
321
322    public T getEntity() throws JsonParseException, JsonMappingException, IOException {
323        if(entity == null) {
324            entity = JsonMapper.instance().read(response, returnType);
325        }
326        return entity;
327    }
328
329    public <V> V getHeader(final Header<V> header) {
330        return getHeader(response, header);
331    }
332
333    private static <V> V getHeader(final Response response, final Header<V> header) {
334        final MultivaluedMap<String, Object> metadata = response.getMetadata();
335        // always returns a String
336        final String value = (String) metadata.getFirst(header.getName());
337        return header.parse(value);
338    }
339
340    /**
341     * Convenience that recasts this response as wrapping some other
342     * representation.
343     * 
344     * <p>
345     * This would typically be as the results of a content type being an
346     * error rather than a representation returned on success.  
347     */
348    @SuppressWarnings("unchecked")
349    public <Q extends JsonRepresentation> RestfulResponse<Q> wraps(Class<Q> cls) {
350        return (RestfulResponse<Q>) this;
351    }
352
353    @Override
354    public String toString() {
355        return "RestfulResponse [httpStatusCode=" + httpStatusCode + "]";
356    }
357
358}