/*
 * 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.servicemix.components.mps;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.jbi.JBIException;
import javax.jbi.component.ComponentContext;
import javax.jbi.messaging.MessageExchange;
import javax.jbi.messaging.MessagingException;
import javax.jbi.messaging.NormalizedMessage;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.servicemix.components.util.TransformComponentSupport;
import org.apache.servicemix.jbi.jaxp.SourceTransformer;
import org.apache.xpath.CachedXPathAPI;
import org.apache.xpath.objects.XObject;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;



/**
 * Sets properties on the message, loaded from an XML MPS file 
 * where the properties to set are located in a <property-set ..>
 * inside the XML config file.
 * 
 * There can be more than one propertySet to "load".
 *
 *  The property values are derived from 3 types of config.
 *  The first config that can return a value as a String to
 *  set onto the message, will be the "value" that is set
 *  as the property.
 *  
 *  <static-value>
 *  		As it's name suggests, the "value" of this element
 *          will be the value of the JBI property.
 *          
 *          This is helpful as a default value, or as a static value.
 *          
 *  <exisiting-property> and <existint-property name="..."/>
 *    		This will obtain the value of an existing property (itself)
 *          or another property on the same message.
 *          
 *           This can be helpful when you want the to "ONLY" change the
 *           the value of the property if there is some "xpath" expression
 *           that could not be derived. 
 *           
 *           name=".." form will copy the string value of the other JBI property
 *           onto this one, (duping it). This may be handy when you have a component 
 *           which expects a new property, but you have it as a different name at the moment.
 *           
 *  <xpath-expression>
 *           As it's name suggests, will locate a value in the inMessage source
 *           and set the resulting XPath String as the value of the JBI property.
 *           
 *  So given the three types, they can be arranged in any order. and the first
 *  PropertyValue Type that returns a value, will become the "value" of the JBI property.
 *  
 *
 * <!-- 
 * <mps>
 *	 <property-set name="someSetNameForASetOfProperties">
 *	     <property name="some.property.name1">
 *	         <static-value><![CDATA[value for the property]]></static-value>
 *	     </property>
 *	     <property name="some.property.name2">
 *	         <xpath-expression>
 *		 	      <![CDATA[/someexpath/statement/to/be/applied/to/message/source]]>
 *		 	 </xpath-expression>
 *	         <existing-property name="someproperty"/>
 *		 	 <static-value><![CDATA[a value in the raw]]></static-value>
 *	     </property>
 *	     <property name="prop.xpath.with.static.default">
 *	        <xpath-expression>
 *		 	    <![CDATA[/someexpath/statement]]>
 *		 	</xpath-expression>
 *		 	<static-value><![CDATA[some default if xpath does not resolve]]></static-value>
 *	     </property>
 *	     <property name="prop.xpath.or.keep.existing">
 *	        <xpath-expression>
 *		 	    <![CDATA[/someexpath/statement]]>
 *		 	</xpath-expression>
 *		 	<existing-property/>
 *	     </property>
 *	     <property name="new.prop.name">
 *		 	<existing-property name="other.property"/>
 *	     </property>
 *       <property name="..."> 
 *              ...
 *       </property>
 *	 </property-set>
 *   <property-set name="...">
 *        ...
 *   </property-set>
 * </mps>
 * -->
 *
 * 
 * @author rbuckland
 * 
 */
public class MessagePropertySetterXML extends TransformComponentSupport  {
	
	/**
	 * Apache commons logger
	 */
	private final transient Log logger = LogFactory.getLog(getClass());

	/**
	 * The name of our JBI property we may have to interrogate to 
	 * use as the "xpath" for the set of config.
	 */
	public static final String MPS_PROP_NAME_PROPERTYSET = "org.apache.servicemix.components.mps.propertyset";
	
	/**
	 * The XML Element name "property-set"
	 */
	public static final String XML_ELEMENT_NAME = "property-set";
	
	/**
	 * This is our XML file
	 */
	private Resource xmlConfiguration = null;
	
	/**
	 * Our XML config MPS.
	 */
	private Document xmlMPSdom = null;
	
	/**
	 * Holds the propertyValueResolver objects.
	 * if propertySet is not null, this holds one.
	 * if propertySet is null, this will hold
	 * one for each propertyvalue entry used during the time
	 * we are "active".
	 */
	private Map propertSets = new HashMap();

	/**
	 * If this is set, it is hardcoded as the fixed value.
	 * 
	 * If this is null 
	 * we will look for a propery MPS_PROP_NAME_PROPERTYSET
	 * as the "name" of the property set we will work with.
	 * So the message incoming, seeds the set of properties to load, just by
	 * the JBI property itself.
	 */
	private String propertySet = null;
	
	
	/**
	 * If the XPath for a propertySet is not null
	 * we will try and locate the propertySet to use by 
	 * pulling a String from the inMessage content using this
	 * XPath.
	 */
	private String xpathForPropertySet = null;
	
	/**
	 * Here is the transform of the message
	 * We will locate the propertySetName to use 
	 * and the load up our propertySet magic wand and apply all the properties
	 * we can to the outgoing NormalizedMessage
	 */
	protected boolean transform(MessageExchange arg0, NormalizedMessage in, NormalizedMessage out) throws MessagingException {
		try {
			copyPropertiesAndAttachments(arg0,in,out);
			out.setContent(in.getContent());
			
			String propertySetName = "";
			if (xpathForPropertySet != null) {
				try {
					CachedXPathAPI xpathApi = new org.apache.xpath.CachedXPathAPI();
					Document doc = new SourceTransformer().toDOMDocument(in);
					XObject propSetXO = xpathApi.eval(doc.getDocumentElement(),xpathForPropertySet);
					propertySetName = propSetXO.str();
				} catch (Exception e) {
					throw new MessagingException("Problem getting the propertySet using XPath", e);
				}
			} else if (this.propertySet != null) {
				propertySetName = this.propertySet;
			} else if (in.getProperty(MPS_PROP_NAME_PROPERTYSET) != null) {
				propertySetName = in.getProperty(MPS_PROP_NAME_PROPERTYSET).toString();
			} else {
				return false;
			}
			logger.info("Applying properties from property-set [" + propertySetName + "]");
			getPropertySetByName(propertySetName).applyProperties(in,out);
			return true;				
		} catch (JBIException e) {
			throw new MessagingException("Problem setting properties",e);
		} catch (PropertySetNotFoundException e) {
			logger.warn(e.getLocalizedMessage());
			return false;
		}
	}

	/**
	 * Initialise by loading the mps.xml file up into an internal DOM.
	 * 
	 * @throws JBIException
	 */
	private void initConfig() throws JBIException {
		Assert.notNull(this.xmlConfiguration);
		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setIgnoringElementContentWhitespace(true);
		domFactory.setIgnoringComments(true);
		domFactory.setCoalescing(true); // convert CDATA to test nodes
        DocumentBuilder domBuilder;
		try {
			logger.info("Intialising MessagePropertySetterXML, loading settings from " + this.xmlConfiguration.getFile().getAbsolutePath());
			domBuilder = domFactory.newDocumentBuilder();
			xmlMPSdom = domBuilder.parse(this.xmlConfiguration.getInputStream());
		} catch (ParserConfigurationException e) {
			throw new JBIException("Problem parsing the XML config file for MPS",e);
		} catch (SAXException e) {
			throw new JBIException("Problem loading the XML config file for MPS",e);
		} catch (IOException e) {
			throw new JBIException("Problem loading the XML config file for MPS",e);
		}
		
	}

	/**
	 * We are only interested  in loading the XML onfig, nothing else
	 */
	public void init(ComponentContext context) throws JBIException {
		super.init(context);
		initConfig();
	}

	/**
	 * Create a propertyset (loading up the Object config based on the XML config)
	 * 
	 * @param propertySetName
	 * @return
	 * @throws JBIException
	 * @throws PropertySetNotFoundException
	 */
	private PropertySet createPropertySet(String propertySetName) throws JBIException , PropertySetNotFoundException{
		PropertySet ps;	
		CachedXPathAPI xpath = new CachedXPathAPI();
		StringBuffer xpathSB = new StringBuffer("//")
									.append(XML_ELEMENT_NAME)
									.append("[@name='")
									.append(propertySetName)
									.append("']");
		try {
			Node propertySetNode = xpath.selectSingleNode(xmlMPSdom,xpathSB.toString());
			if (propertySetNode == null) {
				throw new PropertySetNotFoundException("Could not find a property-set for [" + propertySetName + "] in " + xmlConfiguration.getFilename());
			}
			ps = new PropertySet(propertySetName,(Element) propertySetNode);
			this.propertSets.put(propertySetName, ps);
		} catch (TransformerException e) {
			throw new JBIException("Could not load the PropertySet for " + propertySet,e);		
		} catch (ConfigNotSupportedException e) {
			throw new JBIException("Could not load the PropertySet for. XMLConfig is not good for " + propertySet,e);		
		}
		return ps;

	}
	
	/**
	 * Get a property set from ou
	 * @param name
	 * @return
	 * @throws JBIException 
	 */
	private PropertySet getPropertySetByName(String name) throws JBIException, PropertySetNotFoundException {
		// find a pre "created" one
		if (this.propertSets.containsKey(name)) {
			return (PropertySet)this.propertSets.get(name);
		} else {
			return this.createPropertySet(name);
		}
		
	}

	/**
	 * @param propertySet The propertySet to set.
	 */
	public void setPropertySet(String propertySet) {
		this.propertySet = propertySet;
	}

	/**
	 * @param xmlConfiguration The xmlConfiguration to set.
	 */
	public void setXmlConfiguration(Resource xmlConfiguration) {
		this.xmlConfiguration = xmlConfiguration;
	}

	/**
	 * @param xpathForPropertySet xpath to apply to a message to derive the name of the property-set we want to load
	 */
	public void setXpathForPropertySet(String xpathForPropertySet) {
		this.xpathForPropertySet = xpathForPropertySet;
	}

}
