/*
* Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package com.ning.http.util;
import com.ning.http.client.AsyncHttpProvider;
import com.ning.http.client.ByteArrayPart;
import com.ning.http.client.Cookie;
import com.ning.http.client.FilePart;
import com.ning.http.client.FluentStringsMap;
import com.ning.http.client.HttpResponseBodyPart;
import com.ning.http.client.Part;
import com.ning.http.client.StringPart;
import com.ning.http.multipart.ByteArrayPartSource;
import com.ning.http.multipart.MultipartRequestEntity;
import com.ning.http.multipart.PartSource;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* {@link com.ning.http.client.AsyncHttpProvider} common utilities.
* <p/>
* The cookies's handling code is from the Netty framework.
*/
public class AsyncHttpProviderUtils {
public final static String DEFAULT_CHARSET = "ISO-8859-1";
private final static String BODY_NOT_COMPUTED = "Response's body hasn't been computed by your AsyncHandler.";
protected final static ThreadLocal<SimpleDateFormat[]> simpleDateFormat = new ThreadLocal<SimpleDateFormat[]>() {
protected SimpleDateFormat[] initialValue() {
return new SimpleDateFormat[]
{
new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.US), //ASCTIME
new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), //RFC1036
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US),
new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss z", Locale.US),
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US),
new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss Z", Locale.US),
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) // RFC1123
};
}
};
public final static SimpleDateFormat[] get() {
return simpleDateFormat.get();
}
//space ' '
static final byte SP = 32;
//tab ' '
static final byte HT = 9;
/**
* Carriage return
*/
static final byte CR = 13;
/**
* Equals '='
*/
static final byte EQUALS = 61;
/**
* Line feed character
*/
static final byte LF = 10;
/**
* carriage return line feed
*/
static final byte[] CRLF = new byte[]{CR, LF};
/**
* Colon ':'
*/
static final byte COLON = 58;
/**
* Semicolon ';'
*/
static final byte SEMICOLON = 59;
/**
* comma ','
*/
static final byte COMMA = 44;
static final byte DOUBLE_QUOTE = '"';
static final String PATH = "Path";
static final String EXPIRES = "Expires";
static final String MAX_AGE = "Max-Age";
static final String DOMAIN = "Domain";
static final String SECURE = "Secure";
static final String HTTPONLY = "HTTPOnly";
static final String COMMENT = "Comment";
static final String COMMENTURL = "CommentURL";
static final String DISCARD = "Discard";
static final String PORT = "Port";
static final String VERSION = "Version";
public final static URI createUri(String u) {
URI uri = URI.create(u);
final String scheme = uri.getScheme();
if (scheme == null || !scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
throw new IllegalArgumentException("The URI scheme, of the URI " + u
+ ", must be equal (ignoring case) to 'http' or 'https'");
}
String path = uri.getPath();
if (path == null) {
throw new IllegalArgumentException("The URI path, of the URI " + uri
+ ", must be non-null");
} else if (path.length() > 0 && path.charAt(0) != '/') {
throw new IllegalArgumentException("The URI path, of the URI " + uri
+ ". must start with a '/'");
} else if (path.length() == 0) {
return URI.create(u + "/");
}
return uri;
}
public final static String getBaseUrl(URI uri) {
String url = uri.getScheme() + "://" + uri.getAuthority();
int port = uri.getPort();
if (port == -1) {
port = getPort(uri);
url += ":" + port;
}
return url;
}
public final static String getAuthority(URI uri) {
String url = uri.getAuthority();
int port = uri.getPort();
if (port == -1) {
port = getPort(uri);
url += ":" + port;
}
return url;
}
public final static URI getRedirectUri(URI uri, String location) {
URI newUri = uri.resolve(location);
String scheme = newUri.getScheme();
if (scheme == null || !scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
throw new IllegalArgumentException("The URI scheme, of the URI " + newUri
+ ", must be equal (ignoring case) to 'http' or 'https'");
}
return newUri;
}
public final static int getPort(URI uri) {
int port = uri.getPort();
if (port == -1)
port = uri.getScheme().equals("http") ? 80 : 443;
return port;
}
/**
* This is quite ugly as our internal names are duplicated, but we build on top of HTTP Client implementation.
*
* @param params
* @param methodParams
* @return a MultipartRequestEntity.
* @throws java.io.FileNotFoundException
*/
public final static MultipartRequestEntity createMultipartRequestEntity(List<Part> params, FluentStringsMap methodParams) throws FileNotFoundException {
com.ning.http.multipart.Part[] parts = new com.ning.http.multipart.Part[params.size()];
int i = 0;
for (Part part : params) {
if (part instanceof com.ning.http.multipart.Part) {
parts[i] = (com.ning.http.multipart.Part) part;
} else if (part instanceof StringPart) {
parts[i] = new com.ning.http.multipart.StringPart(part.getName(),
((StringPart) part).getValue(),
((StringPart) part).getCharset());
} else if (part instanceof FilePart) {
parts[i] = new com.ning.http.multipart.FilePart(part.getName(),
((FilePart) part).getFile(),
((FilePart) part).getMimeType(),
((FilePart) part).getCharSet());
} else if (part instanceof ByteArrayPart) {
PartSource source = new ByteArrayPartSource(((ByteArrayPart) part).getFileName(), ((ByteArrayPart) part).getData());
parts[i] = new com.ning.http.multipart.FilePart(part.getName(),
source,
((ByteArrayPart) part).getMimeType(),
((ByteArrayPart) part).getCharSet());
} else if (part == null) {
throw new NullPointerException("Part cannot be null");
} else {
throw new IllegalArgumentException(String.format("Unsupported part type for multipart parameter %s",
part.getName()));
}
++i;
}
return new MultipartRequestEntity(parts, methodParams);
}
public final static byte[] readFully(InputStream in, int[] lengthWrapper) throws IOException {
// just in case available() returns bogus (or -1), allocate non-trivial chunk
byte[] b = new byte[Math.max(512, in.available())];
int offset = 0;
while (true) {
int left = b.length - offset;
int count = in.read(b, offset, left);
if (count < 0) { // EOF
break;
}
offset += count;
if (count == left) { // full buffer, need to expand
b = doubleUp(b);
}
}
// wish Java had Tuple return type...
lengthWrapper[0] = offset;
return b;
}
private static byte[] doubleUp(byte[] b) {
int len = b.length;
byte[] b2 = new byte[len + len];
System.arraycopy(b, 0, b2, 0, len);
return b2;
}
public static String encodeCookies(Collection<Cookie> cookies) {
StringBuilder sb = new StringBuilder();
for (Cookie cookie : cookies) {
if (cookie.getVersion() >= 1) {
add(sb, '$' + VERSION, 1);
}
add(sb, cookie.getName(), cookie.getValue());
if (cookie.getPath() != null) {
add(sb, '$' + PATH, cookie.getPath());
}
if (cookie.getDomain() != null) {
add(sb, '$' + DOMAIN, cookie.getDomain());
}
if (cookie.getVersion() >= 1) {
if (!cookie.getPorts().isEmpty()) {
sb.append('$');
sb.append(PORT);
sb.append((char) EQUALS);
sb.append((char) DOUBLE_QUOTE);
for (int port : cookie.getPorts()) {
sb.append(port);
sb.append((char) COMMA);
}
sb.setCharAt(sb.length() - 1, (char) DOUBLE_QUOTE);
sb.append((char) SEMICOLON);
}
}
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
private static void add(StringBuilder sb, String name, String val) {
if (val == null) {
addQuoted(sb, name, "");
return;
}
for (int i = 0; i < val.length(); i++) {
char c = val.charAt(i);
switch (c) {
case '\t':
case ' ':
case '"':
case '(':
case ')':
case ',':
case '/':
case ':':
case ';':
case '<':
case '=':
case '>':
case '?':
case '@':
case '[':
case '\\':
case ']':
case '{':
case '}':
addQuoted(sb, name, val);
return;
}
}
addUnquoted(sb, name, val);
}
private static void addUnquoted(StringBuilder sb, String name, String val) {
sb.append(name);
sb.append((char) EQUALS);
sb.append(val);
sb.append((char) SEMICOLON);
}
private static void addQuoted(StringBuilder sb, String name, String val) {
if (val == null) {
val = "";
}
sb.append(name);
sb.append((char) EQUALS);
sb.append((char) DOUBLE_QUOTE);
sb.append(val.replace("\\", "\\\\").replace("\"", "\\\""));
sb.append((char) DOUBLE_QUOTE);
sb.append((char) SEMICOLON);
}
private static void add(StringBuilder sb, String name, int val) {
sb.append(name);
sb.append((char) EQUALS);
sb.append(val);
sb.append((char) SEMICOLON);
}
public static String constructUserAgent(Class<? extends AsyncHttpProvider> httpProvider) {
StringBuffer b = new StringBuffer("AsyncHttpClient/1.0")
.append(" ")
.append("(")
.append(httpProvider.getSimpleName())
.append(" - ")
.append(System.getProperty("os.name"))
.append(" - ")
.append(System.getProperty("os.version"))
.append(" - ")
.append(System.getProperty("java.version"))
.append(" - ")
.append(Runtime.getRuntime().availableProcessors())
.append(" core(s))");
return b.toString();
}
public static String parseCharset(String contentType) {
for (String part : contentType.split(";")) {
if (part.trim().startsWith("charset=")) {
String[] val = part.split("=");
if (val[1] != null) {
return val[1].trim();
}
}
}
return null;
}
public static Cookie parseCookie(String value) {
String[] fields = value.split(";\\s*");
String[] cookie = fields[0].split("=");
String cookieName = cookie[0];
String cookieValue = (cookie.length==1) ? null : cookie[1];
int maxAge = -1;
String path = null;
String domain = null;
boolean secure = false;
boolean maxAgeSet = false;
boolean expiresSet = false;
for (int j = 1; j < fields.length; j++) {
if ("secure".equalsIgnoreCase(fields[j])) {
secure = true;
} else if (fields[j].indexOf('=') > 0) {
String[] f = fields[j].split("=");
if(f.length==1) continue; // Add protection against null field values
// favor 'max-age' field over 'expires'
if (!maxAgeSet && "max-age".equalsIgnoreCase(f[0])) {
try {
maxAge = Integer.valueOf(removeQuote(f[1]));
}
catch (NumberFormatException e1) {
// ignore failure to parse -> treat as session cookie
// invalidate a previously parsed expires-field
maxAge = -1;
}
maxAgeSet = true;
} else if (!maxAgeSet && !expiresSet && "expires".equalsIgnoreCase(f[0])) {
try {
maxAge = convertExpireField(f[1]);
}
catch (Exception e) {
// original behavior, is this correct at all (expires field with max-age semantics)?
try {
maxAge = Integer.valueOf(f[1]);
}
catch (NumberFormatException e1) {
// ignore failure to parse -> treat as session cookie
}
}
expiresSet = true;
} else if ("domain".equalsIgnoreCase(f[0])) {
domain = f[1];
} else if ("path".equalsIgnoreCase(f[0])) {
path = f[1];
}
}
}
if (maxAge < -1) {
maxAge = -1;
}
return new Cookie(domain, cookieName, cookieValue, path, maxAge, secure);
}
private static int convertExpireField(String timestring) throws Exception {
Exception exception = null;
for (SimpleDateFormat sdf : simpleDateFormat.get()) {
try {
long expire = sdf.parse(removeQuote(timestring.trim())).getTime();
return (int) ((expire - System.currentTimeMillis()) / 1000);
} catch (ParseException e) {
exception = e;
} catch (NumberFormatException e) {
exception = e;
}
}
throw exception;
}
private final static String removeQuote(String s) {
if (s.startsWith("\"")) {
s = s.substring(1);
}
if (s.endsWith("\"")) {
s = s.substring(0,s.length() -1);
}
return s;
}
public static void checkBodyParts(int statusCode, Collection<HttpResponseBodyPart> bodyParts) {
if (bodyParts == null || bodyParts.size() == 0) {
// We allow empty body on 204
if (statusCode == 204) return;
throw new IllegalStateException(BODY_NOT_COMPUTED);
}
}
}