/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.synapse.transport.nhttp;
import org.apache.axiom.om.OMException;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.impl.llom.soap11.SOAP11Factory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.builder.BuilderUtil;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.TransportUtils;
import org.apache.axis2.transport.http.HTTPTransportUtils;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.synapse.transport.customlogsetter.CustomLogSetter;
import org.apache.synapse.transport.nhttp.debug.ClientConnectionDebug;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
/**
* Performs processing of the HTTP response received for our outgoing request. An instance of this
* class is created to process each unique response.
*/
public class ClientWorker implements Runnable {
private static final Log log = LogFactory.getLog(ClientWorker.class);
/** the Axis2 configuration context */
private ConfigurationContext cfgCtx = null;
/** the response message context that would be created */
private MessageContext responseMsgCtx = null;
/** Request Message context */
private MessageContext outMsgCtx = null;
/** the InputStream out of which the response body should be read */
private InputStream in = null;
/** the HttpResponse received */
private HttpResponse response = null;
/** the endpoint URL prefix */
private String endpointURLPrefix = null;
/**
* Create the thread that would process the response message received for the outgoing message
* context sent
* @param cfgCtx the Axis2 configuration context
* @param in the InputStream to read the body of the response message received
* @param response HTTP response received from the server
* @param outMsgCtx the original outgoing message context (i.e. corresponding request)
* @param endpointURLPrefix The endpoint URL prefix
*/
public ClientWorker(ConfigurationContext cfgCtx, InputStream in,
HttpResponse response, MessageContext outMsgCtx, String endpointURLPrefix) {
this.cfgCtx = cfgCtx;
this.in = in;
this.response = response;
this.endpointURLPrefix = endpointURLPrefix;
this.outMsgCtx = outMsgCtx;
try {
responseMsgCtx = outMsgCtx.getOperationContext().
getMessageContext(WSDL2Constants.MESSAGE_LABEL_IN);
// fix for RM to work because of a soapAction and wsaAction conflict
if (responseMsgCtx != null) {
responseMsgCtx.setSoapAction("");
}
} catch (AxisFault af) {
log.error("Error getting IN message context from the operation context", af);
return;
}
// this conditional block is to support Sandesha, as it uses an out-in mep, but without
// creating the message context to write the response and adding it into the operation
// context, as it may get a 202 accepted or 200. So if the operation is complete ignore
// this message, else, create a new message context and handle this
if (responseMsgCtx == null && outMsgCtx.getOperationContext().isComplete()) {
if (log.isDebugEnabled()) {
log.debug("Error getting IN message context from the operation context. " +
"Possibly an RM terminate sequence message");
}
} else {
if (responseMsgCtx == null) {
responseMsgCtx = new MessageContext();
responseMsgCtx.setOperationContext(outMsgCtx.getOperationContext());
}
responseMsgCtx.setProperty(MessageContext.IN_MESSAGE_CONTEXT, outMsgCtx);
responseMsgCtx.setServerSide(true);
responseMsgCtx.setDoingREST(outMsgCtx.isDoingREST());
responseMsgCtx.setProperty(MessageContext.TRANSPORT_IN, outMsgCtx
.getProperty(MessageContext.TRANSPORT_IN));
responseMsgCtx.setTransportIn(outMsgCtx.getTransportIn());
responseMsgCtx.setTransportOut(outMsgCtx.getTransportOut());
// set any transport headers received
Header[] headers = response.getAllHeaders();
if (headers != null && headers.length > 0) {
Map<String, String> headerMap
= new TreeMap<String, String>(new Comparator<String>() {
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
String servicePrefix = (String)outMsgCtx.getProperty(NhttpConstants.SERVICE_PREFIX);
for (int i=0; i<headers.length; i++) {
Header header = headers[i];
// if this header is already added
if (headerMap.containsKey(header.getName())) {
/* this is a multi-value header */
// generate the key
String key = NhttpConstants.EXCESS_TRANSPORT_HEADERS;
// get the old value
String oldValue = headerMap.get(header.getName());
// adds additional values to a list in a property of
// message context
Map map;
if (responseMsgCtx.getProperty(key) != null) {
map = (Map) responseMsgCtx.getProperty(key);
map.put(header.getName(), oldValue);
} else {
map = new MultiValueMap();
map.put(header.getName(), oldValue);
// set as a property in message context
responseMsgCtx.setProperty(key, map);
}
}
if ("Location".equals(header.getName())
&& endpointURLPrefix != null && servicePrefix != null) {
//Here, we are changing only the host name and the port of the new URI - value of the Location
//header.
//If the new URI is again referring to a resource in the server to which the original request
//is sent, then replace the hostname and port of the URI with the hostname and port of synapse
//We are not changing the request url here, only the host name and the port.
try {
URI serviceURI = new URI(servicePrefix);
URI endpointURI = new URI(endpointURLPrefix);
URI locationURI = new URI(header.getValue());
if ((locationURI.getHost().equalsIgnoreCase(endpointURI.getHost())) &&
(locationURI.getPort() == endpointURI.getPort())) {
URI newURI = new URI(locationURI.getScheme(), locationURI.getUserInfo(),
serviceURI.getHost(), serviceURI.getPort(), locationURI.getPath(),
locationURI.getQuery(), locationURI.getFragment());
headerMap.put(header.getName(), newURI.toString());
responseMsgCtx.setProperty(NhttpConstants.SERVICE_PREFIX,
outMsgCtx.getProperty(NhttpConstants.SERVICE_PREFIX));
} else {
headerMap.put(header.getName(), header.getValue());
}
} catch (URISyntaxException e) {
log.error(e.getMessage(), e);
}
} else {
headerMap.put(header.getName(), header.getValue());
}
}
responseMsgCtx.setProperty(MessageContext.TRANSPORT_HEADERS, headerMap);
}
responseMsgCtx.setAxisMessage(outMsgCtx.getOperationContext().getAxisOperation().
getMessage(WSDLConstants.MESSAGE_LABEL_IN_VALUE));
responseMsgCtx.setOperationContext(outMsgCtx.getOperationContext());
responseMsgCtx.setConfigurationContext(outMsgCtx.getConfigurationContext());
responseMsgCtx.setTo(null);
//Flag to keep track on original messageType on response path
String messageType = (String) outMsgCtx.getProperty(NhttpConstants.MESSAGE_TYPE);
if (messageType != null) {
responseMsgCtx.setProperty(NhttpConstants.ORIGINAL_MESSAGE_TYPE, messageType);
}
// Ensure MessageContext has a ClientConnectionDebug attached before we start streaming
ClientConnectionDebug cd = (ClientConnectionDebug)
outMsgCtx.getProperty(ClientHandler.CLIENT_CONNECTION_DEBUG);
if (cd != null) {
responseMsgCtx.setProperty(ClientHandler.CLIENT_CONNECTION_DEBUG, cd);
}
}
setServerContextAttribute(NhttpConstants.CLIENT_WORKER_INIT_TIME,
System.currentTimeMillis(), outMsgCtx);
}
/**
* Process the received response through Axis2
*/
public void run() {
CustomLogSetter.getInstance().clearThreadLocalContent();
setServerContextAttribute(NhttpConstants.CLIENT_WORKER_START_TIME,
System.currentTimeMillis(), outMsgCtx);
// to support Sandesha.. if there isn't a response message context, we cannot read any
// response and populate it with the soap envelope
if (responseMsgCtx == null) {
return;
}
try {
if (in != null) {
Header cType = response.getFirstHeader(HTTP.CONTENT_TYPE);
String contentType;
if (cType != null) {
// This is the most common case - Most of the time servers send the Content-Type
contentType = cType.getValue();
} else {
// Server hasn't sent the header - Try to infer the content type
contentType = inferContentType();
}
String charSetEnc = BuilderUtil.getCharSetEncoding(contentType);
if (charSetEnc == null) {
charSetEnc = MessageContext.DEFAULT_CHAR_SET_ENCODING;
}
responseMsgCtx.setProperty(
Constants.Configuration.CHARACTER_SET_ENCODING, charSetEnc);
// workaround for Axis2 TransportUtils.createSOAPMessage() issue, where a response
// of content type "text/xml" is thought to be REST if !MC.isServerSide(). This
// question is still under debate and due to the timelines, I am commiting this
// workaround as Axis2 1.2 is about to be released and Synapse 1.0
responseMsgCtx.setServerSide(false);
SOAPEnvelope envelope;
try {
envelope = TransportUtils.createSOAPMessage(
responseMsgCtx,
HTTPTransportUtils.handleGZip(responseMsgCtx, in),
contentType);
} catch (OMException e) {
// handle non SOAP and POX/REST payloads (probably text/html)
String errorMessage = "Unexpected response received. HTTP response code : "
+ this.response.getStatusLine().getStatusCode() + " HTTP status : "
+ this.response.getStatusLine().getReasonPhrase() + " exception : "
+ e.getMessage();
log.warn(errorMessage);
if (log.isDebugEnabled()) {
log.debug(errorMessage, e);
log.debug("Creating the SOAPFault to be injected...");
}
SOAPFactory factory = new SOAP11Factory();
envelope = factory.getDefaultFaultEnvelope();
SOAPFaultDetail detail = factory.createSOAPFaultDetail();
detail.setText(errorMessage);
envelope.getBody().getFault().setDetail(detail);
SOAPFaultReason reason = factory.createSOAPFaultReason();
reason.setText(errorMessage);
envelope.getBody().getFault().setReason(reason);
SOAPFaultCode code = factory.createSOAPFaultCode();
code.setText(Integer.toString(this.response.getStatusLine().getStatusCode()));
envelope.getBody().getFault().setCode(code);
}
responseMsgCtx.setServerSide(true);
responseMsgCtx.setEnvelope(envelope);
} else {
// there is no response entity-body
responseMsgCtx.setProperty(NhttpConstants.NO_ENTITY_BODY, Boolean.TRUE);
responseMsgCtx.setEnvelope(new SOAP11Factory().getDefaultEnvelope());
}
// copy the HTTP status code as a message context property with the key HTTP_SC to be
// used at the sender to set the propper status code when passing the message
int statusCode = this.response.getStatusLine().getStatusCode();
responseMsgCtx.setProperty(NhttpConstants.HTTP_SC, statusCode);
if (statusCode >= 400) {
responseMsgCtx.setProperty(NhttpConstants.FAULT_MESSAGE, NhttpConstants.TRUE);
}
responseMsgCtx.setProperty(NhttpConstants.NON_BLOCKING_TRANSPORT, true);
if (endpointURLPrefix != null) {
responseMsgCtx.setProperty(NhttpConstants.ENDPOINT_PREFIX, endpointURLPrefix);
}
// process response received
try {
AxisEngine.receive(responseMsgCtx);
} catch (AxisFault af) {
// This will be reached if an exception is thrown within an Axis2 handler
String errorMessage = "Fault processing response message through Axis2: " +
af.getMessage();
log.warn(errorMessage);
if (log.isDebugEnabled()) {
log.debug(errorMessage, af);
log.debug("Directly invoking SynapseCallbackReceiver after setting " +
"error properties");
}
responseMsgCtx.setProperty(
NhttpConstants.SENDING_FAULT, Boolean.TRUE);
responseMsgCtx.setProperty(
NhttpConstants.ERROR_CODE, NhttpConstants.RESPONSE_PROCESSING_FAILURE);
responseMsgCtx.setProperty(
NhttpConstants.ERROR_MESSAGE, errorMessage.split("\n")[0]);
responseMsgCtx.setProperty(
NhttpConstants.ERROR_DETAIL, JavaUtils.stackToString(af));
responseMsgCtx.setProperty(
NhttpConstants.ERROR_EXCEPTION, af);
responseMsgCtx.getAxisOperation().getMessageReceiver().receive(responseMsgCtx);
}
} catch (AxisFault af) {
log.error("Fault creating response SOAP envelope", af);
return;
} catch (XMLStreamException e) {
log.error("Error creating response SOAP envelope", e);
} catch (IOException e) {
log.error("Error closing input stream from which message was read", e);
} finally {
// this is the guaranteed location to close the RESPONSE_SOURCE_CHANNEL that was used
// to read the response back from the server.
try {
if (in != null) {
in.close();
}
} catch (IOException ignore) {}
}
}
private String inferContentType() {
// Try to get the content type from the message context
Object cTypeProperty = responseMsgCtx.getProperty(NhttpConstants.CONTENT_TYPE);
if (cTypeProperty != null) {
return cTypeProperty.toString();
}
// Try to get the content type from the axis configuration
Parameter cTypeParam = cfgCtx.getAxisConfiguration().getParameter(
NhttpConstants.CONTENT_TYPE);
if (cTypeParam != null) {
return cTypeParam.getValue().toString();
}
// Unable to determine the content type - Return default value
return NhttpConstants.DEFAULT_CONTENT_TYPE;
}
private void setServerContextAttribute(String key, Object value, MessageContext msgCtx) {
if (msgCtx != null) {
Object outTransport = msgCtx.getProperty(Constants.OUT_TRANSPORT_INFO);
if (outTransport != null && outTransport instanceof org.apache.synapse.transport.nhttp.ServerWorker) {
HttpContext context = ((org.apache.synapse.transport.nhttp.ServerWorker) outTransport).getConn().getContext();
context.setAttribute(key, value);
}
}
}
}