/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.everrest.core.impl;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import org.everrest.core.ExtMultivaluedMap;
import org.everrest.core.impl.header.HeaderHelper;
import org.everrest.core.util.CaselessMultivaluedMap;
import org.everrest.core.util.CaselessStringWrapper;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.RuntimeDelegate;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE;
import static javax.ws.rs.core.HttpHeaders.ALLOW;
import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL;
import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LANGUAGE;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.DATE;
import static javax.ws.rs.core.HttpHeaders.ETAG;
import static javax.ws.rs.core.HttpHeaders.EXPIRES;
import static javax.ws.rs.core.HttpHeaders.LAST_MODIFIED;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static javax.ws.rs.core.HttpHeaders.LOCATION;
import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;
import static javax.ws.rs.core.HttpHeaders.VARY;
import static org.everrest.core.impl.header.HeaderHelper.getHeaderAsString;
/**
* @author andrew00x
*/
public final class ResponseImpl extends Response {
/** HTTP status. */
private final int status;
/** Entity of response */
private final Object entity;
/** Annotations that will be passed to the {@link javax.ws.rs.ext.MessageBodyWriter}. */
private Annotation[] entityAnnotations;
/** HTTP headers. */
private final MultivaluedMap<String, Object> headers;
private boolean closed;
/**
* Construct Response with supplied status, entity and headers.
*
* @param status
* HTTP status
* @param entity
* an entity
* @param headers
* HTTP headers
*/
ResponseImpl(int status, Object entity, Annotation[] entityAnnotations, MultivaluedMap<String, Object> headers) {
this.status = status;
this.entity = entity;
this.entityAnnotations = entityAnnotations;
this.headers = headers;
}
@Override
public Object getEntity() {
checkState(!closed, "Response already closed");
return entity;
}
public Annotation[] getEntityAnnotations() {
return entityAnnotations;
}
@Override
public <T> T readEntity(Class<T> entityType) {
return doReadEntity(entityType, null, null);
}
@SuppressWarnings("unchecked")
@Override
public <T> T readEntity(GenericType<T> entityType) {
return doReadEntity((Class<T>)entityType.getRawType(), entityType.getType(), null);
}
@Override
public <T> T readEntity(Class<T> entityType, Annotation[] annotations) {
return doReadEntity(entityType, null, annotations);
}
@SuppressWarnings("unchecked")
@Override
public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations) {
return doReadEntity((Class<T>)entityType.getRawType(), entityType.getType(), annotations);
}
private <T> T doReadEntity(Class<T> type, Type genericType, Annotation[] annotations) {
checkState(!closed, "Response already closed");
// TODO: implement as part of client implementation
throw new UnsupportedOperationException();
}
@Override
public boolean hasEntity() {
checkState(!closed, "Response already closed");
return entity != null;
}
@Override
public boolean bufferEntity() {
checkState(!closed, "Response already closed");
// TODO: implement as part of client implementation
return false;
}
@Override
public void close() {
this.closed = true;
}
public boolean isClosed() {
return closed;
}
@Override
public MediaType getMediaType() {
Object value = getMetadata().getFirst(CONTENT_TYPE);
if (value == null) {
return null;
}
if (value instanceof MediaType) {
return (MediaType)value;
}
return MediaType.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public Locale getLanguage() {
Object value = getMetadata().getFirst(CONTENT_LANGUAGE);
if (value == null) {
return null;
}
if (value instanceof Locale) {
return (Locale)value;
}
return RuntimeDelegate.getInstance().createHeaderDelegate(Locale.class)
.fromString(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public int getLength() {
Object value = getMetadata().getFirst(CONTENT_LENGTH);
if (value == null) {
return -1;
}
if (value instanceof Integer) {
return (Integer)value;
}
return Integer.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public Set<String> getAllowedMethods() {
List<Object> allowedHeaders = getMetadata().get(ALLOW);
if (allowedHeaders == null) {
return Collections.emptySet();
}
Set<String> allowedMethods = new LinkedHashSet<>();
for (Object allowMethod : allowedHeaders) {
if (allowMethod instanceof String) {
for (String s : ((String)allowMethod).split(",")) {
s = s.trim();
if (!s.isEmpty()) {
allowedMethods.add(s.toUpperCase());
}
}
} else if (allowMethod != null) {
allowedMethods.add(getHeaderAsString(allowMethod).toUpperCase());
}
}
return allowedMethods;
}
@Override
public Map<String, NewCookie> getCookies() {
List<Object> cookieHeaders = getMetadata().get(SET_COOKIE);
if (cookieHeaders == null) {
return Collections.emptyMap();
}
Map<String, NewCookie> cookies = new HashMap<>();
for (Object cookieHeader : cookieHeaders) {
if (cookieHeader instanceof NewCookie) {
NewCookie newCookie = (NewCookie)cookieHeader;
cookies.put(newCookie.getName(), newCookie);
} else if (cookieHeader != null) {
NewCookie newCookie = NewCookie.valueOf(getHeaderAsString(cookieHeader));
if (newCookie != null) {
cookies.put(newCookie.getName(), newCookie);
}
}
}
return cookies;
}
@Override
public EntityTag getEntityTag() {
Object value = getMetadata().getFirst(ETAG);
if (value == null) {
return null;
}
if (value instanceof EntityTag) {
return (EntityTag)value;
}
return EntityTag.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public Date getDate() {
return getDateHeader(DATE);
}
@Override
public Date getLastModified() {
return getDateHeader(LAST_MODIFIED);
}
private Date getDateHeader(String name) {
Object value = getMetadata().getFirst(name);
if (value == null) {
return null;
}
if (value instanceof Date) {
return (Date)value;
}
return HeaderHelper.parseDateHeader(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public URI getLocation() {
Object value = getMetadata().getFirst(LOCATION);
if (value == null) {
return null;
}
if (value instanceof URI) {
return (URI)value;
}
return URI.create(value instanceof String ? (String)value : getHeaderAsString(value));
}
@Override
public Set<Link> getLinks() {
List<Object> links = getMetadata().get(LINK);
if (links == null) {
return Collections.emptySet();
}
Set<Link> linkSet = new LinkedHashSet<>();
for (Object value : links) {
if (value instanceof Link) {
linkSet.add((Link)value);
} else {
linkSet.add(Link.valueOf(value instanceof String ? (String)value : getHeaderAsString(value)));
}
}
return linkSet;
}
@Override
public boolean hasLink(String relation) {
for (Link link : getLinks()) {
if (link.getRels().contains(relation)) {
return true;
}
}
return false;
}
@Override
public Link getLink(String relation) {
for (Link link : getLinks()) {
if (link.getRels().contains(relation)) {
return link;
}
}
return null;
}
@Override
public Link.Builder getLinkBuilder(String relation) {
Link link = getLink(relation);
if (link == null) {
return null;
}
return Link.fromLink(link);
}
@Override
public MultivaluedMap<String, Object> getMetadata() {
return headers;
}
@Override
public MultivaluedMap<String, String> getStringHeaders() {
CaselessMultivaluedMap<String> headerStrings = new CaselessMultivaluedMap<>();
for (Map.Entry<String, List<Object>> entry : getMetadata().entrySet()) {
List<Object> values = entry.getValue();
if (values != null) {
for (Object value : values) {
headerStrings.add(entry.getKey(), getHeaderAsString(value));
}
}
}
return headerStrings;
}
@Override
public String getHeaderString(String name) {
List<Object> headers = getMetadata().get(name);
if (headers == null) {
return null;
}
List<String> headerStrings = headers.stream().map(HeaderHelper::getHeaderAsString).collect(toList());
return HeaderHelper.convertToString(headerStrings);
}
@Override
public int getStatus() {
return status;
}
@Override
public StatusType getStatusInfo() {
final Status statusInstance = Status.fromStatusCode(status);
if (statusInstance != null) {
return statusInstance;
}
return new StatusType() {
@Override
public int getStatusCode() {
return status;
}
@Override
public Status.Family getFamily() {
return Status.Family.familyOf(status);
}
@Override
public String getReasonPhrase() {
return "Unknown";
}
};
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("Status", status)
.add("Content type", getMediaType())
.add("Entity type", entity == null ? null : entity.getClass())
.omitNullValues()
.toString();
}
/** @see ResponseBuilder */
public static final class ResponseBuilderImpl extends ResponseBuilder {
/** HTTP headers which can't be multivalued. */
static final Set<CaselessStringWrapper> SINGLE_VALUE_HEADERS =
newHashSet(new CaselessStringWrapper(CACHE_CONTROL),
new CaselessStringWrapper(CONTENT_LANGUAGE),
new CaselessStringWrapper(CONTENT_LOCATION),
new CaselessStringWrapper(CONTENT_TYPE),
new CaselessStringWrapper(CONTENT_LENGTH),
new CaselessStringWrapper(ETAG),
new CaselessStringWrapper(LAST_MODIFIED),
new CaselessStringWrapper(LOCATION),
new CaselessStringWrapper(EXPIRES));
/** Default HTTP status, No-content, 204. */
private static final int DEFAULT_HTTP_STATUS = Response.Status.NO_CONTENT.getStatusCode();
/** Default HTTP status. */
private int status = DEFAULT_HTTP_STATUS;
/** Entity. Entity will be written as response message body. */
private Object entity;
private Annotation[] entityAnnotations;
/** HTTP headers. */
private final ExtMultivaluedMap<String, Object> headers = new CaselessMultivaluedMap<>();
/** HTTP cookies, Set-Cookie header. */
private final Map<String, NewCookie> cookies = new HashMap<>();
/** See {@link ResponseBuilder}. */
ResponseBuilderImpl() {
}
/**
* Useful for clone method.
*
* @param other
* other ResponseBuilderImpl
* @see #clone()
*/
private ResponseBuilderImpl(ResponseBuilderImpl other) {
this.status = other.status;
this.entity = other.entity;
this.headers.putAll(other.headers);
this.cookies.putAll(other.cookies);
if (other.entityAnnotations != null) {
this.entityAnnotations = new Annotation[other.entityAnnotations.length];
System.arraycopy(other.entityAnnotations, 0, this.entityAnnotations, 0, this.entityAnnotations.length);
}
}
@Override
public Response build() {
MultivaluedMap<String, Object> httpHeaders = new CaselessMultivaluedMap<>(headers);
if (!cookies.isEmpty()) {
for (NewCookie c : cookies.values()) {
httpHeaders.add(SET_COOKIE, c);
}
}
Response response = new ResponseImpl(status, entity, entityAnnotations, httpHeaders);
reset();
return response;
}
/** Set ResponseBuilder to default state. */
private void reset() {
status = DEFAULT_HTTP_STATUS;
entity = null;
entityAnnotations = null;
headers.clear();
cookies.clear();
}
@Override
public ResponseBuilder cacheControl(CacheControl cacheControl) {
if (cacheControl == null) {
headers.remove(CACHE_CONTROL);
} else {
headers.putSingle(CACHE_CONTROL, cacheControl);
}
return this;
}
@Override
public ResponseBuilder encoding(String encoding) {
if (encoding == null) {
headers.remove(CONTENT_ENCODING);
} else {
headers.putSingle(CONTENT_ENCODING, encoding);
}
return this;
}
@Override
public ResponseBuilder clone() {
return new ResponseBuilderImpl(this);
}
@Override
public ResponseBuilder contentLocation(URI location) {
if (location == null) {
headers.remove(CONTENT_LOCATION);
} else {
headers.putSingle(CONTENT_LOCATION, location);
}
return this;
}
@Override
public ResponseBuilder cookie(NewCookie... cookies) {
if (cookies == null) {
this.cookies.clear();
this.headers.remove(SET_COOKIE);
} else {
for (NewCookie cookie : cookies) {
this.cookies.put(cookie.getName(), cookie);
}
}
return this;
}
@Override
public ResponseBuilder entity(Object entity) {
this.entity = entity;
return this;
}
@Override
public ResponseBuilder entity(Object entity, Annotation[] annotations) {
this.entity = entity;
this.entityAnnotations = annotations;
return this;
}
@Override
public ResponseBuilder allow(String... methods) {
if (methods == null) {
headers.remove(ALLOW);
} else {
headers.addAll(ALLOW, methods);
}
return this;
}
@Override
public ResponseBuilder allow(Set<String> methods) {
if (methods == null) {
headers.remove(ALLOW);
} else {
headers.getList(ALLOW).addAll(methods);
}
return this;
}
@Override
public ResponseBuilder expires(Date expires) {
if (expires == null) {
headers.remove(EXPIRES);
} else {
headers.putSingle(EXPIRES, expires);
}
return this;
}
@Override
public ResponseBuilder header(String name, Object value) {
if (value == null) {
headers.remove(name);
} else {
if (SINGLE_VALUE_HEADERS.contains(new CaselessStringWrapper(name))) {
headers.putSingle(name, value);
} else {
headers.add(name, value);
}
}
return this;
}
@Override
public ResponseBuilder replaceAll(MultivaluedMap<String, Object> headers) {
this.headers.clear();
if (headers != null) {
this.headers.putAll(headers);
}
return this;
}
@Override
public ResponseBuilder language(String language) {
if (language == null) {
headers.remove(CONTENT_LANGUAGE);
} else {
headers.putSingle(CONTENT_LANGUAGE, language);
}
return this;
}
@Override
public ResponseBuilder language(Locale language) {
if (language == null) {
headers.remove(CONTENT_LANGUAGE);
} else {
headers.putSingle(CONTENT_LANGUAGE, language);
}
return this;
}
@Override
public ResponseBuilder lastModified(Date lastModified) {
if (lastModified == null) {
headers.remove(LAST_MODIFIED);
} else {
headers.putSingle(LAST_MODIFIED, lastModified);
}
return this;
}
@Override
public ResponseBuilder location(URI location) {
if (location == null) {
headers.remove(LOCATION);
} else {
headers.putSingle(LOCATION, location);
}
return this;
}
@Override
public ResponseBuilder status(int status) {
this.status = status;
return this;
}
@Override
public ResponseBuilder tag(EntityTag tag) {
if (tag == null) {
headers.remove(ETAG);
} else {
headers.putSingle(ETAG, tag);
}
return this;
}
@Override
public ResponseBuilder tag(String tag) {
if (tag == null) {
headers.remove(ETAG);
} else {
headers.putSingle(ETAG, tag);
}
return this;
}
@Override
public ResponseBuilder type(MediaType type) {
if (type == null) {
headers.remove(CONTENT_TYPE);
} else {
headers.putSingle(CONTENT_TYPE, type);
}
return this;
}
@Override
public ResponseBuilder type(String type) {
if (type == null) {
headers.remove(CONTENT_TYPE);
} else {
headers.putSingle(CONTENT_TYPE, type);
}
return this;
}
@Override
public ResponseBuilder variant(Variant variant) {
if (variant == null) {
type((String)null);
language((String)null);
encoding(null);
} else {
type(variant.getMediaType());
language(variant.getLanguage());
encoding(variant.getEncoding());
}
return this;
}
@Override
public ResponseBuilder variants(Variant... variants) {
return variants(variants == null ? null : Arrays.asList(variants));
}
@Override
public ResponseBuilder variants(List<Variant> variants) {
if (variants == null) {
headers.remove(VARY);
return this;
}
if (variants.isEmpty()) {
return this;
}
boolean acceptMediaType = variants.get(0).getMediaType() != null;
boolean acceptLanguage = variants.get(0).getLanguage() != null;
boolean acceptEncoding = variants.get(0).getEncoding() != null;
for (Variant variant : variants) {
acceptMediaType |= variant.getMediaType() != null;
acceptLanguage |= variant.getLanguage() != null;
acceptEncoding |= variant.getEncoding() != null;
}
List<String> varyHeader = new ArrayList<>();
if (acceptMediaType) {
varyHeader.add(ACCEPT);
}
if (acceptLanguage) {
varyHeader.add(ACCEPT_LANGUAGE);
}
if (acceptEncoding) {
varyHeader.add(ACCEPT_ENCODING);
}
if (varyHeader.size() > 0) {
header(VARY, Joiner.on(',').join(varyHeader));
}
return this;
}
@Override
public ResponseBuilder links(Link... links) {
if (links == null) {
headers.remove(LINK);
} else {
headers.addAll(LINK, links);
}
return this;
}
@Override
public ResponseBuilder link(URI uri, String rel) {
headers.getList(LINK).add(Link.fromUri(uri).rel(rel).build());
return this;
}
@Override
public ResponseBuilder link(String uri, String rel) {
return link(URI.create(uri), rel);
}
}
}