/*
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @see original file at https://github.com/playframework/playframework/blob/master/framework/src/play-java-ws/src/main/java/play/libs/ws/ning/NingWSRequestHolder.java
*/
package com.ecomnext.rest.ning;
import com.damnhandy.uri.template.MalformedUriTemplateException;
import com.damnhandy.uri.template.UriTemplate;
import com.damnhandy.uri.template.VariableExpansionException;
import com.ecomnext.rest.RestAuthScheme;
import com.ecomnext.rest.RestRequestHolder;
import com.ecomnext.rest.RestResponse;
import com.ecomnext.rest.RestSignatureCalculator;
import com.ecomnext.rest.utils.Json;
import com.fasterxml.jackson.databind.JsonNode;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.PerRequestConfig;
import com.ning.http.util.AsyncHttpProviderUtils;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class NingRestRequestHolder implements RestRequestHolder {
private final String url;
private String method = "GET";
private Object body = null;
private Map<String, Collection<String>> headers = new HashMap<>();
private Map<String, Collection<String>> queryParameters = new HashMap<>();
private String username = null;
private String password = null;
private RestAuthScheme scheme = null;
private RestSignatureCalculator calculator = null;
private NingRestClient client = null;
private int timeout = 0;
private Boolean followRedirects = null;
private String virtualHost = null;
public NingRestRequestHolder(NingRestClient client, String url) {
try {
this.client = client;
URL reference = new URL(url);
this.url = url;
String userInfo = reference.getUserInfo();
if (userInfo != null) {
this.setAuth(userInfo);
}
if (reference.getQuery() != null) {
this.setQueryString(reference.getQuery());
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
public NingRestRequestHolder(NingRestClient client, String url, String... params) {
try {
this.client = client;
UriTemplate uriTemplate = UriTemplate.fromTemplate(url);
if (uriTemplate.getVariables().length != params.length) {
throw new IllegalArgumentException("The number of variables in the URL and the number of values do not match");
}
for (int i = 0; i < params.length; i++) {
uriTemplate.set(uriTemplate.getVariables()[i], params[i]);
}
url = uriTemplate.expand();
URL reference = new URL(url);
this.url = url;
String userInfo = reference.getUserInfo();
if (userInfo != null) {
this.setAuth(userInfo);
}
if (reference.getQuery() != null) {
this.setQueryString(reference.getQuery());
}
} catch (MalformedURLException | MalformedUriTemplateException | VariableExpansionException e) {
throw new RuntimeException(e);
}
}
/**
* Sets a header with the given name, this can be called repeatedly.
*/
@Override
public NingRestRequestHolder setHeader(String name, String value) {
if (headers.containsKey(name)) {
Collection<String> values = headers.get(name);
values.add(value);
} else {
List<String> values = new ArrayList<>();
values.add(value);
headers.put(name, values);
}
return this;
}
/**
* Sets a query string.
*/
@Override
public RestRequestHolder setQueryString(String query) {
String[] params = query.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length > 2) {
throw new RuntimeException(new MalformedURLException("QueryString parameter should not have more than 2 = per part"));
} else if (keyValue.length >= 2) {
this.setQueryParameter(keyValue[0], keyValue[1]);
} else if (keyValue.length == 1 && param.charAt(0) != '=') {
this.setQueryParameter(keyValue[0], null);
} else {
throw new RuntimeException(new MalformedURLException("QueryString part should not start with an = and not be empty"));
}
}
return this;
}
/**
* Sets a query parameter with the given name,this can be called repeatedly.
*/
@Override
public RestRequestHolder setQueryParameter(String name, String value) {
if (queryParameters.containsKey(name)) {
Collection<String> values = queryParameters.get(name);
values.add(value);
} else {
List<String> values = new ArrayList<>();
values.add(value);
queryParameters.put(name, values);
}
return this;
}
/**
* Sets the authentication header for the current request using BASIC authentication.
*/
@Override
public RestRequestHolder setAuth(String userInfo) {
this.scheme = RestAuthScheme.BASIC;
if (userInfo.equals("")) {
throw new RuntimeException(new MalformedURLException("userInfo should not be empty"));
}
int split = userInfo.indexOf(":");
if (split == 0) { // We only have a password without user
this.username = "";
this.password = userInfo.substring(1);
} else if (split == -1) { // We only have a username without password
this.username = userInfo;
this.password = "";
} else {
this.username = userInfo.substring(0, split);
this.password = userInfo.substring(split + 1);
}
return this;
}
/**
* Sets the authentication header for the current request using BASIC authentication.
*/
@Override
public RestRequestHolder setAuth(String username, String password) {
this.username = username;
this.password = password;
this.scheme = RestAuthScheme.BASIC;
return this;
}
/**
* Sets the authentication header for the current request.
*/
@Override
public RestRequestHolder setAuth(String username, String password, RestAuthScheme scheme) {
this.username = username;
this.password = password;
this.scheme = scheme;
return this;
}
@Override
public RestRequestHolder sign(RestSignatureCalculator calculator) {
this.calculator = calculator;
return this;
}
/**
* Sets whether redirects (301, 302) should be followed automatically.
*/
@Override
public RestRequestHolder setFollowRedirects(Boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
/**
* Sets the virtual host.
*/
@Override
public RestRequestHolder setVirtualHost(String virtualHost) {
this.virtualHost = virtualHost;
return this;
}
/**
* Sets the request timeout in milliseconds.
*/
@Override
public RestRequestHolder setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
/**
* Set the content type. If the request body is a String, and no charset parameter is included, then it will
* default to UTF-8.
*/
@Override
public RestRequestHolder setContentType(String contentType) {
return setHeader(HttpHeaders.Names.CONTENT_TYPE, contentType);
}
@Override
public RestRequestHolder setMethod(String method) {
this.method = method;
return this;
}
@Override
public RestRequestHolder setBody(String body) {
this.body = body;
return this;
}
@Override
public RestRequestHolder setBody(JsonNode body) {
this.body = body;
return this;
}
@Override
public RestRequestHolder setBody(InputStream body) {
this.body = body;
return this;
}
@Override
public RestRequestHolder setBody(File body) {
this.body = body;
return this;
}
/**
* @return the URL of the request.
*/
@Override
public String getUrl() {
return this.url;
}
/**
* @return the headers (a copy to prevent side-effects).
*/
@Override
public Map<String, Collection<String>> getHeaders() {
return new HashMap<>(this.headers);
}
/**
* @return the query parameters (a copy to prevent side-effects).
*/
@Override
public Map<String, Collection<String>> getQueryParameters() {
return new HashMap<>(this.queryParameters);
}
/**
* @return the auth username, null if not an authenticated request.
*/
@Override
public String getUsername() {
return this.username;
}
/**
* @return the auth password, null if not an authenticated request
*/
@Override
public String getPassword() {
return this.password;
}
/**
* @return the auth scheme, null if not an authenticated request
*/
@Override
public RestAuthScheme getScheme() {
return this.scheme;
}
/**
* @return the signature calculator (exemple: OAuth), null if none is set.
*/
@Override
public RestSignatureCalculator getCalculator() {
return this.calculator;
}
/**
* @return the auth scheme (null if not an authenticated request)
*/
@Override
public int getTimeout() {
return this.timeout;
}
/**
* @return true if the request is configure to follow redirect, false if it is configure not to, null if nothing is configured and the global client preference should be used instead.
*/
@Override
public Boolean getFollowRedirects() {
return this.followRedirects;
}
// Intentionally package public.
String getVirtualHost() {
return this.virtualHost;
}
/**
* Perform a GET on the request asynchronously.
*/
@Override
public CompletableFuture<RestResponse> get() {
return execute("GET");
}
/**
* Perform a PATCH on the request asynchronously.
*
* @param body represented as String
*/
@Override
public CompletableFuture<RestResponse> patch(String body) {
setMethod("PATCH");
return executeString(body);
}
/**
* Perform a POST on the request asynchronously.
*
* @param body represented as String
*/
@Override
public CompletableFuture<RestResponse> post(String body) {
setMethod("POST");
return executeString(body);
}
/**
* Perform a PUT on the request asynchronously.
*
* @param body represented as String
*/
@Override
public CompletableFuture<RestResponse> put(String body) {
setMethod("PUT");
return executeString(body);
}
/**
* Perform a PATCH on the request asynchronously.
*
* @param body represented as JSON
*/
@Override
public CompletableFuture<RestResponse> patch(JsonNode body) {
setMethod("PATCH");
return executeJson(body);
}
/**
* Perform a POST on the request asynchronously.
*
* @param body represented as JSON
*/
@Override
public CompletableFuture<RestResponse> post(JsonNode body) {
setMethod("POST");
return executeJson(body);
}
/**
* Perform a PUT on the request asynchronously.
*
* @param body represented as JSON
*/
@Override
public CompletableFuture<RestResponse> put(JsonNode body) {
setMethod("PUT");
return executeJson(body);
}
/**
* Perform a PATCH on the request asynchronously.
*
* @param body represented as an InputStream
*/
@Override
public CompletableFuture<RestResponse> patch(InputStream body) {
setMethod("PATCH");
return executeIS(body);
}
/**
* Perform a POST on the request asynchronously.
*
* @param body represented as an InputStream
*/
@Override
public CompletableFuture<RestResponse> post(InputStream body) {
setMethod("POST");
return executeIS(body);
}
/**
* Perform a PUT on the request asynchronously.
*
* @param body represented as an InputStream
*/
@Override
public CompletableFuture<RestResponse> put(InputStream body) {
setMethod("PUT");
return executeIS(body);
}
/**
* Perform a POST on the request asynchronously.
*
* @param body represented as a File
*/
@Override
public CompletableFuture<RestResponse> post(File body) {
setMethod("POST");
return executeFile(body);
}
/**
* Perform a PUT on the request asynchronously.
*
* @param body represented as a File
*/
@Override
public CompletableFuture<RestResponse> put(File body) {
setMethod("PUT");
return executeFile(body);
}
/**
* Perform a DELETE on the request asynchronously.
*/
@Override
public CompletableFuture<RestResponse> delete() {
return execute("DELETE");
}
/**
* Perform a HEAD on the request asynchronously.
*/
@Override
public CompletableFuture<RestResponse> head() {
return execute("HEAD");
}
/**
* Perform an OPTIONS on the request asynchronously.
*/
@Override
public CompletableFuture<RestResponse> options() {
return execute("OPTIONS");
}
/**
* Execute an arbitrary method on the request asynchronously.
*
* @param method The method to execute
*/
@Override
public CompletableFuture<RestResponse> execute(String method) {
setMethod(method);
return execute();
}
@Override
public CompletableFuture<RestResponse> execute() {
if (body == null) {
NingRestRequest req = new NingRestRequest(client, method, url, queryParameters, headers);
return execute(req);
} else if (body instanceof String) {
return executeString((String) body);
} else if (body instanceof JsonNode) {
return executeJson((JsonNode) body);
} else if (body instanceof File) {
return executeFile((File) body);
} else if (body instanceof InputStream) {
return executeIS((InputStream) body);
} else {
throw new IllegalStateException("Impossible body: " + body);
}
}
private CompletableFuture<RestResponse> executeString(String body) {
FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap(this.headers);
// Detect and maybe add charset
String contentType = headers.getFirstValue(HttpHeaders.Names.CONTENT_TYPE);
if (contentType == null) {
contentType = "text/plain";
}
String charset = AsyncHttpProviderUtils.parseCharset(contentType);
if (charset == null) {
charset = "utf-8";
headers.replace(HttpHeaders.Names.CONTENT_TYPE, contentType + "; charset=utf-8");
}
byte[] bodyBytes;
try {
bodyBytes = body.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
NingRestRequest req = new NingRestRequest(client, method, url, queryParameters, headers, bodyBytes)
.setBody(body)
.setBodyEncoding(charset);
return execute(req);
}
private CompletableFuture<RestResponse> executeJson(JsonNode body) {
FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap(this.headers);
headers.replace(HttpHeaders.Names.CONTENT_TYPE, "application/json; charset=utf-8");
String bodyStr = Json.stringify(body);
byte[] bodyBytes;
try {
bodyBytes = bodyStr.getBytes("utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
NingRestRequest req = new NingRestRequest(client, method, url, queryParameters, headers, bodyBytes)
.setBody(bodyStr)
.setBodyEncoding("utf-8");
return execute(req);
}
private CompletableFuture<RestResponse> executeIS(InputStream body) {
NingRestRequest req = new NingRestRequest(client, method, url, queryParameters, headers)
.setBody(body);
return execute(req);
}
private CompletableFuture<RestResponse> executeFile(File body) {
NingRestRequest req = new NingRestRequest(client, method, url, queryParameters, headers)
.setBody(body);
return execute(req);
}
private CompletableFuture<RestResponse> execute(NingRestRequest req) {
if (this.timeout > 0) {// todo change PerRequestConfig
PerRequestConfig config = new PerRequestConfig();
config.setRequestTimeoutInMs(this.timeout);
req.setPerRequestConfig(config);
}
if (this.followRedirects != null) {
req.setFollowRedirects(this.followRedirects);
}
if (this.virtualHost != null) {
req.setVirtualHost(this.virtualHost);
}
if (this.username != null && this.password != null && this.scheme != null)
req.auth(this.username, this.password, this.scheme);
if (this.calculator != null)
this.calculator.sign(req);
return req.execute();
}
}