/*******************************************************************************
* 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.MoreObjects;
import com.google.common.base.Strings;
import org.everrest.core.ApplicationContext;
import org.everrest.core.GenericContainerRequest;
import org.everrest.core.impl.header.AcceptLanguage;
import org.everrest.core.impl.header.AcceptMediaType;
import org.everrest.core.impl.header.HeaderHelper;
import org.everrest.core.impl.header.MediaTypeHelper;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.RuntimeDelegate;
import java.io.InputStream;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.HEAD;
import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED;
import static org.everrest.core.impl.header.HeaderHelper.convertToString;
import static org.everrest.core.impl.header.HeaderHelper.createAcceptMediaTypeList;
import static org.everrest.core.impl.header.HeaderHelper.createAcceptedLanguageList;
/**
* @author andrew00x
*/
public class ContainerRequest implements GenericContainerRequest {
/** HTTP method. */
private String method;
/** HTTP request message body as stream. */
private InputStream entityStream;
/** HTTP headers. */
private MultivaluedMap<String, String> httpHeaders;
/** Parsed HTTP cookies. */
private Map<String, Cookie> cookies;
/** Source strings of HTTP cookies. */
private List<String> cookieHeaders;
/** HTTP header Content-Type. */
private MediaType contentType;
/** HTTP header Content-Language. */
private Locale contentLanguage;
/** List of accepted media type, HTTP header Accept. List is sorted by quality value factor. */
private List<AcceptMediaType> acceptableMediaTypes;
/** List of accepted language, HTTP header Accept-Language. List is sorted by quality value factor. */
private List<Locale> acceptLanguages;
/** Full request URI, includes query string and fragment. */
private URI requestUri;
/** Base URI, e.g. servlet path. */
private URI baseUri;
/** Security context. */
private SecurityContext securityContext;
private VariantsHandler variantsHandler = new VariantsHandler();
/**
* Constructs new instance of ContainerRequest.
*
* @param method
* HTTP method
* @param requestUri
* full request URI
* @param baseUri
* base request URI
* @param entityStream
* request message body as stream
* @param httpHeaders
* HTTP headers
* @param securityContext
* SecurityContext
*/
public ContainerRequest(String method, URI requestUri, URI baseUri, InputStream entityStream,
MultivaluedMap<String, String> httpHeaders, SecurityContext securityContext) {
this.method = method;
this.requestUri = requestUri;
this.baseUri = baseUri;
this.entityStream = entityStream;
this.httpHeaders = httpHeaders;
this.securityContext = securityContext;
}
@Override
public MediaType getAcceptableMediaType(List<MediaType> mediaTypes) {
for (MediaType acceptMediaType : getAcceptableMediaTypes()) {
for (MediaType checkMediaType : mediaTypes) {
if (MediaTypeHelper.isMatched(acceptMediaType, checkMediaType)) {
return checkMediaType;
}
}
}
return null;
}
@Override
public List<String> getCookieHeaders() {
if (cookieHeaders == null) {
List<String> cookieHeaders = getRequestHeader(COOKIE);
if (cookieHeaders == null || cookieHeaders.isEmpty()) {
this.cookieHeaders = emptyList();
} else {
this.cookieHeaders = unmodifiableList(cookieHeaders);
}
}
return cookieHeaders;
}
@Override
public InputStream getEntityStream() {
return entityStream;
}
@Override
public URI getRequestUri() {
return requestUri;
}
@Override
public URI getBaseUri() {
return baseUri;
}
@Override
public String getMethod() {
return method;
}
@Override
public void setMethod(String method) {
this.method = method;
}
@Override
public void setEntityStream(InputStream entityStream) {
this.entityStream = entityStream;
ApplicationContext context = ApplicationContext.getCurrent();
context.getAttributes().remove("org.everrest.provider.entity.decoded.form");
context.getAttributes().remove("org.everrest.provider.entity.encoded.form");
}
@Override
public void setUris(URI requestUri, URI baseUri) {
this.requestUri = requestUri;
this.baseUri = baseUri;
}
@Override
public void setCookieHeaders(List<String> cookieHeaders) {
this.cookieHeaders = cookieHeaders;
this.cookies = null;
}
@Override
public void setRequestHeaders(MultivaluedMap<String, String> httpHeaders) {
this.httpHeaders = httpHeaders;
this.cookieHeaders = null;
this.cookies = null;
this.contentType = null;
this.contentLanguage = null;
this.acceptableMediaTypes = null;
this.acceptLanguages = null;
}
@Override
public String getAuthenticationScheme() {
return securityContext.getAuthenticationScheme();
}
@Override
public Principal getUserPrincipal() {
return securityContext.getUserPrincipal();
}
@Override
public boolean isSecure() {
return securityContext.isSecure();
}
@Override
public boolean isUserInRole(String role) {
return securityContext.isUserInRole(role);
}
@Override
public ResponseBuilder evaluatePreconditions(EntityTag etag) {
checkArgument(etag != null, "Null ETag is not supported");
ResponseBuilder responseBuilder = evaluateIfMatch(etag);
if (responseBuilder == null) {
responseBuilder = evaluateIfNoneMatch(etag);
}
return responseBuilder;
}
@Override
public ResponseBuilder evaluatePreconditions(Date lastModified) {
checkArgument(lastModified != null, "Null last modification date is not supported");
long lastModifiedTime = lastModified.getTime();
ResponseBuilder responseBuilder = evaluateIfModified(lastModifiedTime);
if (responseBuilder == null) {
responseBuilder = evaluateIfUnmodified(lastModifiedTime);
}
return responseBuilder;
}
@Override
public ResponseBuilder evaluatePreconditions(Date lastModified, EntityTag etag) {
checkArgument(lastModified != null, "Null last modification date is not supported");
checkArgument(etag != null, "Null ETag is not supported");
ResponseBuilder responseBuilder = evaluateIfMatch(etag);
if (responseBuilder != null) {
return responseBuilder;
}
long lastModifiedTime = lastModified.getTime();
responseBuilder = evaluateIfModified(lastModifiedTime);
if (responseBuilder != null) {
return responseBuilder;
}
responseBuilder = evaluateIfNoneMatch(etag);
if (responseBuilder != null) {
return responseBuilder;
}
return evaluateIfUnmodified(lastModifiedTime);
}
@Override
public ResponseBuilder evaluatePreconditions() {
List<String> ifMatch = getRequestHeader(IF_MATCH);
return (ifMatch == null || ifMatch.isEmpty()) ? null : Response.status(PRECONDITION_FAILED);
}
@Override
public Variant selectVariant(List<Variant> variants) {
checkArgument(!(variants == null || variants.isEmpty()), "The list of variants is null or empty");
return variantsHandler.handleVariants(this, variants);
}
/**
* If accept-language header does not present or its length is null then default language list will be returned. This list contains
* only one element Locale with language '*', and it minds any language accepted.
*/
@Override
public List<Locale> getAcceptableLanguages() {
if (acceptLanguages == null) {
List<AcceptLanguage> acceptLanguages = createAcceptedLanguageList(convertToString(getRequestHeader(ACCEPT_LANGUAGE)));
List<Locale> locales = new ArrayList<>(acceptLanguages.size());
locales.addAll(acceptLanguages.stream().map(language -> language.getLanguage().getLocale()).collect(toList()));
this.acceptLanguages = unmodifiableList(locales);
}
return acceptLanguages;
}
@Override
public List<MediaType> getAcceptableMediaTypes() {
return getAcceptMediaTypeList().stream().map(AcceptMediaType::getMediaType).collect(toList());
}
@Override
public List<AcceptMediaType> getAcceptMediaTypeList() {
if (acceptableMediaTypes == null) {
acceptableMediaTypes = createAcceptMediaTypeList(convertToString(getRequestHeader(ACCEPT)));
}
return acceptableMediaTypes;
}
@Override
public Map<String, Cookie> getCookies() {
if (this.cookies == null) {
Map<String, Cookie> cookies = new HashMap<>();
for (String cookieHeader : getCookieHeaders()) {
List<Cookie> parsedCookies = HeaderHelper.parseCookies(cookieHeader);
for (Cookie cookie : parsedCookies) {
cookies.put(cookie.getName(), cookie);
}
}
this.cookies = unmodifiableMap(cookies);
}
return cookies;
}
@Override
public Date getDate() {
String date = getRequestHeaders().getFirst(DATE);
return date == null ? null : HeaderHelper.parseDateHeader(date);
}
@Override
public int getLength() {
String length = getRequestHeaders().getFirst(CONTENT_LENGTH);
return length == null ? -1 : Integer.parseInt(length);
}
@Override
public Locale getLanguage() {
if (contentLanguage == null && httpHeaders.getFirst(CONTENT_LANGUAGE) != null) {
contentLanguage = RuntimeDelegate.getInstance().createHeaderDelegate(Locale.class).fromString(httpHeaders.getFirst(CONTENT_LANGUAGE));
}
return contentLanguage;
}
@Override
public MediaType getMediaType() {
if (contentType == null && httpHeaders.getFirst(CONTENT_TYPE) != null) {
contentType = MediaType.valueOf(httpHeaders.getFirst(CONTENT_TYPE));
}
return contentType;
}
@Override
public List<String> getRequestHeader(String name) {
return httpHeaders.get(name);
}
@Override
public String getHeaderString(String name) {
return convertToString(getRequestHeader(name));
}
@Override
public MultivaluedMap<String, String> getRequestHeaders() {
return httpHeaders;
}
/**
* Comparison for If-Match header and ETag.
*
* @param etag
* the ETag
* @return ResponseBuilder with status 412 (precondition failed) if If-Match header does NOT MATCH to ETag or null otherwise
*/
private ResponseBuilder evaluateIfMatch(EntityTag etag) {
String ifMatch = getRequestHeaders().getFirst(IF_MATCH);
if (isNullOrEmpty(ifMatch)) {
return null;
}
EntityTag otherEtag = EntityTag.valueOf(ifMatch);
if (eTagsStrongEqual(etag, otherEtag)) {
return null;
}
return Response.status(PRECONDITION_FAILED);
}
/**
* Comparison for If-None-Match header and ETag.
*
* @param etag
* the ETag
* @return ResponseBuilder with status 412 (precondition failed) if If-None-Match header is MATCH to ETag and HTTP method is not GET or
* HEAD. If method is GET or HEAD and If-None-Match is MATCH to ETag then ResponseBuilder with status 304 (not modified) will be
* returned.
*/
private ResponseBuilder evaluateIfNoneMatch(EntityTag etag) {
String ifNoneMatch = getRequestHeaders().getFirst(IF_NONE_MATCH);
if (Strings.isNullOrEmpty(ifNoneMatch)) {
return null;
}
EntityTag otherEtag = EntityTag.valueOf(ifNoneMatch);
String httpMethod = getMethod();
if (httpMethod.equals(GET) || httpMethod.equals(HEAD)) {
if (eTagsWeakEqual(etag, otherEtag)) {
return Response.notModified(etag);
}
} else {
if (eTagsStrongEqual(etag, otherEtag)) {
return Response.status(PRECONDITION_FAILED);
}
}
return null;
}
private boolean eTagsStrongEqual(EntityTag etag, EntityTag otherEtag) {
// Strong comparison is required.
// From specification:
// The strong comparison function: in order to be considered equal,
// both validators MUST be identical in every way, and both MUST NOT be weak.
return !etag.isWeak() && !otherEtag.isWeak()
&& ("*".equals(otherEtag.getValue()) || etag.getValue().equals(otherEtag.getValue()));
}
private boolean eTagsWeakEqual(EntityTag etag, EntityTag otherEtag) {
return "*".equals(otherEtag.getValue()) || etag.getValue().equals(otherEtag.getValue());
}
/**
* Comparison for lastModified and unmodifiedSince times.
*
* @param lastModified
* the last modified time
* @return ResponseBuilder with status 412 (precondition failed) if lastModified time is greater then unmodifiedSince otherwise return
* null. If date format in header If-Unmodified-Since is wrong also null returned
*/
private ResponseBuilder evaluateIfUnmodified(long lastModified) {
String ifUnmodified = getRequestHeaders().getFirst(IF_UNMODIFIED_SINCE);
if (isNullOrEmpty(ifUnmodified)) {
return null;
}
try {
long unmodifiedSince = HeaderHelper.parseDateHeader(ifUnmodified).getTime();
if (lastModified > unmodifiedSince) {
return Response.status(PRECONDITION_FAILED);
}
} catch (IllegalArgumentException ignored) {
}
return null;
}
/**
* Comparison for lastModified and modifiedSince times.
*
* @param lastModified
* the last modified time
* @return ResponseBuilder with status 304 (not modified) if lastModified time is greater then modifiedSince otherwise return null. If
* date format in header If-Modified-Since is wrong also null returned
*/
private ResponseBuilder evaluateIfModified(long lastModified) {
String ifModified = getRequestHeaders().getFirst(IF_MODIFIED_SINCE);
if (isNullOrEmpty(ifModified)) {
return null;
}
try {
long modifiedSince = HeaderHelper.parseDateHeader(ifModified).getTime();
if (lastModified <= modifiedSince) {
return Response.notModified();
}
} catch (IllegalArgumentException ignored) {
}
return null;
}
void setVariantsHandler(VariantsHandler variantsHandler) {
this.variantsHandler = variantsHandler;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("Method", method)
.add("BaseUri", baseUri)
.add("RequestUri", requestUri)
.toString();
}
}