/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, JBoss Inc., and others contributors as indicated 
 * by the @authors tag. All rights reserved. 
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors. 
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
 * MA  02110-1301, USA.
 * 
 * (C) 2005-2006,
 * @author JBoss Inc.
 */
package org.jboss.soa.esb.actions.bpel;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.internal.soa.esb.actions.bpel.ESBInvocationAdapter;
import org.jboss.soa.esb.actions.AbstractActionLifecycle;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.message.format.MessageFactory;

import javax.naming.*;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.jboss.soa.bpel.runtime.engine.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class BPELInvoke extends AbstractActionLifecycle {
    
	private static final String TOP_LEVEL_ELEMENT_NAME = "message";
	protected static final String REQUEST_PART_NAME = "requestPartName";
	protected static final String RESPONSE_PART_NAME = "responsePartName";
	protected static final String SERVICE = "service";
	protected static final String PORT = "port";
	protected static final String OPERATION = "operation";
	protected static final String ABORT_ON_FAULT = "abortOnFault";
	
	protected static final String BODY_FAULT_CODE = "org.jboss.soa.esb.message.fault.detail.code";
	protected static final String BODY_BPEL_FAULT_CODE = "org.jboss.soa.bpel.message.fault.detail.code";
	
	protected ConfigTree	_config;
	private static BPELEngine	_bpelEngine;
	  
	public BPELInvoke(ConfigTree config) {
		_config = config;
	} 
	
	/**
	 * This method returns a reference to the BPEL Engine.
	 * 
	 * @return The BPEL engine
	 * @throws Exception Failed to obtain a reference to the
	 * 					BPEL engine
	 */
	protected static BPELEngine getBPELEngine() throws Exception {

		// If BPEL engine not set, then locate from JNDI
		if (_bpelEngine == null) {
			InitialContext context=new InitialContext();
			_bpelEngine = (BPELEngine)context.lookup(BPELEngine.Service);
		}
		
		return(_bpelEngine);
	}
	
	/**
	 * This method sets the reference to the BPEL engine.
	 * 
	 * @param engine The BPEL engine
	 */
	protected void setBPELEngine(BPELEngine engine) {
		_bpelEngine = engine;
	}
  
	/**
	 * This method processes the supplied message, and optionally
	 * returns a response message.
	 * 
	 * @param message The request message
	 * @return The optional response message
	 * @throws Exception Failed to process the message
	 */
	public Message process(Message message) throws Exception {
		Message ret=null;
		
		// Check the operation and service details are available
		if (_config.getAttribute(OPERATION) == null) {
			throw new RuntimeException("Property '"+OPERATION+"' has not been specified");
		}
		
		if (_config.getAttribute(SERVICE) == null) {
			throw new RuntimeException("Property '"+SERVICE+"' has not been specified");
		}
	
		BPELEngine engine=getBPELEngine();
		
		if (engine == null) {
			throw new RuntimeException("Failed to locate BPEL engine");
		}
		
		// Create MessagePayloadProxy
		MessagePayloadProxy proxy=new MessagePayloadProxy(_config);
		
		String requestPartName=_config.getAttribute(REQUEST_PART_NAME);
		
		logger.debug("Request: "+message);

		boolean f_toText=false;
		org.w3c.dom.Element mesgElem=null;
		
		// Get default body value
		Object value=proxy.getPayload(message); //message.getBody().get();
		Node node=null;
			
		if (value instanceof String) {

			// Convert to element
			node = getNode((String)value);
			
			// Convert any response to text
			f_toText = true;
		} else if (value instanceof Node) {
			node = (Node)value;
		}

		if (node == null) {
			throw new RuntimeException("Failed to obtain DOM representation of message value");
		}
		
		logger.debug("Node type is: "+node.getNodeType()+" requestPartName="+requestPartName);
		
		// If value is an element, and no request part name has been provided,
		// then pass the DOM element through as is.
		if (node.getNodeType() == Node.ELEMENT_NODE &&
							requestPartName == null) {
			mesgElem = (org.w3c.dom.Element)node;
			
		} else if (requestPartName == null) {
			// Not possible to pass anything other than an Element
			// if the request partname is not defined
			throw new RuntimeException("Non-element value can only be used in request if part name specified");
			
		} else {
			
			// Need to construct DOM element for message
			mesgElem = createMessage();
			
			// Transfer value into this message element
			node = (Node)node.cloneNode(true);
			
			Element partElem=mesgElem.getOwnerDocument().createElement(requestPartName);
			mesgElem.appendChild(partElem);
			
			node = (Node)mesgElem.getOwnerDocument().adoptNode(node);
			
			partElem.appendChild(node);
		}
				
		String serviceName=_config.getAttribute(SERVICE);
		
		javax.xml.namespace.QName qname=javax.xml.namespace.QName.valueOf(serviceName);
		
		logger.debug("Invoking service: "+qname);

		org.w3c.dom.Element resp=null;
		javax.xml.namespace.QName faultName=null;
		
		// Create invocationContext
		ESBInvocationAdapter invocationContext =
			new ESBInvocationAdapter(_config.getAttribute(OPERATION), qname, _config.getAttribute(PORT));
		invocationContext.setRequestXML(mesgElem);
		
		// invoke ODE
		try {
			engine.invoke(invocationContext);
		} catch(Throwable t) {
			// RIFTSAW-177 - prevent ODE specific exceptions being returned to ESB client where
			// a ClassNotFoundException would be thrown
			throw new Exception("BPEL invoke failed: "+t);
		}

		resp = invocationContext.getInvocationResult();
		faultName = invocationContext.getFaultName();	

		if (resp != null) {
			ret = MessageFactory.getInstance().getMessage();
			
			try {
				String defpart=_config.getAttribute(RESPONSE_PART_NAME);
				
				org.w3c.dom.Node respNode=null;
				
				if (defpart != null) {
					org.w3c.dom.NodeList nl=resp.getChildNodes();
					
					// Find content in named part
					for (int i=0; respNode == null && i < nl.getLength(); i++) {
						if (nl.item(i).getNodeType() == Node.ELEMENT_NODE &&
								nl.item(i).getNodeName().equals(defpart)) {
							
							// Find element
							org.w3c.dom.NodeList subnl=
								((org.w3c.dom.Element)nl.item(i)).getChildNodes();
							if (subnl.getLength() == 1) {
								respNode = subnl.item(0);
							} else {
								// Scan for element
								for (int j=0; respNode != null && j < subnl.getLength(); j++) {
									if (subnl.item(j).getNodeType() == Node.ELEMENT_NODE) {
										respNode = subnl.item(j);
									}
								}
							}
						}
					}
				} else {
					respNode = resp;
				}
				
				Object respValue=respNode;
				
				// Check if node needs to be converted to text
				if ((f_toText || faultName != null) && respNode != null) {
					respValue = getText(respNode);
				}
									
				if (respValue == null) {
					logger.error("Unable to convert message part '"+(defpart==null?"<undefined>":defpart)+
										"' into a response document");
				} else {
					//ret.getBody().add(respValue);
					
					// Set the payload in the returned message
					proxy.setPayload(ret, respValue);
					
					// Check if fault name should be set
					if (faultName != null) {
						logger.debug("Fault '"+faultName+"' detected, throwing exception");
						
						org.jboss.soa.esb.actions.ActionProcessingDetailFaultException faultException=
							new org.jboss.soa.esb.actions.ActionProcessingDetailFaultException(faultName,
									"Fault '"+faultName+"' occurred when calling service '"+serviceName+"'",
											respValue.toString());
						
						// In case we wanted to return the fault message as the message body
						//faultException.getFaultMessage().getBody().add(respValue.toString());
						
						// Record string representation of faultName, as currently if the client
						// does not include the stax-api.jar in its list of endorsed libs (as the
						// JBoss AS server does, then it will result in a class version issue when
						// it tries to retrieve the ESB fault code (which is transferred as a QName).
						// See RIFTSAW-110 for more details.
						faultException.getFaultMessage().getBody().add(BODY_BPEL_FAULT_CODE, faultName.toString());
						
						throw faultException;
					}
				}
			} catch(org.jboss.soa.esb.actions.ActionProcessingDetailFaultException fault) {
				
				// Determine whether to abort action pipeline with exception or return
				// fault message for continued processing
				if (_config.getAttribute(ABORT_ON_FAULT, "true").equalsIgnoreCase("true")) {
					logger.debug("Rethrowing BPEL fault: "+fault);
					
					throw fault;
				} else {
					ret = fault.getFaultMessage();
					
					logger.debug("Returning fault as message: "+ret);
				}
				
			} catch(Exception e) {
				logger.error("Failed to parse response '"+resp+"'", e);
			}
			
			logger.debug("Response: "+ret);
		}
		
		return(ret); 	
	}
	
	/**
	 * This class converts a DOM representation node to
	 * text.
	 * 
	 * @param node The DOM node
	 * @return The text
	 * @throws Exception Failed to convert
	 */
	protected static String getText(Node node) throws Exception {
		String ret=null;
			
		try {
			// Transform the DOM represent to text
			java.io.ByteArrayOutputStream xmlstr=
					new java.io.ByteArrayOutputStream();
				
			DOMSource source=new DOMSource();
			source.setNode(node);
			
			StreamResult result=new StreamResult(xmlstr);
			
			Transformer trans=
					TransformerFactory.newInstance().newTransformer();
			trans.transform(source, result);
			
			xmlstr.close();
			
			ret = new String(xmlstr.toByteArray());
			
			if ((node instanceof org.w3c.dom.Document) == false) {
				
				// Strip off any <?xml> header
				int index=ret.indexOf("<?xml");
				if (index != -1) {
					index = ret.indexOf("<", 1);
					
					if (index != -1) {
						ret = ret.substring(index);
					} else {
						index = ret.indexOf("?>");
						
						if (index != -1) {
							index += 2;
							
							// Remove any trailing whitespaces
							// after XML header
							while (index < ret.length() &&
									Character.isWhitespace(ret.charAt(index))) {
								index++;
							}
							
							ret = ret.substring(index);
						}
					}
				}
			}

		} catch(Exception e) {
			throw new Exception("Failed to transform " +
					"DOM representation into text", e);
		}
		
		return(ret);
	}
	
	/**
	 * Create an empty message element.
	 * 
	 * @return The new message
	 */
	protected org.w3c.dom.Element createMessage() {
		org.w3c.dom.Element ret=null;
		
		try {
			DocumentBuilder builder=DocumentBuilderFactory.newInstance().newDocumentBuilder();
			
			Document doc=builder.newDocument();
			
			ret = doc.createElement(TOP_LEVEL_ELEMENT_NAME);
			
		} catch(Exception e) {
			logger.error("Failed to create message", e);
		}

		return(ret);
	}
	
	/**
	 * This method converts the supplied text representation
	 * of an XML document into a DOM Node.
	 * 
	 * @param text The text
	 * @return The node
	 * @throws Exception Failed to convert the text
	 */
	protected static Node getNode(String text) throws Exception {
		Node ret=null;
		
		try {
			// Transform the text representation to DOM
			DocumentBuilderFactory fact=DocumentBuilderFactory.newInstance();
			fact.setNamespaceAware(true);
			
			DocumentBuilder builder=fact.newDocumentBuilder();
			
			// Check if XML document, and if not return as text node
			if (text.trim().length() == 0 || text.charAt(0) != '<') {
				org.w3c.dom.Document doc=builder.newDocument();
				
				// Assume is text node
				ret = doc.createTextNode(text);
			} else {
				java.io.InputStream xmlstr=
					new java.io.ByteArrayInputStream(text.getBytes());

				org.w3c.dom.Document doc=builder.parse(xmlstr);
				
				xmlstr.close();
			
				ret = doc.getDocumentElement();
			}
			
		} catch(Exception e) {
			throw new Exception("Failed to transform text " +
					"into DOM representation", e);
		}

		return(ret);
	}

    private final Log logger = LogFactory.getLog(getClass());
}
