package play.libs;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.lang.NotImplementedException;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import play.Logger;
import play.Play;
import play.PlayPlugin;
import play.libs.F.Promise;
import play.libs.OAuth.ServiceInfo;
import play.libs.ws.WSAsync;
import play.libs.ws.WSUrlFetch;
import play.mvc.Http;
import play.mvc.Http.Header;
import play.utils.HTTP;
import play.utils.NoOpEntityResolver;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Simple HTTP client to make webservices requests.
*
* <p/>
* Get latest BBC World news as a RSS content
* <pre>
* HttpResponse response = WS.url("http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml").get();
* Document xmldoc = response.getXml();
* // the real pain begins here...
* </pre>
* <p/>
*
* Search what Yahoo! thinks of google (starting from the 30th result).
* <pre>
* HttpResponse response = WS.url("http://search.yahoo.com/search?p=<em>%s</em>&pstart=1&b=<em>%s</em>", "Google killed me", "30").get();
* if( response.getStatus() == 200 ) {
* html = response.getString();
* }
* </pre>
*/
public class WS extends PlayPlugin {
private static WSImpl wsImpl = null;
public enum Scheme {
BASIC, DIGEST, NTLM, KERBEROS, SPNEGO
}
/**
* Singleton configured with default encoding - this one is used
* when calling static method on WS.
*/
private static WSWithEncoding wsWithDefaultEncoding;
/**
* Internal class exposing all the methods previously exposed by WS.
* This impl has information about encoding.
* When calling original static methos on WS, then a singleton of
* WSWithEncoding is called - configured with default encoding.
* This makes this encoding-enabling backward compatible
*/
public static class WSWithEncoding {
public final String encoding;
public WSWithEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Use thos method to get an instance to WS with diferent encoding
* @param newEncoding the encoding to use in the communication
* @return a new instance of WS with specified encoding
*/
public WSWithEncoding withEncoding(String newEncoding ) {
return new WSWithEncoding( newEncoding );
}
/**
* URL-encode a string to be used as a query string parameter.
* @param part string to encode
* @return url-encoded string
*/
public String encode(String part) {
try {
return URLEncoder.encode(part, encoding);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Build a WebService Request with the given URL.
* This object support chaining style programming for adding params, file, headers to requests.
* @param url of the request
* @return a WSRequest on which you can add params, file headers using a chaining style programming.
*/
public WSRequest url(String url) {
init();
return wsImpl.newRequest(url, encoding);
}
/**
* Build a WebService Request with the given URL.
* This constructor will format url using params passed in arguments.
* This object support chaining style programming for adding params, file, headers to requests.
* @param url to format using the given params.
* @param params the params passed to format the URL.
* @return a WSRequest on which you can add params, file headers using a chaining style programming.
*/
public WSRequest url(String url, String... params) {
Object[] encodedParams = new String[params.length];
for (int i = 0; i < params.length; i++) {
encodedParams[i] = encode(params[i]);
}
return url(String.format(url, encodedParams));
}
}
/**
* Use thos method to get an instance to WS with diferent encoding
* @param encoding the encoding to use in the communication
* @return a new instance of WS with specified encoding
*/
public static WSWithEncoding withEncoding(String encoding ) {
return wsWithDefaultEncoding.withEncoding(encoding);
}
@Override
public void onApplicationStop() {
if (wsImpl != null) {
wsImpl.stop();
wsImpl = null;
}
}
@Override
public void onApplicationStart() {
wsWithDefaultEncoding = new WSWithEncoding(Play.defaultWebEncoding);
}
private synchronized static void init() {
if (wsImpl != null) return;
String implementation = Play.configuration.getProperty("webservice", "async");
if (implementation.equals("urlfetch")) {
wsImpl = new WSUrlFetch();
Logger.trace("Using URLFetch for web service");
} else if (implementation.equals("async")) {
Logger.trace("Using Async for web service");
wsImpl = new WSAsync();
} else {
try {
wsImpl = (WSImpl)Play.classloader.loadClass(implementation).newInstance();
Logger.trace("Using the class:" + implementation + " for web service");
} catch (Exception e) {
throw new RuntimeException("Unable to load the class: " + implementation + " for web service");
}
}
}
/**
* URL-encode a string to be used as a query string parameter.
* @param part string to encode
* @return url-encoded string
*/
public static String encode(String part) {
return wsWithDefaultEncoding.encode(part);
}
/**
* Build a WebService Request with the given URL.
* This object support chaining style programming for adding params, file, headers to requests.
* @param url of the request
* @return a WSRequest on which you can add params, file headers using a chaining style programming.
*/
public static WSRequest url(String url) {
return wsWithDefaultEncoding.url(url);
}
/**
* Build a WebService Request with the given URL.
* This constructor will format url using params passed in arguments.
* This object support chaining style programming for adding params, file, headers to requests.
* @param url to format using the given params.
* @param params the params passed to format the URL.
* @return a WSRequest on which you can add params, file headers using a chaining style programming.
*/
public static WSRequest url(String url, String... params) {
return wsWithDefaultEncoding.url(url, params);
}
public interface WSImpl {
public WSRequest newRequest(String url, String encoding);
public void stop();
}
public static abstract class WSRequest {
public String url;
public final String encoding;
public String username;
public String password;
public Scheme scheme;
public Object body;
public FileParam[] fileParams;
public Map<String, String> headers = new HashMap<String, String>();
public Map<String, Object> parameters = new LinkedHashMap<String, Object>();
public String mimeType;
public boolean followRedirects = true;
/**
* timeout: value in seconds
*/
public Integer timeout = 60;
public ServiceInfo oauthInfo = null;
public String oauthToken = null;
public String oauthSecret = null;
public WSRequest() {
this.encoding = Play.defaultWebEncoding;
}
public WSRequest(String url, String encoding) {
this.url = url;
this.encoding = encoding;
}
/**
* Add a MimeType to the web service request.
* @param mimeType
* @return the WSRequest for chaining.
*/
public WSRequest mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
/**
* define client authentication for a server host
* provided credentials will be used during the request
* @param username
* @param password
* @return the WSRequest for chaining.
*/
public WSRequest authenticate(String username, String password, Scheme scheme) {
this.username = username;
this.password = password;
this.scheme = scheme;
return this;
}
/**
* define client authentication for a server host
* provided credentials will be used during the request
* the basic scheme will be used
* @param username
* @param password
* @return the WSRequest for chaining.
*/
public WSRequest authenticate(String username, String password) {
return authenticate(username, password, Scheme.BASIC);
}
/**
* Sign the request for do a call to a server protected by oauth
* @return the WSRequest for chaining.
*/
public WSRequest oauth(ServiceInfo oauthInfo, String token, String secret) {
this.oauthInfo = oauthInfo;
this.oauthToken = token;
this.oauthSecret = secret;
return this;
}
@Deprecated
public WSRequest oauth(ServiceInfo oauthInfo, OAuth.TokenPair oauthTokens) {
return this.oauth(oauthInfo, oauthTokens.token, oauthTokens.secret);
}
/**
* Indicate if the WS should continue when hitting a 301 or 302
* @return the WSRequest for chaining.
*/
public WSRequest followRedirects(boolean value) {
this.followRedirects = value;
return this;
}
/**
* Set the value of the request timeout, i.e. the number of seconds before cutting the
* connection - default to 60 seconds
* @param timeout the timeout value, e.g. "30s", "1min"
* @return the WSRequest for chaining
*/
public WSRequest timeout(String timeout) {
this.timeout = Time.parseDuration(timeout);
return this;
}
/**
* Add files to request. This will only work with POST or PUT.
* @param files
* @return the WSRequest for chaining.
*/
public WSRequest files(File... files) {
this.fileParams = FileParam.getFileParams(files);
return this;
}
/**
* Add fileParams aka File and Name parameter to the request. This will only work with POST or PUT.
* @param fileParams
* @return the WSRequest for chaining.
*/
public WSRequest files(FileParam... fileParams) {
this.fileParams = fileParams;
return this;
}
/**
* Add the given body to the request.
* @param body
* @return the WSRequest for chaining.
*/
public WSRequest body(Object body) {
this.body = body;
return this;
}
/**
* Add a header to the request
* @param name header name
* @param value header value
* @return the WSRequest for chaining.
*/
public WSRequest setHeader(String name, String value) {
this.headers.put(name, value);
return this;
}
/**
* Add a parameter to the request
* @param name parameter name
* @param value parameter value
* @return the WSRequest for chaining.
*/
public WSRequest setParameter(String name, String value) {
this.parameters.put(name, value);
return this;
}
public WSRequest setParameter(String name, Object value) {
this.parameters.put(name, value);
return this;
}
/**
* Use the provided headers when executing request.
* @param headers
* @return the WSRequest for chaining.
*/
public WSRequest headers(Map<String, String> headers) {
this.headers = headers;
return this;
}
/**
* Add parameters to request.
* If POST or PUT, parameters are passed in body using x-www-form-urlencoded if alone, or form-data if there is files too.
* For any other method, those params are appended to the queryString.
* @return the WSRequest for chaining.
*/
public WSRequest params(Map<String, Object> parameters) {
this.parameters = parameters;
return this;
}
/**
* Add parameters to request.
* If POST or PUT, parameters are passed in body using x-www-form-urlencoded if alone, or form-data if there is files too.
* For any other method, those params are appended to the queryString.
* @return the WSRequest for chaining.
*/
public WSRequest setParameters(Map<String, String> parameters) {
this.parameters.putAll(parameters);
return this;
}
/** Execute a GET request synchronously. */
public abstract HttpResponse get();
/** Execute a GET request asynchronously. */
public Promise<HttpResponse> getAsync() {
throw new NotImplementedException();
}
/** Execute a POST request.*/
public abstract HttpResponse post();
/** Execute a POST request asynchronously.*/
public Promise<HttpResponse> postAsync() {
throw new NotImplementedException();
}
/** Execute a PUT request.*/
public abstract HttpResponse put();
/** Execute a PUT request asynchronously.*/
public Promise<HttpResponse> putAsync() {
throw new NotImplementedException();
}
/** Execute a DELETE request.*/
public abstract HttpResponse delete();
/** Execute a DELETE request asynchronously.*/
public Promise<HttpResponse> deleteAsync() {
throw new NotImplementedException();
}
/** Execute a OPTIONS request.*/
public abstract HttpResponse options();
/** Execute a OPTIONS request asynchronously.*/
public Promise<HttpResponse> optionsAsync() {
throw new NotImplementedException();
}
/** Execute a HEAD request.*/
public abstract HttpResponse head();
/** Execute a HEAD request asynchronously.*/
public Promise<HttpResponse> headAsync() {
throw new NotImplementedException();
}
/** Execute a TRACE request.*/
public abstract HttpResponse trace();
/** Execute a TRACE request asynchronously.*/
public Promise<HttpResponse> traceAsync() {
throw new NotImplementedException();
}
protected String basicAuthHeader() {
return "Basic " + Codec.encodeBASE64(this.username + ":" + this.password);
}
protected String encode(String part) {
try {
return URLEncoder.encode(part, encoding);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected String createQueryString() {
StringBuilder sb = new StringBuilder();
for (String key : this.parameters.keySet()) {
if (sb.length() > 0) {
sb.append("&");
}
Object value = this.parameters.get(key);
if (value != null) {
if (value instanceof Collection<?> || value.getClass().isArray()) {
Collection<?> values = value.getClass().isArray() ? Arrays.asList((Object[]) value) : (Collection<?>) value;
boolean first = true;
for (Object v : values) {
if (!first) {
sb.append("&");
}
first = false;
sb.append(encode(key)).append("=").append(encode(v.toString()));
}
} else {
sb.append(encode(key)).append("=").append(encode(this.parameters.get(key).toString()));
}
}
}
return sb.toString();
}
}
public static class FileParam {
public File file;
public String paramName;
public FileParam(File file, String name) {
this.file = file;
this.paramName = name;
}
public static FileParam[] getFileParams(File[] files) {
FileParam[] filesp = new FileParam[files.length];
for (int i = 0; i < files.length; i++) {
filesp[i] = new FileParam(files[i], files[i].getName());
}
return filesp;
}
}
/**
* An HTTP response wrapper
*/
public static abstract class HttpResponse {
private String _encoding = null;
/**
* the HTTP status code
* @return the status code of the http response
*/
public abstract Integer getStatus();
/**
* @return true if the status code is 20x, false otherwise
*/
public boolean success() {
return Http.StatusCode.success(this.getStatus());
}
/**
* The http response content type
* @return the content type of the http response
*/
public String getContentType() {
return getHeader("content-type");
}
public String getEncoding() {
// Have we already parsed it?
if( _encoding != null ) {
return _encoding;
}
// no! must parse it and remember
String contentType = getContentType();
if( contentType == null ) {
_encoding = Play.defaultWebEncoding;
} else {
HTTP.ContentTypeWithEncoding contentTypeEncoding = HTTP.parseContentType( contentType );
if( contentTypeEncoding.encoding == null ) {
_encoding = Play.defaultWebEncoding;
} else {
_encoding = contentTypeEncoding.encoding;
}
}
return _encoding;
}
public abstract String getHeader(String key);
public abstract List<Header> getHeaders();
/**
* Parse and get the response body as a {@link Document DOM document}
* @return a DOM document
*/
public Document getXml() {
return getXml( getEncoding() );
}
/**
* parse and get the response body as a {@link Document DOM document}
* @param encoding xml charset encoding
* @return a DOM document
*/
public Document getXml(String encoding) {
try {
InputSource source = new InputSource(getStream());
source.setEncoding(encoding);
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
builder.setEntityResolver(new NoOpEntityResolver());
return builder.parse(source);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* get the response body as a string
* @return the body of the http response
*/
public String getString() {
return IO.readContentAsString(getStream(), getEncoding());
}
/**
* get the response body as a string
* @param encoding string charset encoding
* @return the body of the http response
*/
public String getString(String encoding) {
return IO.readContentAsString(getStream(), encoding);
}
/**
* Parse the response string as a query string.
* @return The parameters as a Map. Return an empty map if the response
* is not formed as a query string.
*/
public Map<String, String> getQueryString() {
Map<String, String> result = new HashMap<String, String>();
String body = getString();
for (String entry: body.split("&")) {
if (entry.indexOf("=") > 0) {
result.put(entry.split("=")[0], entry.split("=")[1]);
}
}
return result;
}
/**
* get the response as a stream
* @return an inputstream
*/
public abstract InputStream getStream();
/**
* get the response body as a {@link com.google.gson.JsonElement}
* @return the json response
*/
public JsonElement getJson() {
String json = getString();
try {
return new JsonParser().parse(json);
} catch (Exception e) {
Logger.error("Bad JSON: \n%s", json);
throw new RuntimeException("Cannot parse JSON (check logs)", e);
}
}
}
}