/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.isis.viewer.restfulobjects.applib.client;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.Map;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.Response.StatusType;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Maps;
import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
import org.apache.isis.viewer.restfulobjects.applib.util.Parser;
public class RestfulResponse<T> {
public final static class HttpStatusCode {
private final static Map<Status, HttpStatusCode> statii = Maps.newHashMap();
private final static Map<Integer, HttpStatusCode> statusCodes = Maps.newHashMap();
private static class StatusTypeImpl implements StatusType {
private final int statusCode;
private final Family family;
private final String reasonPhrase;
private StatusTypeImpl(final int statusCode, final Family family, final String reasonPhrase) {
this.statusCode = statusCode;
this.family = family;
this.reasonPhrase = reasonPhrase;
}
@Override
public int getStatusCode() {
return statusCode;
}
@Override
public Family getFamily() {
return family;
}
@Override
public String getReasonPhrase() {
return reasonPhrase;
}
}
public static HttpStatusCode lookup(final int status) {
return statusCodes.get(status);
}
public static Family lookupFamily(final int statusCode) {
switch (statusCode / 100) {
case 1:
return Family.INFORMATIONAL;
case 2:
return Family.SUCCESSFUL;
case 3:
return Family.REDIRECTION;
case 4:
return Family.CLIENT_ERROR;
case 5:
return Family.SERVER_ERROR;
default:
return Family.OTHER;
}
}
// public static final int SC_CONTINUE = 100;
// public static final int SC_SWITCHING_PROTOCOLS = 101;
// public static final int SC_PROCESSING = 102;
public final static HttpStatusCode OK = new HttpStatusCode(200, Status.OK);
public final static HttpStatusCode CREATED = new HttpStatusCode(201, Status.CREATED);
// public static final int SC_ACCEPTED = 202;
// public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
public static final HttpStatusCode NO_CONTENT = new HttpStatusCode(204, Status.NO_CONTENT);
// public static final int SC_RESET_CONTENT = 205;
// public static final int SC_PARTIAL_CONTENT = 206;
// public static final int SC_MULTI_STATUS = 207;
// public static final int SC_MULTIPLE_CHOICES = 300;
// public static final int SC_MOVED_PERMANENTLY = 301;
// public static final int SC_MOVED_TEMPORARILY = 302;
// public static final int SC_SEE_OTHER = 303;
public final static HttpStatusCode NOT_MODIFIED = new HttpStatusCode(304, Status.BAD_REQUEST);
// public static final int SC_NOT_MODIFIED = 304;
// public static final int SC_USE_PROXY = 305;
// public static final int SC_TEMPORARY_REDIRECT = 307;
public final static HttpStatusCode BAD_REQUEST = new HttpStatusCode(400, Status.BAD_REQUEST);
public final static HttpStatusCode UNAUTHORIZED = new HttpStatusCode(401, Status.UNAUTHORIZED);
// public static final int SC_PAYMENT_REQUIRED = 402;
public static final HttpStatusCode FORBIDDEN = new HttpStatusCode(403, Status.FORBIDDEN);
public final static HttpStatusCode NOT_FOUND = new HttpStatusCode(404, Status.NOT_FOUND);
public final static HttpStatusCode METHOD_NOT_ALLOWED = new HttpStatusCode(405, new StatusTypeImpl(405, Family.CLIENT_ERROR, "Method not allowed"));
public final static HttpStatusCode NOT_ACCEPTABLE = new HttpStatusCode(406, Status.NOT_ACCEPTABLE);
// public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
// public static final int SC_REQUEST_TIMEOUT = 408;
public final static HttpStatusCode CONFLICT = new HttpStatusCode(409, Status.CONFLICT);
// public static final int SC_GONE = 410;
// public static final int SC_LENGTH_REQUIRED = 411;
// public static final int SC_PRECONDITION_FAILED = 412;
// public static final int SC_REQUEST_TOO_LONG = 413;
// public static final int SC_REQUEST_URI_TOO_LONG = 414;
// public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
public final static HttpStatusCode UNSUPPORTED_MEDIA_TYPE = new HttpStatusCode(415, Status.UNSUPPORTED_MEDIA_TYPE);
// public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
// public static final int SC_EXPECTATION_FAILED = 417;
// public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
public final static HttpStatusCode METHOD_FAILURE = new HttpStatusCode(420, new StatusTypeImpl(420, Family.CLIENT_ERROR, "Method failure"));
// public static final int SC_UNPROCESSABLE_ENTITY = 422;
public final static HttpStatusCode VALIDATION_FAILED = new HttpStatusCode(422, new StatusTypeImpl(422, Family.CLIENT_ERROR, "Validation failed"));
// public static final int SC_LOCKED = 423;
// public static final int SC_FAILED_DEPENDENCY = 424;
public final static HttpStatusCode PRECONDITION_HEADER_MISSING = new HttpStatusCode(428, new StatusTypeImpl(428, Family.CLIENT_ERROR, "Precondition header missing"));
public final static HttpStatusCode INTERNAL_SERVER_ERROR = new HttpStatusCode(500, Status.INTERNAL_SERVER_ERROR);
public final static HttpStatusCode NOT_IMPLEMENTED = new HttpStatusCode(501, new StatusTypeImpl(501, Family.SERVER_ERROR, "Not implemented"));
// public static final int SC_BAD_GATEWAY = 502;
// public static final int SC_SERVICE_UNAVAILABLE = 503;
// public static final int SC_GATEWAY_TIMEOUT = 504;
// public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
// public static final int SC_INSUFFICIENT_STORAGE = 507;
public final static HttpStatusCode statusFor(final int statusCode) {
final HttpStatusCode httpStatusCode = statusCodes.get(statusCode);
if (httpStatusCode != null) {
return httpStatusCode;
}
return statusForSynchronized(statusCode);
}
public final static HttpStatusCode statusFor(final Status status) {
return statii.get(status);
}
private final static synchronized HttpStatusCode statusForSynchronized(final int statusCode) {
HttpStatusCode httpStatusCode = statusCodes.get(statusCode);
if (httpStatusCode != null) {
return httpStatusCode;
}
httpStatusCode = new HttpStatusCode(statusCode, null);
statusCodes.put(statusCode, httpStatusCode);
return httpStatusCode;
}
private final int statusCode;
private final Family family;
private final StatusType jaxrsStatusType;
private HttpStatusCode(final int statusCode, final StatusType status) {
this.statusCode = statusCode;
this.jaxrsStatusType = status;
family = lookupFamily(statusCode);
statusCodes.put(statusCode, this);
}
public int getStatusCode() {
return statusCode;
}
public StatusType getJaxrsStatusType() {
return jaxrsStatusType;
}
public Family getFamily() {
return family;
}
@Override
public int hashCode() {
return statusCode;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final HttpStatusCode other = (HttpStatusCode) obj;
if (statusCode != other.statusCode) {
return false;
}
return true;
}
@Override
public String toString() {
return "HttpStatusCode " + statusCode + ", " + family;
}
}
public static class Header<X> {
public final static Header<String> WARNING = new Header<String>("Warning", warningParser());
public final static Header<Date> LAST_MODIFIED = new Header<Date>("Last-Modified", Parser.forDate());
public final static Header<CacheControl> CACHE_CONTROL = new Header<CacheControl>("Cache-Control", Parser.forCacheControl());
public final static Header<MediaType> CONTENT_TYPE = new Header<MediaType>("Content-Type", Parser.forJaxRsMediaType());
public final static Header<Integer> CONTENT_LENGTH = new Header<Integer>("Content-Length", Parser.forInteger());
public final static Header<String> ETAG = new Header<String>("ETag", Parser.forETag());
private final String name;
private final Parser<X> parser;
private Header(final String name, final Parser<X> parser) {
this.name = name;
this.parser = parser;
}
public String getName() {
return name;
}
public X parse(final String value) {
return value != null? parser.valueOf(value): null;
}
public String render(X message) {
return parser.asString(message);
}
private static Parser<String> warningParser() {
return new Parser<String>(){
private static final String PREFIX = "199 RestfulObjects ";
@Override
public String valueOf(String str) {
return stripPrefix(str, PREFIX);
}
@Override
public String asString(String str) {
return PREFIX + str;
}
private String stripPrefix(String str, String prefix) {
return str.startsWith(prefix) ? str.substring(prefix.length()) : str;
}
};
}
}
private final Response response;
private final HttpStatusCode httpStatusCode;
private final Class<T> returnType;
private T entity;
@SuppressWarnings({ "rawtypes", "unchecked" })
public static RestfulResponse<JsonRepresentation> of(final Response response) {
final MediaType jaxRsMediaType = getHeader(response, Header.CONTENT_TYPE);
final RepresentationType representationType = RepresentationType.lookup(jaxRsMediaType);
final Class<? extends JsonRepresentation> returnType = representationType.getRepresentationClass();
return new RestfulResponse(response, returnType);
}
@SuppressWarnings("unchecked")
public static <T extends JsonRepresentation> RestfulResponse<T> ofT(final Response response) {
return (RestfulResponse<T>) of(response);
}
private RestfulResponse(final Response response, final Class<T> returnType) {
this.response = response;
this.httpStatusCode = HttpStatusCode.statusFor(response.getStatus());
this.returnType = returnType;
}
public HttpStatusCode getStatus() {
return httpStatusCode;
}
public T getEntity() throws JsonParseException, JsonMappingException, IOException {
if(entity == null) {
// previously this was good enough, but no longer it seems
//entity = JsonMapper.instance().read(response, returnType);
// instead, we do it manually
final JsonNode jsonNode = JsonMapper.instance().read(response, JsonNode.class);
try {
final Constructor<T> constructor = returnType.getConstructor(JsonNode.class);
entity = constructor.newInstance(jsonNode);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
return entity;
}
public <V> V getHeader(final Header<V> header) {
return getHeader(response, header);
}
private static <V> V getHeader(final Response response, final Header<V> header) {
final MultivaluedMap<String, Object> metadata = response.getMetadata();
// always returns a String
final String value = (String) metadata.getFirst(header.getName());
return header.parse(value);
}
/**
* Convenience that recasts this response as wrapping some other
* representation.
*
* <p>
* This would typically be as the results of a content type being an
* error rather than a representation returned on success.
*/
@SuppressWarnings("unchecked")
public <Q extends JsonRepresentation> RestfulResponse<Q> wraps(Class<Q> cls) {
return (RestfulResponse<Q>) this;
}
@Override
public String toString() {
return "RestfulResponse [httpStatusCode=" + httpStatusCode + "]";
}
}