package org.etk.core.rest.impl.header;
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.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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;
public final class HeaderHelper {
/**
* Constructor.
*/
private HeaderHelper() {
}
/**
* Pattern for search whitespace and quote in string.
*/
private static final Pattern WHITESPACE_QOUTE_PATTERN = Pattern.compile("[\\s\"]");
/**
* Pattern for 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 String SEPARTORS = "()<>@,;:\"\\/[]?={}";
/**
* 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>() {
/**
* Compare two QualityValue for order.
*
* @param o1 first QualityValue to be compared
* @param o2 second QualityValue to be compared
* @return result of comparison
* @see Comparator#compare(Object, Object)
* @see QualityValue
*/
public int compare(QualityValue o1, QualityValue o2) {
float q1 = o1.getQvalue();
float q2 = o2.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<AcceptMediaType>() {
/**
* {@inheritDoc}
*/
@Override
protected AcceptMediaType create(String part) {
return AcceptMediaType.valueOf(part);
}
};
/**
* Create sorted by quality value accepted media type list.
*
* @param header source header string
* @return List of AcceptMediaType
*/
public static List<AcceptMediaType> createAcceptedMediaTypeList(String header) {
if (header == null || header.length() == 0)
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<AcceptLanguage>() {
/**
* {@inheritDoc}
*/
@Override
protected AcceptLanguage create(String part) {
return AcceptLanguage.valueOf(part);
}
};
/**
* Create sorted by quality value accepted language list.
*
* @param header source header string
* @return List of AcceptLanguage
*/
public static List<AcceptLanguage> createAcceptedLanguageList(String header) {
if (header == null || header.length() == 0)
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<AcceptToken>() {
@Override
protected AcceptToken create(String part) {
try {
// check does contains parameter
int col = part.indexOf(';');
String token = col > 0 ? part.substring(0, col).trim() : part.trim();
int i = -1;
if ((i = isToken(token)) != -1) // check is valid token
throw new IllegalArgumentException("Not valid character at index " + i + " in " + token);
if (col < 0)
return new AcceptToken(token);
Map<String, String> param = new HeaderParameterParser().parse(part);
if (param.containsKey(QualityValue.QVALUE))
return new AcceptToken(token, parseQualityValue(param.get(QualityValue.QVALUE)));
return new AcceptToken(token);
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
};
/**
* Create 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 (header == null || header.length() == 0)
return ACCEPT_ALL_TOKENS;
return LIST_TOKEN_PRODUCER.createQualitySortedList(header);
}
/**
* Create 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 (header == null || header.length() == 0)
return ACCEPT_ALL_TOKENS;
return LIST_TOKEN_PRODUCER.createQualitySortedList(header);
}
// cookie
/**
* Temporary cookie image.
*/
private static class TempCookie {
/**
* Cookie name.
*/
String name;
/**
* Cookie value.
*/
String value;
/**
* Cookie version.
*/
int version;
/**
* Cookie path.
*/
String path;
/**
* Cookie domain.
*/
String domain;
// For NewCokie.
/**
* Comments about cookie.
*/
String comment;
/**
* Cookie max age.
*/
int maxAge;
/**
* True if cookie secure false otherwise.
*/
boolean security;
/**
* @param name cookie name
* @param value cookie value
*/
public TempCookie(String name, String value) {
this.name = name;
this.value = value;
this.version = Cookie.DEFAULT_VERSION;
this.domain = null;
this.path = null;
this.comment = null;
this.maxAge = NewCookie.DEFAULT_MAX_AGE;
this.security = false;
}
}
/**
* Parse cookie header string and create collection of cookie from it.
*
* @param cookie the cookie string.
* @return collection of Cookie.
*/
public static List<Cookie> parseCookies(String cookie) {
int n = 0;
int p = 0;
TempCookie temp = null;
int version = 0;
List<Cookie> l = new ArrayList<Cookie>();
while (p < cookie.length()) {
n = findCookieParameterSeparator(cookie, p);
// cut pair of key/value
String pair = cookie.substring(p, n);
String name = "";
String value = "";
// '=' separator
int eq = pair.indexOf('=');
if (eq != -1) {
name = pair.substring(0, eq).trim();
value = pair.substring(eq + 1).trim();
if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1)
value = value.substring(1, value.length() - 1);
} else {
// there is no value
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 (name.indexOf('$') == -1) {
// first save previous cookie
if (temp != null)
l.add(new Cookie(temp.name, temp.value, temp.path, temp.domain, temp.version));
temp = new TempCookie(name, value);
// version was kept before
// @see http://www.ietf.org/rfc/rfc2109.txt section 4.4
temp.version = version;
} else if (name.equalsIgnoreCase("$Version")) {
// keep version number
version = Integer.valueOf(value);
} else if (name.equalsIgnoreCase("$Path") && temp != null) {
// Temporary cookie must exists, otherwise this parameter will be lost
temp.path = value;
} else if (name.equalsIgnoreCase("$Domain") && temp != null) {
// Temporary cookie must exists, otherwise this parameter will be lost.
temp.domain = value;
}
p = n + 1;
}
if (temp != null)
l.add(new Cookie(temp.name, temp.value, temp.path, temp.domain, temp.version));
return l;
}
// 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
/**
* RFC 822, updated by RFC 1123.
*/
private static final String RFC_1123_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
/**
* RFC 850, obsoleted by RFC 1036.
*/
private static final String RFC_1036_DATE_FORMAT = "EEEE, dd-MMM-yy HH:mm:ss zzz";
/**
* ANSI C's asctime() format.
*/
private static final String ANSI_C_DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy";
/**
* SimpleDateFormat java docs says:
* <p>
* Date formats are not synchronized. It is recommended to create separate
* format instances for each thread. If multiple threads access a format
* concurrently, it must be synchronized externally.
*/
private static ThreadLocal<List<SimpleDateFormat>> dateFormats = new ThreadLocal<List<SimpleDateFormat>>() {
/**
* {@inheritDoc}
*/
@Override
protected List<SimpleDateFormat> initialValue() {
List<SimpleDateFormat> l = new ArrayList<SimpleDateFormat>(3);
l.add(new SimpleDateFormat(RFC_1123_DATE_FORMAT, Locale.US));
l.add(new SimpleDateFormat(RFC_1036_DATE_FORMAT, Locale.US));
l.add(new SimpleDateFormat(ANSI_C_DATE_FORMAT, Locale.US));
TimeZone tz = TimeZone.getTimeZone("GMT");
l.get(0).setTimeZone(tz);
l.get(1).setTimeZone(tz);
l.get(2).setTimeZone(tz);
return Collections.unmodifiableList(l);
}
};
/**
* @return list of allowed date formats
*/
public static List<SimpleDateFormat> getDateFormats() {
return dateFormats.get();
}
/**
* Parse date header. Will try to found appropriated format for given date
* header. Format can be one of see {@link <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) {
try {
for (SimpleDateFormat format : getDateFormats())
return format.parse(header);
} catch (ParseException e) {
// ignore all ParseException now
}
// no one format was found
throw new IllegalArgumentException("Not found appropriated date format for " + header);
}
//
/**
* @param httpHeaders HTTP headers
* @return parsed content-length or null if content-length header is not specified
*/
public static long getContentLengthLong(MultivaluedMap<String, String> httpHeaders) {
String t = httpHeaders.getFirst(HttpHeaders.CONTENT_LENGTH);
// to be sure Content-length header is not null, usually it must be not null
return t != null ? Long.parseLong(t) : 0;
}
/**
* Create string representation of Java Object for adding to response. Method
* use {@link HeaderDelegate#toString()}.
*
* @param o HTTP header as Java type.
* @return string representation of supplied type
*/
@SuppressWarnings("unchecked")
public static String getHeaderAsString(Object o) {
HeaderDelegate hd = RuntimeDelegate.getInstance().createHeaderDelegate(o.getClass());
return hd.toString(o);
}
/**
* Convert Collection<String> to single String, where values separated by ','.
* Useful for getting source string of HTTP header for next processing quality
* value of header tokens.
*
* @param collection the source list
* @return String result
*/
public static String convertToString(Collection<String> collection) {
if (collection == null)
return null;
if (collection.size() == 0)
return "";
StringBuffer sb = new StringBuffer();
for (String t : collection) {
if (sb.length() > 0)
sb.append(',');
sb.append(t);
}
return sb.toString();
}
/**
* Append string in given string buffer, if string contains quotes or
* whitespace, then it be escaped.
*
* @param sb string buffer
* @param s string
*/
static void appendWithQuote(StringBuffer sb, String s) {
if (s == null)
return;
Matcher m = WHITESPACE_QOUTE_PATTERN.matcher(s);
if (m.find()) {
sb.append('"');
appendEscapeQuote(sb, s);
sb.append('"');
return;
}
sb.append(s);
}
/**
* Append string in given string buffer, quotes will be escaped.
*
* @param sb string buffer
* @param s string
*/
static void appendEscapeQuote(StringBuffer sb, String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '"')
sb.append('\\');
sb.append(c);
}
}
/**
* Remove all whitespace from given string.
*
* @param s the source string
* @return the result string
*/
static String removeWhitespaces(String s) {
Matcher m = WHITESPACE_PATTERN.matcher(s);
if (m.find())
return m.replaceAll("");
return s;
}
/**
* Add quotes to <code>String</code> if it consists whitespaces, otherwise
* <code>String</code> will be returned without changes.
*
* @param s the source string.
* @return new string.
*/
static String addQuotesIfHasWhitespace(String s) {
Matcher macther = WHITESPACE_PATTERN.matcher(s);
if (macther.find())
return '"' + s + '"';
return s;
}
/**
* Check syntax of quality value and parse it. Quality value must have not
* more then 5 characters and be not more 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 q = Float.valueOf(qstring);
if (q > 1.0F)
throw new IllegalArgumentException("Quality value can't be greater then 1.0");
return q;
}
/**
* Check is given string token. Token may contains only US-ASCII characters
* except separators, {@link #SEPARTORS} and controls.
*
* @param token the token
* @return -1 if string has only valid character otherwise index of first
* wrong character
*/
static int isToken(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c >= 127 || SEPARTORS.indexOf(c) != -1)
return i;
}
return -1;
}
/**
* The cookies parameters can be separated by ';' or ',', try to find first
* available separator in cookie string. If both not found the string length
* will be returned.
*
* @param cookie the cookie string.
* @param start index for start searching.
* @return the index of ',' or ';'.
*/
private static int findCookieParameterSeparator(String cookie, int start) {
int p;
int comma = cookie.indexOf(',', start);
int semicolon = cookie.indexOf(';', start);
if (comma > 0 && semicolon > 0)
p = comma < semicolon ? comma : semicolon;
else if (comma < 0 && semicolon > 0)
p = semicolon;
else if (comma > 0 && semicolon < 0)
p = comma;
else
p = cookie.length(); // end of string? not comma nor semicolon found
return p;
}
/**
* 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 filterEscape(String token) {
StringBuffer sb = new StringBuffer();
// boolean escape = false;
int strlen = token.length();
for (int i = 0; i < strlen; i++) {
char c = token.charAt(i);
// escape = !escape && c == '\\';
if (c == '\\' && i < strlen - 1 && token.charAt(i + 1) == '"')
continue;
sb.append(c);
}
return sb.toString();
}
}