package com.mozu.client;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.RequestLine;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.util.EntityUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mozu.api.ApiContext;
import com.mozu.api.ApiError;
import com.mozu.api.ApiException;
import com.mozu.api.AsyncCallback;
import com.mozu.api.Headers;
import com.mozu.api.MozuClient;
import com.mozu.api.MozuConfig;
import com.mozu.api.MozuUrl;
import com.mozu.api.Version;
import com.mozu.api.ApiError.Item;
import com.mozu.api.cache.CacheManager;
import com.mozu.api.cache.CacheManagerFactory;
import com.mozu.api.cache.impl.CacheItem;
import com.mozu.api.contracts.appdev.AuthTicket;
import com.mozu.api.contracts.tenant.Tenant;
import com.mozu.api.resources.platform.TenantResource;
import com.mozu.api.security.AppAuthenticator;
import com.mozu.api.security.AuthenticationScope;
import com.mozu.api.security.CustomerAuthenticator;
import com.mozu.api.security.UserAuthenticator;
import com.mozu.api.utils.HttpHelper;
import com.mozu.api.utils.JsonUtils;
import com.mozu.api.utils.MozuHttpClientPool;
public class MozuClientImpl <TResult> implements MozuClient<TResult> {
private static final ObjectMapper mapper = JsonUtils.initObjectMapper();
private final Class<TResult> responseType;
private ApiContext apiContext = null;
private String baseAddress = null;
private HashMap<String, String> headers = new HashMap<String, String>();
private String verb = null;
private MozuUrl resourceUrl = null;
private String httpContent = null;
private InputStream streamContent = null;
private CacheItem cacheItem = null;
private String cacheKey = null;
private TResult syncResult = null;
private Exception syncException = null;
private HttpResponse syncResponse = null;
public MozuClientImpl() {
this.responseType = null;
}
public MozuClientImpl(Class<TResult> responseType) throws Exception {
this.responseType = responseType;
}
@Override
public void setContext(ApiContext apiContext) {
this.apiContext = apiContext;
if (apiContext != null) {
if (apiContext.getTenantId() > 0) {
addHeader(Headers.X_VOL_TENANT, String.valueOf(apiContext.getTenantId()));
}
if (apiContext.getSiteId() != null && apiContext.getSiteId() > 0) {
addHeader(Headers.X_VOL_SITE, String.valueOf(apiContext.getSiteId()));
}
if (apiContext.getMasterCatalogId() != null && apiContext.getMasterCatalogId() > 0) {
addHeader(Headers.X_VOL_MASTER_CATALOG, String.valueOf(apiContext.getMasterCatalogId()));
}
if (apiContext.getCatalogId() != null && apiContext.getCatalogId() > 0) {
addHeader(Headers.X_VOL_CATALOG, String.valueOf(apiContext.getCatalogId()));
}
if (apiContext.getLocale() != null) {
addHeader(Headers.X_VOL_LOCALE, String.valueOf(apiContext.getLocale()));
}
if (apiContext.getCurrency() != null) {
addHeader(Headers.X_VOL_CURRENCY, String.valueOf(apiContext.getCurrency()));
}
}
}
@Override
public void setBaseAddress(String baseAddress) {
this.baseAddress = baseAddress;
}
@Override
public void addHeader(String header, String value) {
headers.put(header, value);
}
@Override
public void setVerb(String verb) {
this.verb = verb;
}
@Override
public void setResourceUrl(MozuUrl resourceUrl) {
this.resourceUrl = resourceUrl;
}
@Override
public <TBody> void setBody(TBody body) throws JsonProcessingException {
httpContent = mapper.writeValueAsString(body);
}
@Override
public void setBody(InputStream body) throws JsonProcessingException {
streamContent = body;
}
@Override
public void setBody(String body) {
httpContent = body;
}
@Override
public String getStringResult() throws Exception {
// TODO: Fix This!
return syncResponse!=null?stringContent(syncResponse):null;
}
@Override
public TResult getResult() throws Exception {
return syncResult;
}
@SuppressWarnings("unchecked")
public TResult getResult(HttpResponse httpResponse) throws Exception {
TResult tResult = null;
try {
if (httpResponse.getStatusLine().getStatusCode() == 404)
return null;
if (responseType != null) {
String className = responseType.getName();
if (className.equals(java.io.InputStream.class.getName())) {
tResult = (TResult) httpResponse.getEntity().getContent();
} else {
if (cacheItem != null)
tResult = deserialize(cacheItem.getContent(), responseType);
else
tResult = deserialize(stringContent(httpResponse), responseType);
}
}
} finally {
EntityUtils.consume(httpResponse.getEntity());
}
return tResult;
}
@Override
public void executeRequest() throws Exception {
// Old style request, make a psuedo synchronous request
syncException = null;
AsyncCallback<TResult> callbackHandler = new DefaultCallbackHandler();
CountDownLatch latch = executeRequest(callbackHandler);
latch.await();
if (syncException!=null) {
throw syncException;
}
}
@Override
public TResult executePostRequest(Object bodyObject, String resourceUrl,
Map<String, String> headers) throws ApiException {
// Old style request, make a psuedo synchronous request
syncException = null;
AsyncCallback<TResult> callbackHandler = new DefaultCallbackHandler();
CountDownLatch latch = executePostRequest(bodyObject, resourceUrl, headers, callbackHandler);
try {
latch.await();
} catch (Exception e) {
throw new ApiException("ASync I/O Exception: " + e.getMessage());
}
if (syncException!=null) {
throw new ApiException("ASync I/O Exception: " + syncException.getMessage());
}
return syncResult;
}
@Override
public TResult executePutRequest(Object bodyObject, String resourceUrl,
Map<String, String> headers) throws ApiException {
// Old style request, make a psuedo synchronous request
syncException = null;
AsyncCallback<TResult> callbackHandler = new DefaultCallbackHandler();
CountDownLatch latch = executePutRequest(bodyObject, resourceUrl, headers, callbackHandler);
try {
latch.await();
} catch (Exception e) {
throw new ApiException("ASync I/O Exception: " + e.getMessage());
}
if (syncException!=null) {
throw new ApiException("ASync I/O Exception: " + syncException.getMessage());
}
return syncResult;
}
@Override
public void executeDeleteRequest(String resourceUrl,
Map<String, String> headers) throws ApiException {
// Old style request, make a psuedo synchronous request
syncException = null;
AsyncCallback<TResult> callbackHandler = new DefaultCallbackHandler();
CountDownLatch latch = executeDeleteRequest(resourceUrl, headers, callbackHandler);
try {
latch.await();
} catch (Exception e) {
throw new ApiException("ASync I/O Exception: " + e.getMessage());
}
if (syncException!=null) {
throw new ApiException("ASync I/O Exception: " + syncException.getMessage());
}
}
@Override
public void cleanupHttpConnection() throws Exception {
}
@SuppressWarnings("unchecked")
@Override
public CountDownLatch executeRequest(final AsyncCallback<TResult> callback) throws Exception {
validateContext();
CloseableHttpAsyncClient client = MozuHttpClientPool.getInstance().getHttpClient();
final BasicHttpEntityEnclosingRequest request = buildRequest();
URL url = new URL(baseAddress);
String hostNm = url.getHost();
int port = url.getPort();
String sche = url.getProtocol();
HttpHost httpHost = new HttpHost(hostNm, port, sche);
setCacheKey(request);
CacheManager<CacheItem> cache = (CacheManager<CacheItem>) CacheManagerFactory.getCacheManager();
if (cache != null) {
cacheItem = cache.get(cacheKey);
if (cacheItem != null)
request.addHeader("If-None-Match", cacheItem.geteTag());
}
final CountDownLatch latch = new CountDownLatch(1);
BaseCallbackHandler futureCallback = new BaseCallbackHandler(callback, latch, request);
client.execute(httpHost, request, futureCallback);
return latch;
}
@Override
public CountDownLatch executePostRequest(Object bodyObject, String resourceUrl,
Map<String, String> headers, AsyncCallback<TResult> callback) throws ApiException {
return executeRequest(bodyObject, new HttpPost(resourceUrl), headers, callback);
}
@Override
public CountDownLatch executePutRequest(Object bodyObject, String resourceUrl,
Map<String, String> headers, AsyncCallback<TResult> callback) throws ApiException {
return executeRequest(bodyObject, new HttpPut(resourceUrl), headers, callback);
}
@Override
public CountDownLatch executeDeleteRequest(String resourceUrl,
Map<String, String> headers, AsyncCallback<TResult> callback) throws ApiException {
return executeRequest(new HttpDelete(resourceUrl), headers, callback);
}
private CountDownLatch executeRequest(HttpRequestBase request, Map<String, String> headers, AsyncCallback<TResult> callback) {
try {
request.setHeader("Accept", "application/json");
request.setHeader("Content-type", "application/json");
if (headers!=null) {
for (Map.Entry<String, String> entry: headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
}
return execute(request, callback);
} catch (IOReactorException iore) {
throw new ApiException("Async I/O error: " + iore.getMessage());
}
}
private CountDownLatch executeRequest(Object bodyObject, HttpEntityEnclosingRequestBase request, Map<String, String> headers, AsyncCallback<TResult> callback) {
try {
String body = mapper.writeValueAsString(bodyObject);
StringEntity se = new StringEntity(body);
request.setEntity(se);
request.setHeader("Accept", "application/json");
request.setHeader("Content-type", "application/json");
if (headers!=null) {
for (Map.Entry<String, String> entry: headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
}
return execute(request, callback);
} catch (UnsupportedEncodingException uee) {
throw new ApiException("JSON error proccessing authentication: " + uee.getMessage());
} catch (JsonProcessingException jpe) {
throw new ApiException("JSON error proccessing authentication: " + jpe.getMessage());
} catch (IOReactorException iore) {
throw new ApiException("Async I/O error: " + iore.getMessage());
}
}
private CountDownLatch execute(final HttpEntityEnclosingRequestBase request, final AsyncCallback<TResult> callback) throws IOReactorException {
final CountDownLatch latch = new CountDownLatch(1);
CloseableHttpAsyncClient client = MozuHttpClientPool.getInstance().getHttpClient();
BaseCallbackHandler futureCallback = new BaseCallbackHandler(callback, latch, request);
client.execute(request, futureCallback);
return latch;
}
private CountDownLatch execute(final HttpRequestBase request, final AsyncCallback<TResult> callback) throws IOReactorException {
final CountDownLatch latch = new CountDownLatch(1);
CloseableHttpAsyncClient client = MozuHttpClientPool.getInstance().getHttpClient();
BaseCallbackHandler futureCallback = new BaseCallbackHandler(callback, latch, request);
client.execute(request, futureCallback);
return latch;
}
private TResult deserialize(String jsonString, Class<TResult> cls) throws Exception {
return mapper.readValue(jsonString, cls);
}
private String stringContent(HttpResponse responseMessage) throws Exception {
HttpEntity httpEntity = responseMessage.getEntity();
InputStream stream = (InputStream) httpEntity.getContent();
return org.apache.commons.io.IOUtils.toString(stream, "UTF-8");
}
private BasicHttpEntityEnclosingRequest buildRequest() throws Exception {
String url = baseAddress + resourceUrl.getUrl();
BasicHttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest(verb, url);
if (verb.equals("POST") || verb.equals("PUT")) {
if (StringUtils.isNotBlank(httpContent)) {
request.setHeader("Accept", "application/json");
StringEntity entity = new StringEntity(httpContent, Consts.UTF_8);
request.setEntity(entity);
} else if (this.streamContent != null) {
long length = -1;
if (streamContent instanceof FileInputStream) {
if (((FileInputStream)streamContent).getChannel() != null) {
length = ((FileInputStream)streamContent).getChannel().size();
}
} else {
throw new UnsupportedOperationException ("The InputStream is not supported. It must be an instance of FileInputStream.");
}
InputStreamEntity entity = new InputStreamEntity(this.streamContent, length);
request.setEntity(entity);
}
}
if (!headers.containsKey(Headers.CONTENT_TYPE)) {
request.setHeader("Content-type", "application/json; charset=utf-8");
}
if (apiContext != null && apiContext.getUserAuthTicket() != null) {
setUserClaims();
}
Iterator<Entry<String, String>> i = headers.entrySet().iterator();
while (i.hasNext()) {
Entry<String, String> header = i.next();
request.addHeader(header.getKey(), header.getValue());
}
request.addHeader(Headers.X_VOL_APP_CLAIMS, AppAuthenticator.addAuthHeader());
request.addHeader(Headers.X_VOL_VERSION, Version.API_VERSION);
return request;
}
private void setUserClaims() {
AuthTicket newAuthTicket = null;
if (apiContext.getUserAuthTicket().getScope() == AuthenticationScope.Customer)
newAuthTicket = CustomerAuthenticator.ensureAuthTicket(apiContext.getUserAuthTicket());
else
newAuthTicket = UserAuthenticator.ensureAuthTicket(apiContext.getUserAuthTicket());
if (newAuthTicket != null) {
apiContext.getUserAuthTicket().setAccessToken(newAuthTicket.getAccessToken());
apiContext.getUserAuthTicket().setAccessTokenExpiration(
newAuthTicket.getAccessTokenExpiration());
}
addHeader(Headers.X_VOL_USER_CLAIMS, apiContext.getUserAuthTicket().getAccessToken());
}
protected void validateContext() throws Exception {
AppAuthenticator appAuthenticator = AppAuthenticator.getInstance();
if (appAuthenticator == null) {
throw new ApiException("Application has not been authorized to access Mozu.");
}
if (resourceUrl.getLocation() == MozuUrl.UrlLocation.TENANT_POD) {
if (apiContext == null || apiContext.getTenantId() <= 0)
throw new ApiException("TenantId is missing");
if (StringUtils.isBlank(apiContext.getTenantUrl())) {
TenantResource tenantResource = new TenantResource();
Tenant tenant = tenantResource.getTenant(apiContext.getTenantId());
if (tenant == null)
throw new ApiException("Tenant " + apiContext.getTenantId() + " Not found");
baseAddress = HttpHelper.getUrl(tenant.getDomain());
} else {
baseAddress = apiContext.getTenantUrl();
}
}else if (resourceUrl.getLocation() == MozuUrl.UrlLocation.HOME_POD){
if (StringUtils.isBlank(MozuConfig.getBaseUrl())) {
throw new ApiException("Authentication.Instance.BaseUrl is missing");
}
baseAddress = MozuConfig.getBaseUrl();
}else if(resourceUrl.getLocation() == MozuUrl.UrlLocation.PCI_POD){
if(apiContext == null ||apiContext.getTenantId() < 0){
throw new ApiException("TenantId is missing");
}
if (StringUtils.isBlank(MozuConfig.getBasePciUrl())) {
throw new ApiException("Authentication.Instance.BasePciUrl is missing");
}
Tenant tenant=getTenant(apiContext.getTenantId());
baseAddress = tenant.getIsDevTenant()?MozuConfig.getBaseDevPciUrl():MozuConfig.getBasePciUrl();
}
}
private Tenant getTenant(Integer tenantId) throws Exception{
TenantResource tenantResource = new TenantResource();
Tenant tenant = tenantResource.getTenant(tenantId);
if (tenant == null)
throw new ApiException("Tenant " + tenantId + " Not found");
return tenant;
}
protected Map<String, String> getHeaders() {
return headers;
}
protected void ensureSuccess(HttpResponse response, RequestLine requestLine ) throws ApiException {
int statusCode = response.getStatusLine().getStatusCode();
String correlationId = getHeaderValue(Headers.X_VOL_CORRELATION, response);
if (statusCode == 304 || statusCode >= 200 && statusCode <= 300) return;
ApiError apiError = new ApiError();
apiError.setCorrelationId(correlationId);
if (statusCode == 404 && StringUtils.isEmpty(correlationId))
apiError.setMessage(requestLine.getUri().toString() +" not found");
else if (!StringUtils.isEmpty(correlationId)) {
try {
apiError = mapper.readValue(stringContent(response), ApiError.class);
apiError.setCorrelationId(correlationId);
if (apiError.getErrorCode().equals("ITEM_NOT_FOUND") && statusCode == 404 && StringUtils.endsWithIgnoreCase("get",requestLine.getMethod())) return;
} catch (JsonProcessingException jpe) {
throw new ApiException("An error has occurred. Status Code: " + statusCode
+ " Status Message: " + response.getStatusLine().getReasonPhrase(), statusCode);
} catch (IOException ioe) {
throw new ApiException("An error occurred. Status Code: " + statusCode
+ " Status Message: " + response.getStatusLine().getReasonPhrase(), statusCode);
} catch (Exception e) {
throw new ApiException("An error occurred. Status Code: " + statusCode
+ " Status Message: " + response.getStatusLine().getReasonPhrase(), statusCode);
}
} else
apiError.setMessage("Unknown Error");
throw new ApiException(getMozuErrorMessage(apiError), apiError, statusCode);
}
private static String getMozuErrorMessage(ApiError apiError) {
StringBuilder errorMessage = new StringBuilder("Error returned from Mozu. Correlation ID: ");
errorMessage.append(apiError.getCorrelationId());
errorMessage.append(". Message: ");
if (StringUtils.isNotBlank(apiError.getMessage())) {
errorMessage.append(apiError.getMessage());
} else if (apiError.getExceptionDetail() != null && StringUtils.isNotBlank(apiError.getExceptionDetail().getMessage())) {
errorMessage.append(apiError.getExceptionDetail().getMessage());
} else if (apiError.getItems().size() > 0) {
for (Item item : apiError.getItems()) {
errorMessage.append(item.getMessage());
errorMessage.append(". Error Code: ").append(item.getErrorCode());
}
} else {
errorMessage.append("No error message returned from Mozu.");
}
return errorMessage.toString();
}
private String getHeaderValue(String header, HttpMessage httpMessage) {
if (httpMessage.containsHeader(header))
return (httpMessage.getFirstHeader(header)).getValue();
else
return null;
}
private void setCacheKey(BasicHttpEntityEnclosingRequest request) {
StringBuilder key = new StringBuilder();
key.append(request.getRequestLine().getUri().toString());
if (apiContext != null) {
if (apiContext.getSiteId() != null)
key.append(apiContext.getSiteId());
if (!StringUtils.isEmpty(apiContext.getCurrency()))
key.append(apiContext.getCurrency());
if (!StringUtils.isEmpty(apiContext.getLocale()))
key.append(apiContext.getLocale());
if (apiContext.getMasterCatalogId() != null)
key.append(apiContext.getMasterCatalogId());
if (apiContext.getCatalogId() != null)
key.append(apiContext.getCatalogId());
String dataViewMode = getHeaderValue(Headers.X_VOL_DATAVIEW_MODE, request);
if (!StringUtils.isEmpty(dataViewMode))
key.append(dataViewMode);
}
cacheKey = key.toString();
}
@SuppressWarnings("unchecked")
private void setCache(HttpResponse httpResponseMessage) throws Exception {
String eTag = getHeaderValue("ETag", httpResponseMessage);
if (cacheItem != null && httpResponseMessage.getStatusLine().getStatusCode() == 304)
{
//Do nothing
//httpResponseMessage = (CloseableHttpResponse) cacheItem.getItem();
}
else if (StringUtils.isNotEmpty(eTag) && httpResponseMessage.getStatusLine().getStatusCode() != 404)
{
com.mozu.api.cache.CacheManager<CacheItem> cache = (com.mozu.api.cache.CacheManager<CacheItem>) com.mozu.api.cache.CacheManagerFactory.getCacheManager();
if (cache != null) {
cacheItem = new CacheItem();
cacheItem.seteTag(eTag);
cacheItem.setKey(cacheKey);
cacheItem.setContent(stringContent(httpResponseMessage));
cache.put(cacheKey, cacheItem);
}
}
}
protected class DefaultCallbackHandler implements AsyncCallback<TResult> {
@Override
public void success(TResult result) {
syncResult = result;
}
@Override
public void failure(Exception exception) {
syncException = exception;
}
@Override
public void cancelled() {
}
}
protected class BaseCallbackHandler implements FutureCallback<HttpResponse> {
AsyncCallback<TResult> callback;
CountDownLatch latch;
HttpRequest request;
public BaseCallbackHandler(AsyncCallback<TResult> callback, CountDownLatch latch, HttpRequest request) {
this.callback = callback;
this.latch = latch;
this.request = request;
}
@Override
public void failed(Exception ex) {
callback.failure(ex);
latch.countDown();
}
@Override
public void completed(HttpResponse response) {
// TODO: FIX THIS!
// This is for backwards compatability to support getStringContent.
// The contents of syncResponse are transient and cannot be relied on
// to be correct if async mode is used
syncResponse = response;
try {
ensureSuccess(response, request.getRequestLine());
setCache(response);
callback.success(getResult(response));
} catch (ApiException ae) {
// make sure on exception that that response is closed
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e1) {
// Ignore and move on
}
callback.failure(ae);
} catch (Exception e) {
callback.failure(e);
} finally {
latch.countDown();
}
}
@Override
public void cancelled() {
callback.cancelled();
latch.countDown();
}
}
}