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}