package com.genesys.wsclient;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.channels.CompletionHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.EofException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.genesys.wsclient.impl.Authentication;
import com.genesys.wsclient.impl.CookieSession;
import com.genesys.wsclient.impl.ExecutorWrappedCompletionHandler;
import com.genesys.wsclient.impl.Jetty769HttpRequest;
import com.genesys.wsclient.impl.Jetty769Util;
import com.genesys.wsclient.impl.JsonUtil;
import com.genesys.wsclient.impl.StringUtil;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
/**
* Use this class for setting up and execute Genesys Web Services requests.
*
* This class is not thread-safe. Therefore always set up and execute your request
* in the same thread, or use according multi-threading techniques.
*/
public class GenesysRequest {
private static final Logger LOG_MESSAGE;
private static final Logger LOG_MESSAGE_CONTENT_HEADERS;
private static final Logger LOG_MESSAGE_CONTENT_RAW;
private static final Logger LOG_MESSAGE_CONTENT_PRETTY;
static {
String packageName = GenesysRequest.class.getPackage().getName();
LOG_MESSAGE = LoggerFactory.getLogger(packageName + ".MESSAGE");
LOG_MESSAGE_CONTENT_HEADERS = LoggerFactory.getLogger(packageName + ".MESSAGE.CONTENT.HEADERS");
LOG_MESSAGE_CONTENT_RAW = LoggerFactory.getLogger(packageName + ".MESSAGE.CONTENT.RAW");
LOG_MESSAGE_CONTENT_PRETTY = LoggerFactory.getLogger(packageName + ".MESSAGE.CONTENT.PRETTY");
}
private final GenesysClient client;
private final CookieSession cookieSession;
private final Authentication authentication;
private final String httpMethod;
private final String uri;
private Integer timeout; // null if timeout not set
private boolean allFields;
private final List<String> fields = new ArrayList<>();
private boolean allSubresources;
private final List<KeyValue> genericQueryParameters = new ArrayList<>();
private final JsonObject jsonObject = new JsonObject();
private HttpRequestSetup customSetup;
private Executor asyncExecutor;
protected GenesysRequest(
GenesysClient client, String httpMethod, String uri,
CookieSession cookieSession, Authentication authentication) {
this.client = client;
this.httpMethod = httpMethod;
this.uri = uri;
this.cookieSession = cookieSession;
this.authentication = authentication;
this.asyncExecutor = client.asyncExecutor;
}
/**
* Sends this request and returns the response from the server.
*
* <p>This method is blocking.
*
* <p>A request can be executed more than once.
*
* @return
* The content (body) of the response.
*
* @throws GenesysMethodException
* If the server responds with statusCode != 0, which denotes a method error.
* This exception is unchecked.
*
* @throws TimeoutException
* The request timed out.
*
* @throws ConnectionFailedException
* Connection to server failed. You may want to retry with an alternative server,
* or retry later.
* For your convenience, this exception extends {@link RequestIOException}.
*
* @throws RequestNotSentException
* The request failed, most probably it could not be sent to the server.
* You may want to retry the operation using the same server later.
* For your convenience, this exception extends {@link RequestIOException}.
*
* @throws ResponseNotReceivedException
* Trouble on receiving the response from the server from the network.
* You may want to retry the operation using the same server if the
* operation can be repeated without harm (as is the case of idempotent operations).
* For your convenience, this exception extends {@link RequestIOException}.
*
* @throws InvalidGenesysResponseException
* The response does not follow the Genesys Web Services conventions.
* This would indicate a defect in the server or in this client library,
* that is why this exception is unchecked.
*
* @throws HttpStatusException
* HTTP status code received which indicates non-success,
* and the response does not indicate a {@link GenesysMethodException}.
* You may react depending on the HTTP status code.
* This exception is unchecked.
*
* @throws InterruptedException
* If this methods is interrupted while blocked waiting for a response.
*/
public String execute()
throws
GenesysMethodException,
ConnectionFailedException,
RequestNotSentException,
ResponseNotReceivedException,
InvalidGenesysResponseException,
HttpStatusException,
TimeoutException,
InterruptedException {
ExceptionCachingExchange exchange = new ExceptionCachingExchange();
setupAndSend(exchange);
exchange.waitForDone();
checkExchangeCompleted(exchange);
cookieSession.handleResponse(exchange);
return checkAndRetrieveResponse(exchange);
}
private void setupAndSend(ContentExchange exchange) {
Jetty769HttpRequest request = new Jetty769HttpRequest(exchange);
setupRequest(request);
logRequest(exchange);
try {
client.httpClient.send(exchange);
} catch (IOException e) {
// This IOException is not a result of trying to write the request on the socket.
// The Jetty HttpClient inappropriately throws it as a checked exception.
// Browse the code for more details.
throw new RuntimeException(e);
}
}
private static class ExceptionCachingExchange extends ContentExchange {
private final AtomicReference<Throwable> connectionFailedException = new AtomicReference<Throwable>();
private final AtomicReference<Throwable> otherException = new AtomicReference<Throwable>();
public ExceptionCachingExchange() {
super(true); // true for caching headers
}
@Override
protected void onConnectionFailed(Throwable e) {
super.onConnectionFailed(e);
connectionFailedException.set(e);
}
@Override
protected void onException(Throwable e) {
super.onException(e);
otherException.set(e);
}
public Throwable getConnectionFailedException() {
return connectionFailedException.get();
}
public Throwable getOtherException() {
return otherException.get();
}
}
/**
* Asynchronous counterpart of {@link #execute()}.
*
* <p>Please expect the same exceptions to be passed to <code>completionHandler.failed()</code>
* as defined in {@link #execute()}.
*
* @param attachment
* Contextual object to attach to the operation, which will be passed to the completion handler.
* Can be <code>null</code>.
*
* @param completionHandler
* Callback interface for notifying completion and result.
*/
public <A> void executeAsync(final A attachment, CompletionHandler<String, ? super A> completionHandler) {
if (asyncExecutor == null) {
throw new IllegalStateException("asyncExecutor is null");
}
final CompletionHandler<String, ? super A> asyncCompletionHandler =
new ExecutorWrappedCompletionHandler<>(asyncExecutor, completionHandler);
AsyncExchange<A> exchange = new AsyncExchange<A>(attachment, asyncCompletionHandler);
setupAndSend(exchange);
}
class AsyncExchange<A> extends ContentExchange {
private final A attachment;
private final CompletionHandler<String, ? super A> asyncCompletionHandler;
/**
* onRequestComplete and onResponseComplete must both be received to consider the exchange done.
* @see HttpExchange#waitForDone
*/
private boolean requestAndResponseCompleted = false;
private boolean requestCompleted = false;
private boolean responseCompleted = false;
public AsyncExchange(A attachment, CompletionHandler<String, ? super A> asyncCompletionHandler) {
super(true); // true for caching headers
this.attachment = attachment;
this.asyncCompletionHandler = asyncCompletionHandler;
}
@Override protected void onRequestComplete() throws IOException {
super.onRequestComplete();
synchronized (this) {
requestCompleted = true;
requestAndResponseCompleted = requestCompleted && responseCompleted;
}
onComplete();
}
@Override protected void onResponseComplete() throws IOException {
super.onResponseComplete();
synchronized (this) {
responseCompleted = true;
requestAndResponseCompleted = requestCompleted && responseCompleted;
}
onComplete();
}
private void onComplete() {
if (requestAndResponseCompleted) {
cookieSession.handleResponse(this);
try {
String result = checkAndRetrieveResponse(this);
asyncCompletionHandler.completed(result, attachment);
} catch (InvalidGenesysResponseException | GenesysMethodException | HttpStatusException e) {
asyncCompletionHandler.failed(e, attachment);
}
}
}
@Override protected void onConnectionFailed(Throwable cause) {
super.onConnectionFailed(cause);
Throwable exc = new ConnectionFailedException(cause);
asyncCompletionHandler.failed(exc, attachment);
}
@Override protected void onExpire() {
super.onExpire();
asyncCompletionHandler.failed(new TimeoutException(), attachment);
}
@Override protected void onException(Throwable cause) {
super.onException(cause);
Throwable e = onOtherException(cause, this.getStatus());
asyncCompletionHandler.failed(e, attachment);
}
}
private void setupRequest(HttpRequest request) {
request.setMethod(httpMethod);
StringBuilder uriBuilder = new StringBuilder();
uriBuilder.append(uri);
appendUriQuery(uriBuilder);
request.setUri(uriBuilder.toString());
authentication.setupRequest(request);
cookieSession.setupRequest(request);
setRequestContent(request);
request.setTimeout(timeout);
if (customSetup != null)
customSetup.setupRequest(request);
}
private void appendUriQuery(StringBuilder uriBuilder) {
ArrayList<KeyValue> queryParams = new ArrayList<>();
if (allFields)
queryParams.add(new KeyValue("fields", "*"));
else if (!fields.isEmpty())
queryParams.add(new KeyValue("fields", StringUtil.join(fields, ",")));
if (allSubresources)
queryParams.add(new KeyValue("subresources", "*"));
char separator = '?';
for (KeyValue param : queryParams) {
uriBuilder.append(separator);
separator = '&';
uriBuilder.append(urlEncode(param.key));
if (param.value != null)
uriBuilder.append('=' + urlEncode(param.value));
}
}
private static String urlEncode(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private void setRequestContent(HttpRequest request) {
if (!jsonObject.entrySet().isEmpty()) {
request.setContentType("application/json");
String content = JsonUtil.toJson(jsonObject);
request.setContent(content, "UTF-8");
}
}
private static void checkExchangeCompleted(ExceptionCachingExchange exchange)
throws
ConnectionFailedException,
RequestNotSentException,
ResponseNotReceivedException,
TimeoutException {
if (exchange.getStatus() != HttpExchange.STATUS_COMPLETED) {
if (exchange.getStatus() == HttpExchange.STATUS_EXPIRED) {
throw new TimeoutException();
} else if (exchange.getConnectionFailedException() != null) {
throw new ConnectionFailedException(exchange.getConnectionFailedException());
} else {
Throwable exc = onOtherException(exchange.getOtherException(), exchange.getStatus());
if (exc instanceof RequestNotSentException) {
throw (RequestNotSentException)exc;
} else if (exc instanceof ResponseNotReceivedException) {
throw (ResponseNotReceivedException)exc;
} else {
throw new AssertionError();
}
}
}
}
private static Throwable onOtherException(Throwable otherException, int exchangeStatus) {
if (otherException != null) {
if (otherException instanceof EofException || isRequestSent(exchangeStatus))
return new ResponseNotReceivedException(otherException);
else
return new RequestNotSentException(otherException);
} else {
String message = "HttpExchange status: " + HttpExchange.toState(exchangeStatus);
if (isRequestSent(exchangeStatus))
return new ResponseNotReceivedException(message);
else
return new RequestNotSentException(message);
}
}
private static boolean isRequestSent(int exchangeStatus) {
return exchangeStatus >= HttpExchange.STATUS_WAITING_FOR_RESPONSE;
}
private static String checkAndRetrieveResponse(ContentExchange exchange)
throws
InvalidGenesysResponseException,
GenesysMethodException,
HttpStatusException {
int httpStatus = exchange.getResponseStatus();
if (HttpStatus.isRedirection(httpStatus) || HttpStatus.isInformational(httpStatus)) {
// Informational responses will actually not get handled here, they will timeout.
throw new HttpStatusException(httpStatus, exchange);
}
try {
String responseContent = checkContentAndRetrieveResponse(exchange);
if (HttpStatus.isSuccess(httpStatus)) {
return responseContent;
} else {
throw new HttpStatusException(httpStatus, exchange);
}
} catch (InvalidGenesysResponseException e) {
if (HttpStatus.isSuccess(httpStatus)) {
throw e;
} else {
// If the HTTP status code indicates an error, ignore an invalid content.
throw new HttpStatusException(httpStatus, exchange);
}
}
}
private static class GenesysResponse {
public Integer statusCode;
public String statusMessage;
}
private static String checkContentAndRetrieveResponse(ContentExchange exchange)
throws
InvalidGenesysResponseException,
GenesysMethodException {
String responseContent;
try {
responseContent = exchange.getResponseContent();
} catch (UnsupportedEncodingException e) {
throw new InvalidGenesysResponseException(e);
}
logResponse(exchange, responseContent);
String contentTypeValue = exchange.getResponseFields().getStringField("Content-Type");
if (contentTypeValue == null)
throw new InvalidGenesysResponseException("No Content-Type header");
String contentType = contentTypeValue.split(";", 2)[0].trim();
if (!contentType.equals("application/json"))
throw new InvalidGenesysResponseException("Content-Type of is not application/json");
GenesysResponse response;
try {
response = JsonUtil.fromJson(responseContent, GenesysResponse.class);
} catch (JsonSyntaxException e) {
throw new InvalidGenesysResponseException(e);
}
if (response == null)
throw new InvalidGenesysResponseException("Invalid JSON");
if (response.statusCode == null)
throw new InvalidGenesysResponseException("Missing statusCode");
if (response.statusCode != 0)
throw new GenesysMethodException(exchange.getResponseStatus(),
response.statusCode, response.statusMessage);
return responseContent;
}
private void logRequest(ContentExchange exchange) {
LOG_MESSAGE.debug(
"Sending request to server: " + exchange.getAddress() +
", request: " + exchange.getMethod() + " " + exchange.getRequestURI());
Jetty769Util.logHeaders(LOG_MESSAGE_CONTENT_HEADERS, exchange.getRequestFields());
if (LOG_MESSAGE_CONTENT_RAW.isDebugEnabled() || LOG_MESSAGE_CONTENT_PRETTY.isDebugEnabled() ) {
if (exchange.getRequestContent() != null) {
try {
String content = new String(exchange.getRequestContent().array(), "UTF-8");
LOG_MESSAGE_CONTENT_RAW.debug("Content: " + content);
if (LOG_MESSAGE_CONTENT_PRETTY.isDebugEnabled())
LOG_MESSAGE_CONTENT_PRETTY.debug("Content:\n" + JsonUtil.prettify(content));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
}
private static void logResponse(ContentExchange exchange, String content) {
LOG_MESSAGE.debug("Received response: HTTP " + exchange.getResponseStatus());
Jetty769Util.logHeaders(LOG_MESSAGE_CONTENT_HEADERS, exchange.getResponseFields());
if (content == null)
content = "";
LOG_MESSAGE_CONTENT_RAW.debug("Content: " + content);
if (LOG_MESSAGE_CONTENT_PRETTY.isDebugEnabled() )
LOG_MESSAGE_CONTENT_PRETTY.debug("Content:\n" + JsonUtil.prettify(content));
}
private static class KeyValue {
final String key;
final String value;
KeyValue(String key, String value) {
this.key = key;
this.value = value;
}
}
public GenesysRequest timeout(int timeout) {
this.timeout = timeout;
return this;
}
public GenesysRequest asyncExecutor(Executor asyncExecutor) {
this.asyncExecutor = asyncExecutor;
return this;
}
public GenesysRequest allFields() {
allFields = true;
return this;
}
public GenesysRequest fields(String... fieldNames) {
fields.addAll(Arrays.asList(fieldNames));
return this;
}
public GenesysRequest allSubresources() {
allSubresources = true;
return this;
}
public GenesysRequest queryParameter(String name, String value) {
this.genericQueryParameters.add(new KeyValue(name, value));
return this;
}
public GenesysRequest jsonParameter(String name, String value) {
this.jsonObject.addProperty(name, value);
return this;
}
public GenesysRequest jsonParameter(String name, String[] value) {
JsonArray jsonArray = new JsonArray();
for (String element : value) {
jsonArray.add(new JsonPrimitive(element));
}
this.jsonObject.add(name, jsonArray);
return this;
}
public GenesysRequest jsonParameter(String parentName, String name, String value) {
JsonObject parentObj = jsonObject.getAsJsonObject(parentName);
if (parentObj == null) {
parentObj = new JsonObject();
this.jsonObject.add(parentName, parentObj);
}
parentObj.addProperty(name, value);
return this;
}
public GenesysRequest customize(HttpRequestSetup httpRequestSetup) {
this.customSetup = httpRequestSetup;
return this;
}
public GenesysRequest operationName(String operationName) {
return jsonParameter("operationName", operationName);
}
public GenesysRequest destinationPhoneNumber(String phoneNumber) {
return jsonParameter("destination", "phoneNumber", phoneNumber);
}
}