/*******************************************************************************
* 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.header;
import com.google.common.base.Joiner;
import org.everrest.core.header.QualityValue;
import org.everrest.core.impl.header.ListHeaderProducer.ListItemFactory;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.ext.RuntimeDelegate;
import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Strings.isNullOrEmpty;
import static javax.ws.rs.core.MediaType.WILDCARD;
import static org.everrest.core.impl.header.CookieBuilder.aCookie;
import static org.everrest.core.impl.header.NewCookieBuilder.aNewCookie;
import static org.everrest.core.util.StringUtils.charAtIs;
import static org.everrest.core.util.StringUtils.doesNotContain;
import static org.everrest.core.util.StringUtils.scan;
public class HeaderHelper {
private HeaderHelper() {
}
/** Pattern for search whitespace and quote in string. */
private static final Pattern WHITESPACE_QUOTE_PATTERN = Pattern.compile("[\\s\"]");
/** Pattern for search whitespace in string. */
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
/** Header separators. Header token MUST NOT contains any of it. */
private static final boolean[] SEPARATORS = new boolean[128];
static {
for (char c : "()<>@,;:\"\\/[]?={}".toCharArray()) {
SEPARATORS[c] = true;
}
}
/** Accept all media type list. */
private static final List<AcceptMediaType> ACCEPT_ALL_MEDIA_TYPE = Collections.singletonList(AcceptMediaType.DEFAULT);
/** Accept all languages list. */
private static final List<AcceptLanguage> ACCEPT_ALL_LANGUAGE = Collections.singletonList(AcceptLanguage.DEFAULT);
/** Accept all tokens list. */
private static final List<AcceptToken> ACCEPT_ALL_TOKENS = Collections.singletonList(new AcceptToken("*"));
//
/**
* Comparator for tokens which have quality value.
*
* @see QualityValue
*/
public static final Comparator<QualityValue> QUALITY_VALUE_COMPARATOR = new Comparator<QualityValue>() {
@Override
public int compare(QualityValue qualityValueOne, QualityValue qualityValueTwo) {
float q1 = qualityValueOne.getQvalue();
float q2 = qualityValueTwo.getQvalue();
if (q1 < q2) {
return 1;
}
if (q1 > q2) {
return -1;
}
return 0;
}
};
// accept headers
/**
* Accept media type producer.
*
* @see ListHeaderProducer
*/
private static final ListHeaderProducer<AcceptMediaType> LIST_MEDIA_TYPE_PRODUCER = new ListHeaderProducer<>(new AcceptMediaTypeFactory());
/**
* Creates sorted by quality value accepted media type list.
*
* @param header
* source header string
* @return List of AcceptMediaType
*/
public static List<AcceptMediaType> createAcceptMediaTypeList(String header) {
if (isNullOrEmpty(header) || WILDCARD.equals(header.trim())) {
return ACCEPT_ALL_MEDIA_TYPE;
}
return LIST_MEDIA_TYPE_PRODUCER.createQualitySortedList(header);
}
/**
* Accept language producer.
*
* @see ListHeaderProducer
*/
private static final ListHeaderProducer<AcceptLanguage> LIST_LANGUAGE_PRODUCER = new ListHeaderProducer<>(new AcceptLanguageFactory());
/**
* Creates sorted by quality value accepted language list.
*
* @param header
* source header string
* @return List of AcceptLanguage
*/
public static List<AcceptLanguage> createAcceptedLanguageList(String header) {
if (isNullOrEmpty(header) || "*".equals(header)) {
return ACCEPT_ALL_LANGUAGE;
}
return LIST_LANGUAGE_PRODUCER.createQualitySortedList(header);
}
/**
* Accept token producer. Useful for processing 'accept-charset' and 'accept-encoding' request headers.
*
* @see ListHeaderProducer
*/
private static final ListHeaderProducer<AcceptToken> LIST_TOKEN_PRODUCER = new ListHeaderProducer<>(new AcceptTokenFactory());
/**
* Creates sorted by quality value 'accept-character' list.
*
* @param header
* source header string
* @return List of accept charset tokens
*/
public static List<AcceptToken> createAcceptedCharsetList(String header) {
if (isNullOrEmpty(header) || "*".equals(header)) {
return ACCEPT_ALL_TOKENS;
}
return LIST_TOKEN_PRODUCER.createQualitySortedList(header);
}
/**
* Creates sorted by quality value 'accept-encoding' list.
*
* @param header
* source header string
* @return List of accept encoding tokens
*/
public static List<AcceptToken> createAcceptedEncodingList(String header) {
if (isNullOrEmpty(header) || "*".equals(header)) {
return ACCEPT_ALL_TOKENS;
}
return LIST_TOKEN_PRODUCER.createQualitySortedList(header);
}
/**
* Parses cookie header string and create collection of cookie from it.
*
* @param cookiesString
* the cookie string.
* @return collection of Cookie.
*/
public static List<Cookie> parseCookies(String cookiesString) {
final List<Cookie> cookies = new ArrayList<>();
int p = 0;
int n;
CookieBuilder cookieBuilder = null;
int version = Cookie.DEFAULT_VERSION;
while (p < cookiesString.length()) {
n = findCookieParameterSeparator(cookiesString, p);
String pair = cookiesString.substring(p, n);
String name;
String value = "";
int eq = scan(pair, '=');
if (charAtIs(pair, eq, '=')) {
name = pair.substring(0, eq).trim();
value = pair.substring(eq + 1).trim();
if (value.length() > 1 && value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
} else {
name = pair.trim();
}
// Name of parameter not start from '$', then it is cookie name and value.
// In header string name/value pair (name without '$') SHOULD be before
// '$Path' and '$Domain' parameters, but '$Version' goes before name/value pair.
if (doesNotContain(name, '$')) {
if (cookieBuilder != null) {
cookies.add(cookieBuilder.build());
}
cookieBuilder = aCookie().withName(name).withValue(value);
// version was kept before http://www.ietf.org/rfc/rfc2109.txt section 4.4
cookieBuilder.withVersion(version);
} else if ("$Version".equalsIgnoreCase(name)) {
version = Integer.valueOf(value);
} else if (cookieBuilder != null && "$Path".equalsIgnoreCase(name)) {
cookieBuilder.withPath(value);
} else if (cookieBuilder != null && "$Domain".equalsIgnoreCase(name)) {
cookieBuilder.withDomain(value);
}
p = n + 1;
}
if (cookieBuilder != null) {
cookies.add(cookieBuilder.build());
}
return cookies;
}
/**
* Parses cookie header string and create collection of NewCookie from it.
*
* @param newCookieString
* the new cookie string.
* @return collection of NewCookie.
*/
public static NewCookie parseNewCookie(String newCookieString) {
int p = 0;
int n = findCookieParameterSeparator(newCookieString, p);
int separator = -1;
if (n > 0 && n < newCookieString.length()) {
separator = newCookieString.charAt(n);
}
NewCookieBuilder newCookieBuilder = null;
while (p < newCookieString.length()) {
String pair = newCookieString.substring(p, n);
String name;
String value = "";
int eq = scan(pair, '=');
if (charAtIs(pair, eq, '=')) {
name = pair.substring(0, eq).trim();
value = pair.substring(eq + 1).trim();
if (value.length() > 1 && value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
} else {
name = pair.trim();
}
if (newCookieBuilder == null) {
newCookieBuilder = aNewCookie().withName(name).withValue(value).withVersion(Cookie.DEFAULT_VERSION);
} else {
if (name.equalsIgnoreCase("version")) {
newCookieBuilder.withVersion(Integer.parseInt(value));
} else if (name.equalsIgnoreCase("domain")) {
newCookieBuilder.withDomain(value);
} else if (name.equalsIgnoreCase("path")) {
newCookieBuilder.withPath(value);
} else if (name.equalsIgnoreCase("secure")) {
newCookieBuilder.withSecure(true);
} else if (name.equalsIgnoreCase("HttpOnly")) {
newCookieBuilder.withHttpOnly(true);
} else if (name.equalsIgnoreCase("Max-Age")) {
newCookieBuilder.withMaxAge(Integer.parseInt(value));
} else if (name.equalsIgnoreCase("expires")) {
try {
newCookieBuilder.withExpiry(parseDateHeader(value));
} catch (IllegalArgumentException ignored) {
ignored.printStackTrace();
}
} else if (name.equalsIgnoreCase("comment")) {
newCookieBuilder.withComment(value);
}
}
if (separator == -1) {
break;
}
p = n + 1;
n = scan(newCookieString, p, (char)separator);
}
if (newCookieBuilder == null) {
return null;
}
return newCookieBuilder.build();
}
// Date
// HTTP applications have historically allowed three different formats
// for the representation of date/time stamps
// For example :
// Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
// Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
private static class DateFormats {
/** RFC 822, updated by RFC 1123. */
static final String RFC_1123_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
/** RFC 850, obsoleted by RFC 1036. */
static final String RFC_1036_DATE_FORMAT = "EEEE, dd-MMM-yy HH:mm:ss zzz";
/** ANSI C's asctime() format. */
static final String ANSI_C_DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy";
static final SimpleDateFormat[] formats = createFormats();
private static SimpleDateFormat[] createFormats() {
SimpleDateFormat[] formats = new SimpleDateFormat[3];
formats[0] = new SimpleDateFormat(RFC_1123_DATE_FORMAT, Locale.US);
formats[1] = new SimpleDateFormat(RFC_1036_DATE_FORMAT, Locale.US);
formats[2] = new SimpleDateFormat(ANSI_C_DATE_FORMAT, Locale.US);
TimeZone tz = TimeZone.getTimeZone("GMT");
formats[0].setTimeZone(tz);
formats[1].setTimeZone(tz);
formats[2].setTimeZone(tz);
return formats;
}
}
/**
* Parses date header. Will try to found appropriated format for given date header. Format can be one of described in
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1" >HTTP/1.1 documentation</a> .
*
* @param header
* source date header
* @return parsed Date
*/
public static Date parseDateHeader(String header) {
for (SimpleDateFormat format : DateFormats.formats) {
try {
return ((SimpleDateFormat)format.clone()).parse(header);
} catch (ParseException ignored) {
}
}
throw new IllegalArgumentException(String.format("Not found appropriated date format for %s", header));
}
/**
* Formats {@code date} in RFC 1123 format.
*
* @param date
* date
* @return string in RFC 1123 format
*/
public static String formatDate(Date date) {
return ((DateFormat)DateFormats.formats[0].clone()).format(date);
}
//
public static long getContentLengthLong(MultivaluedMap<String, String> httpHeaders) {
String contentLengthHeader = httpHeaders.getFirst(HttpHeaders.CONTENT_LENGTH);
return contentLengthHeader == null ? 0 : Long.parseLong(contentLengthHeader);
}
/**
* Creates string representation of given object for adding to response header. Method uses {@link HeaderDelegate#toString()}.
* If required implementation of HeaderDelegate is not accessible via {@link RuntimeDelegate#createHeaderDelegate(java.lang.Class)}
* then method {@code toString} of given object is used.
*
* @param o
* object
* @return string representation of supplied type
*/
@SuppressWarnings({"unchecked"})
public static String getHeaderAsString(Object o) {
HeaderDelegate headerDelegate = RuntimeDelegate.getInstance().createHeaderDelegate(o.getClass());
if (headerDelegate == null) {
return o.toString();
}
return headerDelegate.toString(o);
}
/**
* Convert Collection<String> to single String, where values separated by ','.
*
* @param collection
* the source list
* @return String result
*/
public static String convertToString(Collection<String> collection) {
if (collection == null) {
return null;
}
if (collection.isEmpty()) {
return "";
}
return Joiner.on(',').join(collection);
}
/**
* Appends string in given string builder. All quotes and whitespace are escaped.
*
* @param target
* string builder
* @param appendMe
* string to append
*/
static void appendWithQuote(StringBuilder target, String appendMe) {
if (appendMe == null) {
return;
}
Matcher matcher = WHITESPACE_QUOTE_PATTERN.matcher(appendMe);
if (matcher.find()) {
target.append('"');
appendEscapeQuote(target, appendMe);
target.append('"');
} else {
target.append(appendMe);
}
}
/**
* Appends string in given string builder. All quotes are escaped.
*
* @param target
* string builder
* @param appendMe
* string to append
*/
static void appendEscapeQuote(StringBuilder target, String appendMe) {
for (int i = 0; i < appendMe.length(); i++) {
char c = appendMe.charAt(i);
if (c == '"') {
target.append('\\');
}
target.append(c);
}
}
/**
* Removes all whitespace from given string.
*
* @param str
* the source string
* @return the result string
*/
static String removeWhitespaces(String str) {
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
if (matcher.find()) {
return matcher.replaceAll("");
}
return str;
}
/**
* Adds quotes to {@code String} if it consists whitespaces, otherwise {@code String} will be returned without changes.
*
* @param str
* the source string.
* @return new string.
*/
static String addQuotesIfHasWhitespace(String str) {
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
if (matcher.find()) {
return '"' + str + '"';
}
return str;
}
/**
* Parses quality value. Quality value must have not more then 5 characters and must not be greater then 1 .
*
* @param qString
* string representation of quality value
* @return quality value
*/
static float parseQualityValue(String qString) {
if (qString.length() > 5) {
throw new IllegalArgumentException("Quality value string has more then 5 characters");
}
float qValue;
try {
qValue = Float.valueOf(qString);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Invalid quality value '%s'", qString));
}
if (qValue > 1.0F) {
throw new IllegalArgumentException("Quality value can't be greater then 1.0");
}
return qValue;
}
/**
* Checks that given string is token. Token contains only US-ASCII characters except separators, {@link #SEPARATORS} and controls.
*
* @param token
* the token
* @return -1 if string has only valid characters otherwise index of first illegal character
*/
static int isToken(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c > 127 || SEPARATORS[c]) {
return i;
}
}
return -1;
}
/**
* The cookies parameters can be separated by ';' or ','. This method tries to find first available separator in cookie string.
* If both are not found then string length is returned.
*
* @param cookie
* the cookie string.
* @param start
* index for start searching.
* @return the index of ',' or ';' or length of given cookie string.
*/
private static int findCookieParameterSeparator(String cookie, int start) {
int comma = scan(cookie, start, ',');
int semicolon = scan(cookie, start, ';');
return Math.min(comma, semicolon);
}
/**
* Unescape '"' characters in string, e. g.
* <p>
* String \"hello \\\"someone\\\"\" will be changed to hello \"someone\"
* </p>
*
* @param token
* token for processing
* @return result
*/
static String removeQuoteEscapes(String token) {
StringBuilder sb = new StringBuilder();
int length = token.length();
for (int i = 0; i < length; i++) {
if (charAtIs(token, i, '\\') && charAtIs(token, i + 1, '"')) {
continue;
}
sb.append(token.charAt(i));
}
return sb.toString();
}
private static class AcceptTokenFactory implements ListItemFactory<AcceptToken> {
@Override
public AcceptToken createItem(String part) {
return AcceptToken.valueOf(part);
}
}
private static class AcceptMediaTypeFactory implements ListItemFactory<AcceptMediaType> {
@Override
public AcceptMediaType createItem(String part) {
return AcceptMediaType.valueOf(part);
}
}
private static class AcceptLanguageFactory implements ListItemFactory<AcceptLanguage> {
@Override
public AcceptLanguage createItem(String singleHeader) {
return AcceptLanguage.valueOf(singleHeader);
}
}
}