/*******************************************************************************
* Copyright (c) 2006-2010 eBay Inc. All Rights Reserved.
* 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
*******************************************************************************/
package org.ebayopensource.turmeric.runtime.sif.impl.transport.http;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Future;
import org.ebayopensource.turmeric.runtime.binding.BindingConstants;
import org.ebayopensource.turmeric.runtime.common.binding.DataBindingDesc;
import org.ebayopensource.turmeric.runtime.common.exceptions.ErrorDataFactory;
import org.ebayopensource.turmeric.runtime.common.exceptions.HTTPTransportException;
import org.ebayopensource.turmeric.runtime.common.exceptions.ServiceException;
import org.ebayopensource.turmeric.runtime.common.impl.utils.HTTPCommonUtils;
import org.ebayopensource.turmeric.runtime.common.pipeline.InboundMessage;
import org.ebayopensource.turmeric.runtime.common.pipeline.Message;
import org.ebayopensource.turmeric.runtime.common.pipeline.MessageContext;
import org.ebayopensource.turmeric.runtime.common.pipeline.OutboundMessage;
import org.ebayopensource.turmeric.runtime.common.pipeline.Transport;
import org.ebayopensource.turmeric.runtime.common.pipeline.TransportOptions;
import org.ebayopensource.turmeric.runtime.common.service.ServiceId;
import org.ebayopensource.turmeric.runtime.common.types.Cookie;
import org.ebayopensource.turmeric.runtime.common.types.SOAConstants;
import org.ebayopensource.turmeric.runtime.common.types.SOAHeaders;
import org.ebayopensource.turmeric.runtime.common.types.ServiceAddress;
import org.ebayopensource.turmeric.runtime.errorlibrary.ErrorConstants;
import org.ebayopensource.turmeric.runtime.sif.pipeline.ClientMessageContext;
import org.ebayopensource.turmeric.runtime.sif.service.ClientServiceId;
import com.ebay.kernel.service.invocation.SvcInvocationConfig;
import com.ebay.kernel.service.invocation.client.exception.BaseClientSideException;
import com.ebay.kernel.service.invocation.client.exception.ConnectionException;
import com.ebay.kernel.service.invocation.client.exception.ConnectionTimeoutException;
import com.ebay.kernel.service.invocation.client.exception.ReceivingException;
import com.ebay.kernel.service.invocation.client.exception.ReceivingTimeoutException;
import com.ebay.kernel.service.invocation.client.exception.SendingException;
import com.ebay.kernel.service.invocation.client.http.HttpClient;
import com.ebay.kernel.service.invocation.client.http.HttpStatusEnum;
import com.ebay.kernel.service.invocation.client.http.Request;
import com.ebay.kernel.service.invocation.client.http.RequestBodyWriter;
import com.ebay.kernel.service.invocation.client.http.Response;
/**
* Transport making use of HTTPClient.
*
* @author rmurphy, wdeng
*/
// TODO - for binary, may have to do transfer encoding and set
// Content-Transfer-Encoding header. Handler
// will do it?
public class HTTPClientTransport implements Transport {
static public final String QUERY_STR = "?";
static public final char AMP_STR = '&';
static public final char EQUAL_STR = '=';
private ClientServiceId m_svcId;
private HttpClient m_client;
private String m_httpVersion;
private boolean m_chunked = false;
private boolean m_zipped = false;
private static Map<String, HTTPClientTransportConfig> s_configs = new HashMap<String, HTTPClientTransportConfig>();
private HTTPClientTransportConfig m_config;
public HTTPClientTransport() {
// empty
}
public void init(InitContext ctx) throws ServiceException {
m_svcId = (ClientServiceId) ctx.getServiceId();
m_config = createSvcConfig(ctx.getServiceId(), ctx.getName(), ctx
.getOptions());
SvcInvocationConfig svcInvConfig = m_config.getSvcInvocationConfig();
m_client = new HttpClient(svcInvConfig, null);
Map<String, String> transportProperties = ctx.getOptions()
.getProperties();
if (null == transportProperties) {
return;
}
String httpVersion = transportProperties.get(SOAConstants.HTTP_VERSION);
if (httpVersion != null
&& httpVersion.equals(SOAConstants.TRANSPORT_HTTP_10)) {
m_httpVersion = Request.HTTP_10;
} else {
m_httpVersion = Request.HTTP_11;
String useChunkedEncoding = transportProperties
.get(SOAConstants.CHUNKED_ENCODING);
m_chunked = useChunkedEncoding != null
&& Boolean.parseBoolean(useChunkedEncoding);
}
String useZipping = transportProperties.get(SOAConstants.GZIP_ENCODING);
m_zipped = useZipping != null && Boolean.parseBoolean(useZipping);
}
private HTTPClientTransportConfig createSvcConfig(ServiceId svcId,
String name, TransportOptions options) {
ClientServiceId clientId = (ClientServiceId) svcId;
StringBuilder sb = new StringBuilder();
sb.append(clientId.getClientName()).append('.').append(
clientId.getAdminName()).append('.').append(name);
String configName = sb.toString();
HTTPClientTransportConfig config;
// ClientServiceDescFactory creates one instance of HTTPClientTransport
// per service. We keep a map of all previously
// initialized config beans by service name and transport name. This
// avoids creation of multiple beans across uses,
// for the same service name and transport name (currently equal to the
// transport name, HTTP10 or HTTP11).
synchronized (s_configs) {
config = s_configs.get(configName);
}
if (config == null) {
config = new HTTPClientTransportConfig(configName, options);
synchronized (s_configs) {
HTTPClientTransportConfig regetconfig = s_configs
.get(configName);
if (regetconfig == null) {
s_configs.put(configName, config);
} else {
config = regetconfig;
}
}
}
return config;
}
// Preinvoke is used only in cases of deferred invoke (e.g. async).
// Other cases do nothing.
public Object preInvoke(MessageContext ctx) throws ServiceException {
OutboundMessage clientRequestMsg = (OutboundMessage) ctx
.getRequestMessage();
// Set the content-type of request only if it has not been set (in
// soap1.2 case,
// this gets over-written at the Client Protocol processor.
// So make sure we are only setting here if it has not already been set)
if (clientRequestMsg
.getTransportHeader(SOAConstants.HTTP_HEADER_CONTENT_TYPE) == null) {
DataBindingDesc binding = clientRequestMsg.getDataBindingDesc();
String mimeType = binding.getMimeType();
Charset charset = clientRequestMsg.getG11nOptions().getCharset();
String contentType = HTTPCommonUtils.formatContentType(mimeType,
charset);
clientRequestMsg.setTransportHeader(
SOAConstants.HTTP_HEADER_CONTENT_TYPE, contentType);
}
return null;
}
public void invoke(Message msg, TransportOptions transportOptions)
throws ServiceException {
ClientMessageContext clientCtx = (ClientMessageContext) msg
.getContext();
ServiceAddress serviceAddress = clientCtx.getServiceAddress();
if (serviceAddress == null || serviceAddress.getServiceUrl() == null) {
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_NO_SERVICE_ADDRESS,
ErrorConstants.ERRORDOMAIN, new Object[] { clientCtx.getAdminName(), clientCtx.getOperationName() }));
}
URL serviceLocation = serviceAddress.getServiceUrl();
String serviceLocationString = serviceLocation.toString();
OutboundMessage clientRequestMsg = (OutboundMessage) msg;
if (clientRequestMsg.isUnserializable()) {
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_UNSERIALIZABLE_MESSAGE,
ErrorConstants.ERRORDOMAIN, new Object[] { clientCtx.getAdminName(),
clientRequestMsg.getUnserializableReason() }));
}
boolean httpGet = clientRequestMsg.isREST();
int httpGetBufferSize = clientRequestMsg.getMaxURLLengthForREST();
String adminName = clientCtx.getAdminName();
if (httpGet) {
if (clientRequestMsg.hasAttachment()) {
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_NO_GET_WITH_ATTACHMENTS,
ErrorConstants.ERRORDOMAIN, new Object[] { adminName, serviceLocationString }));
}
String payloadType = clientRequestMsg.getPayloadType();
// Should ideally allow a data binding to register whether it
// supports REST. However, only
// NV is anticipated in the forseeable future.
if (!payloadType.equals(BindingConstants.PAYLOAD_NV)) {
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_GET_REQUIRES_NV,
ErrorConstants.ERRORDOMAIN, new Object[] { adminName, serviceLocationString }));
}
// Should ideally allow message protocols to register whether they
// suport REST; but we have
// only SOAP, and it does not support REST.
String messageProtocol = clientCtx.getMessageProtocol();
if (!messageProtocol.equals(SOAConstants.MSG_PROTOCOL_NONE)) {
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_NO_GET_WITH_SOAP,
ErrorConstants.ERRORDOMAIN, new Object[] { adminName, serviceLocationString }));
}
}
Map<String, String> transportHeaders = clientRequestMsg
.buildOutputHeaders();
Request request;
if (httpGet) {
request = createHTTPGetRequest(adminName, serviceLocationString,
clientRequestMsg, transportHeaders, httpGetBufferSize);
} else {
request = createHTTPPostRequest(adminName, serviceLocation,
serviceLocationString, clientRequestMsg, transportHeaders);
}
Cookie[] cookies = clientRequestMsg.getCookies();
if (cookies != null && cookies.length != 0) {
StringBuffer buf = new StringBuffer();
HTTPCommonUtils.encodeCookieValue(buf, cookies);
String cookieString = buf.toString();
request.addHeader("Cookie", cookieString);
}
DeferredBodyReaderSelector selector = new DeferredBodyReaderSelector();
DeferredBodyReader bodyReader = selector.getReader();
Response response = sendMessage(serviceLocationString, request,
selector, clientCtx);
setResponseToContextInputStream(response, clientCtx, bodyReader);
}
private Request createHTTPGetRequest(String adminName,
String serviceLocationString, OutboundMessage clientRequestMsg,
Map<String, String> transportHeaders, int httpGetBufferSize)
throws ServiceException {
StringBuilder urlBuffer = new StringBuilder(httpGetBufferSize);
urlBuffer.append(serviceLocationString);
if (urlBuffer.indexOf(QUERY_STR) != -1) {
// already have the query start
urlBuffer.append(AMP_STR);
} else {
urlBuffer.append(QUERY_STR);
}
// The bytes here will be in UTF-8 or whatever encoding is passed via
// the g11n options in the
// clientRequestMsg - indicated by "charset" below.
//
// TODO do we want to do a more efficient serialize process in which we
// go directly to an OutputStream with
// an underlying StringBuffer? XMLStreamWriters deal in character data.
// However, all SOA output is normally based
// on output streams. Since Get encodings are normally small, we can
// probably leave it the way it is.
byte[] httpPayloadData = serializeRequest(clientRequestMsg);
String httpPayloadString;
try {
String charset = clientRequestMsg.getG11nOptions().getCharset()
.name();
httpPayloadString = new String(httpPayloadData, charset);
} catch (Exception e) { // UnsupportedEncodingException, etc.
throw new ServiceException(ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_UNSERIALIZABLE_MESSAGE,
ErrorConstants.ERRORDOMAIN, new Object[] { m_svcId.getAdminName(), e.toString() }), e);
}
// this is a SOA-specific parameter, that is only analyzed on the server
// end
// until we have free-form HTTP paramters, we should not need it on the
// client end
// urlBuffer.append(SOAHeaders.REST_PAYLOAD);
// urlBuffer.append("=true");
// urlBuffer.append(AMP_STR);
urlBuffer.append(httpPayloadString);
int urlLength = urlBuffer.length();
String urlString = urlBuffer.toString();
if (urlLength > httpGetBufferSize) {
String urlStringToLog = getUrlLogString(urlString, urlLength);
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_URL_TOO_LONG,
ErrorConstants.ERRORDOMAIN, new Object[] {adminName, urlStringToLog,Integer.valueOf(urlLength) }));
}
Request request;
try {
request = new Request(urlString);
} catch (MalformedURLException e) {
String urlStringToLog = getUrlLogString(urlString, urlLength);
throw new ServiceException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_INVALID_SERVICE_ADDRESS,
ErrorConstants.ERRORDOMAIN, new Object[] { adminName, urlStringToLog }), e);
}
request.setHttpVersion(m_httpVersion);
addTransportHeaders(transportHeaders, request);
request.setMethod(Request.GET);
return request;
}
private String getUrlLogString(String urlString, int urlLength) {
if (urlLength > 80) {
return urlString.substring(0, 80);
}
return urlString;
}
private Request createHTTPPostRequest(String adminName,
URL serviceLocation, String serviceLocationString,
OutboundMessage clientRequestMsg,
Map<String, String> transportHeaders) throws ServiceException {
Request request = new Request(serviceLocation);
request.setMethod(Request.POST);
request.setHttpVersion(m_httpVersion);
addTransportHeaders(transportHeaders, request);
if (m_chunked) {
request.setChunkedEncoding();
request.addHeader("Transfer-Encoding", "chunked");
RequestBodyWriter outWriter = new StreamingMessageBodyWriter(
clientRequestMsg);
request.setBodyWriter(outWriter);
} else {
byte[] httpPayloadData = serializeRequest(clientRequestMsg);
request.setRawData(httpPayloadData);
}
if (m_zipped) {
request.addHeader(SOAConstants.HTTP_HEADER_ACCEPT_ENCODING, "gzip");
}
return request;
}
private void addTransportHeaders(Map<String, String> transportHeaders,
Request request) {
if (transportHeaders == null || transportHeaders.isEmpty()) {
return;
}
for (Map.Entry<String, String> entry : transportHeaders.entrySet()) {
String header = entry.getKey();
String value = entry.getValue();
request.addHeader(header, value);
}
}
private byte[] serializeRequest(OutboundMessage clientRequestMsg)
throws ServiceException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(8192);
clientRequestMsg.serialize(bos);
return bos.toByteArray();
}
private Response sendMessage(String serviceLocationString, Request request,
DeferredBodyReaderSelector selector, ClientMessageContext clientCtx)
throws ServiceException {
Response response = null;
try {
// response = m_client.invokeWithSelector(request, selector);
response = m_client.invoke(request);
} catch (ConnectionTimeoutException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_CONNECT_TIMEOUT_EXCEPTION,
ErrorConstants.ERRORDOMAIN, new Object[] { serviceLocationString,
m_svcId.getAdminName(), e.toString() }), -1, e);
} catch (ConnectionException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_CONNECT_EXCEPTION,
ErrorConstants.ERRORDOMAIN, new Object[] { serviceLocationString,
m_svcId.getAdminName(), e.toString() }), -1, e);
} catch (ReceivingTimeoutException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_INBOUND_IO_EXCEPTION,
ErrorConstants.ERRORDOMAIN, new Object[] { m_svcId.getAdminName(), e.toString(),
serviceLocationString }), -1, e);
} catch (ReceivingException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_INBOUND_IO_EXCEPTION,
ErrorConstants.ERRORDOMAIN, new Object[] { m_svcId.getAdminName(), e.toString(),
serviceLocationString }), -1, e);
} catch (SendingException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_OUTBOUND_IO_EXCEPTION,
ErrorConstants.ERRORDOMAIN, new Object[] { m_svcId.getAdminName(), e.toString(),
serviceLocationString }), -1, e);
} catch (BaseClientSideException e) {
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_COMM_FAILURE,
ErrorConstants.ERRORDOMAIN, new Object[] {serviceLocationString, e.toString() }), -1, e);
}
setResponseToContextHeaderMapping(response, clientCtx, selector
.getReader());
if (response.getRequestStatus() == HttpStatusEnum.SUCCESS) {
return response;
}
boolean isSoap = false;
if (clientCtx.getMessageProtocol().equals(
SOAConstants.MSG_PROTOCOL_SOAP_11)
|| clientCtx.getMessageProtocol().equals(
SOAConstants.MSG_PROTOCOL_SOAP_12)) {
isSoap = true;
}
int httpStatusCode = response.getStatusCode();
if (httpStatusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
if (isSoap) {
// if HTTP 500 and SOAP, we will manually set the ERROR_RESPON
// header, to handler the case when
// we are talking to foreign SOAP web servers that doesn't set
// the ERROR_RESPONSE header.
InboundMessage responseMsg = (InboundMessage) clientCtx
.getResponseMessage();
responseMsg.setTransportHeader(SOAHeaders.ERROR_RESPONSE,
"true");
}
// if (response.getHeader(SOAHeaders.ERROR_RESPONSE) != null ||
// isSoap) {
if (((InboundMessage) clientCtx.getResponseMessage())
.getTransportHeader(SOAHeaders.ERROR_RESPONSE) != null
|| isSoap) {
return response;
}
generateExceptionMessage(serviceLocationString, response,
httpStatusCode);
}
String responseBody = getAnyOtherErrorAsString(response);
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_HTTP_ERROR,
ErrorConstants.ERRORDOMAIN, new Object[] {serviceLocationString,
response.getRequestStatus().getName(),
Integer.valueOf(httpStatusCode), responseBody }), httpStatusCode, null);
}
private String getAnyOtherErrorAsString(
Response response) {
String responseBody = " ";
final String responseBodyStr = response.getBody() == null ? " "
: response.getBody();
responseBody = responseBodyStr
.substring(0, responseBodyStr.length() > 256 ? 256
: responseBodyStr.length());
return responseBody;
}
private static final int RESPONSE_BODY_LIMIT = 256;
private static final String LOOKUP_BODY_TEXT = "HTTP Status 500";
private void generateExceptionMessage(String serviceLocationString,
Response response, int httpStatusCode)
throws HTTPTransportException {
String responseBody = get500ErrorResponseAsString(response);
throw new HTTPTransportException(
ErrorDataFactory.createErrorData(ErrorConstants.SVC_TRANSPORT_HTTP_ERROR,
ErrorConstants.ERRORDOMAIN, new Object[] {
serviceLocationString,
response.getRequestStatus().getName(),
Integer.valueOf(httpStatusCode), responseBody }),
httpStatusCode, null);
}
private String get500ErrorResponseAsString(Response response) {
String responseBody = " ";
final String responseBodyStr = response.getBody() == null ? " "
: response.getBody();
int startIndex = responseBodyStr.indexOf(LOOKUP_BODY_TEXT);
startIndex = startIndex > 0 ? startIndex + LOOKUP_BODY_TEXT.length()
: 0;
try {
responseBody = responseBodyStr.substring(startIndex,
(responseBodyStr.length() > startIndex
+ RESPONSE_BODY_LIMIT ? startIndex
+ RESPONSE_BODY_LIMIT : Math.min(startIndex
+ RESPONSE_BODY_LIMIT, responseBodyStr.length())));
} catch (StringIndexOutOfBoundsException e) {
responseBody = responseBodyStr.substring(0, Math.min(
RESPONSE_BODY_LIMIT, responseBodyStr.length()));
}
return responseBody;
}
private void setResponseToContextHeaderMapping(Response httpClientResponse,
ClientMessageContext clientCtx, DeferredBodyReader bodyReader)
throws ServiceException {
InboundMessage clientResponse = (InboundMessage) clientCtx
.getResponseMessage();
@SuppressWarnings("deprecation")
Enumeration headers = httpClientResponse.getHeaders();
while (headers.hasMoreElements()) {
String header = (String) headers.nextElement();
String value = httpClientResponse.getHeader(header);
// HOT FIX: For Porlet call, there's a compatibility issue regarding
// Upper case vs lower case operation.
// Here, we want to make the first letter of the operation name
// lower case always,
// to handle 589 client talking to 587 server scenario
// CLIENT SIDE RECEIVING SIDE LOGIC
if (header != null
&& header
.equalsIgnoreCase(SOAHeaders.SERVICE_OPERATION_NAME)
&& value != null && value.equals("Portlet")) {
StringBuffer sb = new StringBuffer();
sb.append(Character.toLowerCase(value.charAt(0)));
sb.append(value.substring(1));
value = sb.toString();
}
clientResponse.setTransportHeader(header, value);
}
Iterator cookies = httpClientResponse.getCookies();
while (cookies.hasNext()) {
String cookieString = (String) cookies.next();
clientResponse.setCookie(HTTPCommonUtils
.parseSetCookieValue(cookieString));
}
}
private void setResponseToContextInputStream(Response httpClientResponse,
ClientMessageContext clientCtx, DeferredBodyReader bodyReader)
throws ServiceException {
InboundMessage clientResponse = (InboundMessage) clientCtx
.getResponseMessage();
byte[] rawData = httpClientResponse.getRawData();
if (clientCtx.isOutboundRawMode()) {
clientResponse.setByteBuffer(ByteBuffer.wrap(rawData));
} else {
ByteArrayInputStream bis = new ByteArrayInputStream(rawData);
clientResponse.setInputStream(bis);
}
// httpClientResponse.getRawData();
// InputStream responseInputStream = bodyReader.getInputStream();
// clientResponse.setInputStream(responseInputStream);
}
public Future<?> invokeAsync(Message msg, TransportOptions transportOptions)
throws ServiceException {
throw new UnsupportedOperationException(
"HTTPClientTransport transport doesn't support invokeAsync, use HTTPSyncAsyncClientTransport");
}
public void retrieve(MessageContext ctx, Future<?> futureResp)
throws ServiceException {
throw new UnsupportedOperationException(
"HTTPClientTransport transport doesn't support retrieve");
}
public boolean supportsPoll() {
return false;
}
public HTTPClientTransportConfig getConfig() {
return m_config;
}
}