/*
* Copyright (c) 2005-2008, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.passthru;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.nio.NHttpClientConnection;
import org.apache.synapse.transport.http.conn.ProxyConfig;
import org.apache.synapse.transport.http.conn.SynapseDebugInfoHolder;
import org.apache.synapse.transport.passthru.config.TargetConfiguration;
import org.apache.synapse.transport.passthru.connections.TargetConnections;
import org.apache.synapse.transport.passthru.util.TargetRequestFactory;
import java.io.OutputStream;
import java.util.Queue;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class acts as a gateway for differed delivery of the messages. When a message is to be
* delivered it is submitted to this class. If a connection is available to the target this
* class will try to deliver the message immediately over that connection. If a connection is
* not available it will queue the message and request a connection from the pool. When a new
* connection is available a queued message will be sent through it.
*/
public class DeliveryAgent {
private static final Log log = LogFactory.getLog(DeliveryAgent.class);
/**
* This Map holds the messages that need to be delivered. But at the moment maximum
* number of connections to the host:pair is being used. So these messages has to wait
* until a new connection is available.
*/
private Map<HttpRoute, Queue<MessageContext>> waitingMessages =
new ConcurrentHashMap<HttpRoute, Queue<MessageContext>>();
/** The connection management */
private TargetConnections targetConnections;
/** Configuration of the sender */
private TargetConfiguration targetConfiguration;
/** Proxy config */
private ProxyConfig proxyConfig;
/** The maximum number of messages that can wait for a connection */
private int maxWaitingMessages = Integer.MAX_VALUE;
private TargetErrorHandler targetErrorHandler;
/** Lock for synchronizing access */
private Lock lock = new ReentrantLock();
/**
* Create a delivery agent with the target configuration and connection management.
*
* @param targetConfiguration configuration of the sender
* @param targetConnections connection management
*/
public DeliveryAgent(TargetConfiguration targetConfiguration,
TargetConnections targetConnections,
ProxyConfig proxyConfig) {
this.targetConfiguration = targetConfiguration;
this.targetConnections = targetConnections;
this.proxyConfig = proxyConfig;
this.targetErrorHandler = new TargetErrorHandler(targetConfiguration);
}
/**
* This method queues the message for delivery. If a connection is already existing for
* the destination epr, the message will be delivered immediately. Otherwise message has
* to wait until a connection is established. In this case this method will inform the
* system about the need for a connection.
*
* @param msgContext the message context to be sent
* @param epr the endpoint to which the message should be sent
* @throws AxisFault if an error occurs
*/
public void submit(MessageContext msgContext, EndpointReference epr)
throws AxisFault {
try {
URL url = new URL(epr.getAddress());
String scheme = url.getProtocol() != null ? url.getProtocol() : "http";
String hostname = url.getHost();
int port = url.getPort();
if (port == -1) {
// use default
if ("http".equals(scheme)) {
port = 80;
} else if ("https".equals(scheme)) {
port = 443;
}
}
HttpHost target = new HttpHost(hostname, port, scheme);
boolean secure = "https".equalsIgnoreCase(target.getSchemeName());
HttpHost proxy = proxyConfig.selectProxy(target);
msgContext.setProperty(PassThroughConstants.PROXY_PROFILE_TARGET_HOST, target.getHostName());
HttpRoute route;
if (proxy != null) {
route = new HttpRoute(target, null, proxy, secure);
} else {
route = new HttpRoute(target, null, secure);
}
// first we queue the message
Queue<MessageContext> queue = null;
lock.lock();
try {
queue = waitingMessages.get(route);
if (queue == null) {
queue = new ConcurrentLinkedQueue<MessageContext>();
waitingMessages.put(route, queue);
}
if (queue.size() == maxWaitingMessages) {
MessageContext msgCtx = queue.poll();
targetErrorHandler.handleError(msgCtx,
ErrorCodes.CONNECTION_TIMEOUT,
"Error connecting to the back end",
null,
ProtocolState.REQUEST_READY);
}
queue.add(msgContext);
} finally {
lock.unlock();
}
NHttpClientConnection conn = targetConnections.getConnection(route);
if (conn != null) {
conn.resetInput();
conn.resetOutput();
MessageContext messageContext = queue.poll();
if (messageContext != null) {
tryNextMessage(messageContext, route, conn);
}
}
} catch (MalformedURLException e) {
handleException("Malformed URL in the target EPR", e);
}
}
public void errorConnecting(HttpRoute route, int errorCode, String message) {
Queue<MessageContext> queue = waitingMessages.get(route);
if (queue != null) {
MessageContext msgCtx = queue.poll();
if (msgCtx != null) {
targetErrorHandler.handleError(msgCtx, errorCode,
"Error connecting to the back end", null,
ProtocolState.REQUEST_READY);
synchronized (msgCtx) {
msgCtx.setProperty(PassThroughConstants.WAIT_BUILDER_IN_STREAM_COMPLETE,
Boolean.TRUE);
msgCtx.notifyAll();
}
}
} else {
throw new IllegalStateException("Queue cannot be null for: " + route);
}
}
/**
* Notification for a connection availability. When this occurs a message in the
* queue for delivery will be tried.
*
* @param host name of the remote host
* @param port remote port number
*/
public void connected(HttpRoute route, NHttpClientConnection conn) {
Queue<MessageContext> queue = null;
lock.lock();
try {
queue = waitingMessages.get(route);
} finally {
lock.unlock();
}
while (queue.size() > 0) {
if(conn == null) {
conn = targetConnections.getExistingConnection(route);
}
if (conn != null) {
MessageContext messageContext = queue.poll();
if (messageContext != null) {
tryNextMessage(messageContext, route, conn);
conn = null;
}
} else {
break;
}
}
//releasing the connection to pool when connection is not null and message queue is empty,
//otherwise connection remains in busyConnections pool till it gets timeout and It is not used.
if(conn != null && TargetContext.getState(conn) == ProtocolState.REQUEST_READY) {
targetConfiguration.getConnections().releaseConnection(conn);
}
}
private void tryNextMessage(MessageContext messageContext, HttpRoute route, NHttpClientConnection conn) {
if (conn != null) {
try {
TargetContext.updateState(conn, ProtocolState.REQUEST_READY);
TargetContext.get(conn).setRequestMsgCtx(messageContext);
submitRequest(conn, route, messageContext);
} catch (AxisFault e) {
log.error("IO error while sending the request out", e);
}
}
}
private void submitRequest(NHttpClientConnection conn, HttpRoute route, MessageContext msgContext) throws AxisFault {
if (log.isDebugEnabled()) {
log.debug("Submitting new request to the connection: " + conn);
}
if (SynapseDebugInfoHolder.getInstance().isDebuggerEnabled()) {
conn.getContext().setAttribute(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_HOLDER_PROPERTY,
msgContext.getProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_HOLDER_PROPERTY));
conn.getContext().setAttribute(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_MEDIATOR_ID_PROPERTY,
msgContext.getProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_MEDIATOR_ID_PROPERTY));
}
TargetRequest request = TargetRequestFactory.create(msgContext, route, targetConfiguration);
TargetContext.setRequest(conn, request);
Pipe pipe = (Pipe) msgContext.getProperty(PassThroughConstants.PASS_THROUGH_PIPE);
if (pipe != null) {
pipe.attachConsumer(conn);
request.connect(pipe);
if (Boolean.TRUE.equals(msgContext.getProperty(PassThroughConstants.MESSAGE_BUILDER_INVOKED))) {
synchronized (msgContext) {
OutputStream out = pipe.getOutputStream();
msgContext.setProperty(PassThroughConstants.BUILDER_OUTPUT_STREAM, out);
msgContext.setProperty(PassThroughConstants.WAIT_BUILDER_IN_STREAM_COMPLETE, Boolean.TRUE);
msgContext.notifyAll();
}
return;
}
}
conn.requestOutput();
}
/**
* Throws an AxisFault if an error occurs at this level
* @param s a message describing the error
* @param e original exception leads to the error condition
* @throws AxisFault wrapping the original exception
*/
private void handleException(String s, Exception e) throws AxisFault {
log.error(s, e);
throw new AxisFault(s, e);
}
}