package com.silverforge.webconnector;
import android.text.TextUtils;
import android.util.Base64;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Predicates;
import com.silverforge.webconnector.exceptions.ServerIsNotAvailableException;
import com.silverforge.webconnector.exceptions.SettingsIsNullException;
import com.silverforge.webconnector.model.ConnectorSettings;
import com.silverforge.webconnector.model.InvokeBinaryResult;
import com.silverforge.webconnector.model.InvokeResult;
import com.silverforge.webconnector.model.InvokeStringResult;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import rx.Observable;
import static com.silverforge.webconnector.utils.ParameterCheck.ensureValue;
class Connector {
private static final int RETRY_WAIT_TIME = 500;
private static final int BYTE_CHUNK_SIZE = 1024;
private static final String DEFAULT_BASE_URL = "http://localhost";
private static final int DEFAULT_TIMEOUT = 7000;
private static final int DEFAULT_RETRY_COUNT = 3;
protected static final String STRING_EMPTY = "";
protected ConnectorSettings connectorSettings;
public Connector(ConnectorSettings settings)
throws SettingsIsNullException, URISyntaxException {
prepareConnectorSettings(settings);
}
public ConnectorSettings getSettings() {
return connectorSettings;
}
public void setSettings(ConnectorSettings settings)
throws SettingsIsNullException, URISyntaxException {
prepareConnectorSettings(settings);
}
// region Async methods
protected Observable<String> invokeToEndpointAsync(String httpMethod, String path) {
return invokeToEndpointAsync(httpMethod, path, STRING_EMPTY);
}
protected Observable<String> invokeToEndpointAsync(String httpMethod, String path, String data) {
return Observable.create(subscriber -> {
InvokeStringResult result = invokeToEndpoint(httpMethod, path, data);
if (result.isSuccess()) {
subscriber.onNext(result.getResult());
} else {
Observable
.from(result.getAggregatedExceptions())
.subscribe(e -> subscriber.onError(e));
}
subscriber.onCompleted();
});
}
protected Observable<byte[]> invokeToBinaryEndpointAsync(String httpMethod, String path) {
return invokeToBinaryEndpointAsync(httpMethod, path, BYTE_CHUNK_SIZE);
}
protected Observable<byte[]> invokeToBinaryEndpointAsync(String httpMethod, String path, int chunkSize) {
return Observable.create(subscriber -> {
try {
URL url = connectorSettings
.getUri()
.resolve(path)
.toURL();
HttpURLConnection conn = getConnection(url, httpMethod);
InputStream inputStream = new BufferedInputStream(conn.getURL().openStream(), 8192);
byte data[] = new byte[chunkSize];
while (inputStream.read(data) != -1) {
subscriber.onNext(data);
}
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
subscriber.onError(e);
} finally {
subscriber.onCompleted();
}
});
}
// endregion
// region Sync methods
protected InvokeStringResult invokeToEndpoint(String httpMethod, String path) {
return invokeToEndpoint(httpMethod, path, STRING_EMPTY);
}
protected InvokeStringResult invokeToEndpoint(String httpMethod, String path, String data) {
InvokeStringResult returnResult = new InvokeStringResult();
returnResult.setSuccess(true);
// Build up Http(s)UrlConnection
try {
URL url = connectorSettings
.getUri()
.resolve(path)
.toURL();
HttpURLConnection conn = getConnection(url, httpMethod);
// Add data to connection if any
if (data != null && !data.equals(STRING_EMPTY))
addDataToConnection(data, conn);
// Retrieves with connection status code
String statusCode = getStatusCode(conn);
returnResult.setStatusCode(statusCode);
// Retry pattern for x times invoke if read input fails
final HttpURLConnection finalConn = conn;
Callable<StringBuilder> callable = () -> readInputFromConnection(finalConn);
Retryer<StringBuilder> retryer = RetryerBuilder.<StringBuilder>newBuilder()
.retryIfResult(Predicates.<StringBuilder>isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.fixedWait(RETRY_WAIT_TIME, TimeUnit.MILLISECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(connectorSettings.getRetryCount()))
.build();
// Reads the response from connection
StringBuilder returnValue = retryer.call(callable);
returnResult.setResult(returnValue.toString());
} catch (ExecutionException | RetryException |IOException | NoSuchAlgorithmException | KeyManagementException | ServerIsNotAvailableException e) {
return (InvokeStringResult)invokeExceptionResult(returnResult, e);
}
return returnResult;
}
protected InvokeBinaryResult invokeToBinaryEndpoint(String httpMethod, String path) {
InvokeBinaryResult returnResult = new InvokeBinaryResult();
returnResult.setSuccess(true);
try {
URL url = connectorSettings
.getUri()
.resolve(path)
.toURL();
HttpURLConnection conn = getConnection(url, httpMethod);
// Retrieves with connection status code
String statusCode = getStatusCode(conn);
returnResult.setStatusCode(statusCode);
// Retry pattern for x times invoke if read input fails
final HttpURLConnection finalConn = conn;
Callable<byte[]> callable = () -> readBinaryInputFromConnection(finalConn);
Retryer<byte[]> retryer = RetryerBuilder.<byte[]>newBuilder()
.retryIfResult(Predicates.<byte[]>isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.fixedWait(RETRY_WAIT_TIME, TimeUnit.MILLISECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(connectorSettings.getRetryCount()))
.build();
// Reads the response from connection
byte[] returnValue = retryer.call(callable);
returnResult.setResult(returnValue);
} catch (ExecutionException | RetryException |ServerIsNotAvailableException |IOException | NoSuchAlgorithmException | KeyManagementException e) {
return (InvokeBinaryResult)invokeExceptionResult(returnResult, e);
}
return returnResult;
}
// endregion
// region private methods
private void prepareConnectorSettings(ConnectorSettings settings)
throws SettingsIsNullException, URISyntaxException {
if (settings == null)
throw new SettingsIsNullException();
connectorSettings = ConnectorSettings
.builder()
.baseUrl(ensureValue(settings.getBaseUrl(), DEFAULT_BASE_URL))
.userName(ensureValue(settings.getUserName()))
.password(ensureValue(settings.getPassword()))
.readTimeout(ensureValue(settings.getReadTimeout(), DEFAULT_TIMEOUT, 1000, 10000))
.connectTimeout(ensureValue(settings.getConnectTimeout(), DEFAULT_TIMEOUT, 1000, 10000))
.retryCount(ensureValue(settings.getRetryCount(), DEFAULT_RETRY_COUNT, 1, 5))
.build();
URI url = new URI(connectorSettings.getBaseUrl());
connectorSettings.setUri(url);
}
private String getStatusCode(HttpURLConnection conn)
throws IOException, ServerIsNotAvailableException {
int code;
String statusCode;
code = conn.getResponseCode();
statusCode = Integer.toString(code);
if (statusCode.startsWith("4") || statusCode.startsWith("5"))
throw new ServerIsNotAvailableException(statusCode);
return statusCode;
}
private HttpURLConnection getConnection(URL url, String httpMethod)
throws IOException, KeyManagementException, NoSuchAlgorithmException {
HttpURLConnection conn = null;
String protocol = url.getProtocol().toLowerCase();
if (protocol.equals("http")) {
conn = (HttpURLConnection) url.openConnection();
setConnectionSettings(conn, httpMethod);
} else if (protocol.equals("https")) {
conn = (HttpsURLConnection) url.openConnection();
HttpsURLConnection secureConn = (HttpsURLConnection)conn;
addSSLAuthorizationToConnection(secureConn);
setConnectionSettings(secureConn, httpMethod);
}
return conn;
}
private InvokeResult invokeExceptionResult(InvokeResult returnResult, Exception e) {
e.printStackTrace();
returnResult.addExceptionToResult(e);
returnResult.setSuccess(false);
return returnResult;
}
private StringBuilder readInputFromConnection(HttpURLConnection conn)
throws IOException {
// Read InputStream
InputStream inputStream = conn.getInputStream();
StringBuilder retValue = new StringBuilder();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
String inputLine;
while ((inputLine = in.readLine()) != null) {
retValue.append(inputLine);
retValue.append(System.getProperty("line.separator"));
}
return retValue;
}
private byte[] readBinaryInputFromConnection(HttpURLConnection conn)
throws IOException {
int count;
InputStream inputStream = new BufferedInputStream(conn.getURL().openStream(), 8192);
ByteArrayOutputStream retValue = new ByteArrayOutputStream();
byte data[] = new byte[1024];
while ((count = inputStream.read(data)) != -1) {
retValue.write(data, 0, count);
}
retValue.flush();
retValue.close();
inputStream.close();
return retValue.toByteArray();
}
private void addDataToConnection(String data, HttpURLConnection conn)
throws IOException {
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Cache-Control", "no-cache");
conn.setRequestProperty("Content-Length", Integer.toString(data.getBytes().length));
// Add any data you wish to post here
OutputStreamWriter outputWriter = new OutputStreamWriter(conn.getOutputStream());
outputWriter.write(data);
outputWriter.flush();
outputWriter.close();
}
private void setConnectionSettings(HttpURLConnection conn, String httpMethod)
throws ProtocolException {
// set Timeout and method
conn.setReadTimeout(connectorSettings.getReadTimeout());
conn.setConnectTimeout(connectorSettings.getConnectTimeout());
conn.setRequestMethod(httpMethod);
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(true);
}
private void addSSLAuthorizationToConnection(HttpsURLConnection conn)
throws NoSuchAlgorithmException, KeyManagementException {
String userName = connectorSettings.getUserName();
String password = connectorSettings.getPassword();
// Create the SSL connection
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, null, new java.security.SecureRandom());
conn.setSSLSocketFactory(sc.getSocketFactory());
// Use this if you need SSL authentication
if (!TextUtils.isEmpty(userName) || !TextUtils.isEmpty(password)) {
String userPass = userName + ":" + password;
String basicAuth = "Basic "
+ Base64.encodeToString(userPass.getBytes(),
Base64.DEFAULT);
// [known issue in jre] : http://mail-archives.apache.org/mod_mbox/maven-users/201103.mbox/%3CAANLkTikrKWesVvAHxntLVSNoRw=DZRO=SBhsvqUJ0zHy@mail.gmail.com%3E
conn.setRequestProperty("Authorization", basicAuth.replace("\n", ""));
}
}
// endregion
}