package org.apache.cocoon.transformation;

import org.apache.cocoon.transformation.helper.CastorEventAdapter;
import org.apache.cocoon.transformation.helper.CastorMarshalCommand;
import org.apache.cocoon.transformation.helper.CastorUnmarshalCommand;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.environment.Context;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.transformation.AbstractTransformer;
import org.apache.excalibur.source.SourceException;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.UnmarshalHandler;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.ValidationException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;


/**
 * Description: Marshals a object from the Sitemap, Session, Request or the
 * Conext into a series of SAX events Configuation: The Castortransformer
 * need to be configured with a default mapping. This mapping is used as long
 * as no other mapping is spezfied as the element
 * <pre>
 *  &ltmap:transformer name="CastorTransformer" src="org.apache.cocoon.transformation.CastorTransformer"/&gt
 * </pre>
 * A sample for the use:
 * <pre>
 *   &ltroot xmlns:castor="http://castor.exolab.org/castortransformer"&gt
 *         &ltcastor:marshall name="invoice"/&gt
 *         &ltcastor:unmarshall name="product" scope="sitemap" mapping="castor/specicalmapping.xml"/&gt
 *  &lt/root&gt
 * </pre>
 * The CastorTransfomer supports two elements
 * <code>castor:unmarshal</code> and <code>castor:marshal</code>. 
 * 
 * The marshal element is replaced with the marshalled object. 
 * The Object given through the attrbute <code>name</code>
 * will be searched in the <code>sitemap, request, session</code> and at
 * least in <code>application</code> If the scope is explicitly given, e.g ,
 * the object will ge located only here. The Attribute <code>mapping</code>
 * specifys the mapping to be used. The attribute <code>command</code> specifies a class that
 * implements CastorMarshalCommand and will be called before and after marshalling.
 * 
 * The elements within the unmarshal element will be sent to the castor unmarshaller
 * the resulting java object with be placed in the object specified by name and scope (see also marshall element).
 * The <code>command</code> attribute specifies the class that implements CastorUnmarshalCommand
 * and will be called before and after unmarshalling.
 * 
 * Author <a href="mailto:mauch@imkenberg.de">Thorsten Mauch</a>
 * Author <a href="mailto:michael.homeijer@ordina.nl">Michael Homeijer</a>
 */
public class CastorTransformer extends AbstractTransformer
{
    // stores all used mappings in the cache
    private static HashMap mappingCache;
    private static final String CASTOR_URI = "http://castor.exolab.org/castortransformer";
    private static final String CMD_UNMARSHAL = "unmarshal";
    private static final String CMD_MARSHAL = "marshal";
    private static final String ATTRIB_NAME = "name";
    private static final String ATTRIB_SCOPE = "scope";
    private static final String ATTRIB_MAPPING = "mapping";
    private static final String ATTRIB_CMD = "command";
    private static final String VALUE_SITEMAP = "sitemap";
    private static final String VALUE_SESSION = "session";
    private static final String VALUE_REQUEST = "request";
    private static final String VALUE_CONTEXT = "context";

    // Static initializer:
    static {
        // Try to find out which XML parser to use:
        if (xmlParserFoundAndSet("org.apache.xerces.parsers.SAXParser")) // Xerces / XML4J
        {
        } else if (xmlParserFoundAndSet("org.apache.crimson.jaxp.SAXParserImpl")) // Crimson
        {
        } else if (xmlParserFoundAndSet("com.jclark.xml.sax.Driver")) // James Clark XP
        {
        } else if (xmlParserFoundAndSet("com.microstar.xml.SAXDriver")) // Aelfred
        {
        } else {
            System.err.println("ERROR: No SAX XML parser found!");
        }
    }

    private Map objectModel;
    private SourceResolver resolver;
    
    private boolean in_castor_marshal = false;
    
    private Unmarshaller unmarshaller;
    private UnmarshalHandler unmarshalHandler;
    private ContentHandler unmarshalContentHandler;
	private CastorUnmarshalCommand unmarshalCommand;
	private String beanName;
	private String beanScope;

    /**
     * DOCUMENT ME!
     * 
     * @param resolver DOCUMENT ME!
     * @param objectModel DOCUMENT ME!
     * @param src DOCUMENT ME!
     * @param params DOCUMENT ME!
     * @throws org.apache.cocoon.ProcessingException DOCUMENT ME!
     * @throws org.xml.sax.SAXException DOCUMENT ME!
     * @throws java.io.IOException DOCUMENT ME!
     */
    public void setup(SourceResolver resolver, Map objectModel, String src, 
                      Parameters params)
               throws org.apache.cocoon.ProcessingException, 
                      org.xml.sax.SAXException, java.io.IOException {
        this.objectModel = objectModel;
        this.resolver = resolver;
    }

    /**
     * DOCUMENT ME!
     * 
     * @param uri DOCUMENT ME!
     * @param name DOCUMENT ME!
     * @param raw DOCUMENT ME!
     * @throws org.xml.sax.SAXException DOCUMENT ME!
     */
    public void endElement(String uri, String name, String raw)
                    throws org.xml.sax.SAXException {
        if (unmarshalContentHandler != null) {
            // check if this marks the end of the unmarshalling
            if ((CASTOR_URI.equals(uri)) && (CMD_UNMARSHAL.equals(name))) {
            	
                // End marshalling
                unmarshalContentHandler.endDocument();
                unmarshalContentHandler = null;

                // store the result of the unmarshaller
                Object root = unmarshalHandler.getObject();
                this.storeBean(objectModel, beanName, beanScope, root);
            } else {
                unmarshalContentHandler.endElement(uri, name, raw);
                
                if (unmarshalCommand != null) {
	       			unmarshalCommand.post(unmarshalHandler, xmlConsumer, objectModel);
                }

            }

            return;
        } else if (CASTOR_URI.equals(uri)) {
            in_castor_marshal = false;

            return;
        }

        super.endElement(uri, name, raw);
    }

    /**
     * DOCUMENT ME!
     * 
     * @param uri DOCUMENT ME!
     * @param name DOCUMENT ME!
     * @param raw DOCUMENT ME!
     * @param attr DOCUMENT ME!
     * @throws org.xml.sax.SAXException DOCUMENT ME!
     */
    public void startElement(String uri, String name, String raw, 
                             Attributes attr) throws org.xml.sax.SAXException {
        // Check if the unmarshaller is active
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.startElement(uri, name, raw, attr);

            return;
        }

        if (CASTOR_URI.equals(uri)) {
            process(name, attr);

            return;
        }

        super.startElement(uri, name, raw, attr);
    }

    /**
     * DOCUMENT ME!
     * 
     * @param ch DOCUMENT ME!
     * @param start DOCUMENT ME!
     * @param len DOCUMENT ME!
     * @throws org.xml.sax.SAXException DOCUMENT ME!
     */
    public void characters(char[] ch, int start, int len)
                    throws org.xml.sax.SAXException {
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.characters(ch, start, len);

            return;
        }

        if (in_castor_marshal)
            return;

        super.characters(ch, start, len);
    }

    /**
     * Begin the scope of a prefix-URI Namespace mapping.
     * 
     * @param prefix The Namespace prefix being declared.
     * @param uri The Namespace URI the prefix is mapped to.
     */
    public void startPrefixMapping(String prefix, String uri)
                            throws SAXException {
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.startPrefixMapping(prefix, uri);

            return;
        }

        super.startPrefixMapping(prefix, uri);
    }

    /**
     * End the scope of a prefix-URI mapping.
     * 
     * @param prefix The prefix that was being mapping.
     */
    public void endPrefixMapping(String prefix) throws SAXException {
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.endPrefixMapping(prefix);

            return;
        }

        super.endPrefixMapping(prefix);
    }

    /**
     * Receive notification of ignorable whitespace in element content.
     * 
     * @param c The characters from the XML document.
     * @param start The start position in the array.
     * @param len The number of characters to read from the array.
     */
    public void ignorableWhitespace(char[] c, int start, int len)
                             throws SAXException {
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.ignorableWhitespace(c, start, len);

            return;
        }

        super.ignorableWhitespace(c, start, len);
    }

    /**
     * Receive notification of a processing instruction.
     * 
     * @param target The processing instruction target.
     * @param data The processing instruction data, or null if none was
     *        supplied.
     */
    public void processingInstruction(String target, String data)
                               throws SAXException {
        if (unmarshalContentHandler != null) {
            unmarshalContentHandler.processingInstruction(target, data);

            return;
        }

        super.processingInstruction(target, data);
    }

    /**
     * DOCUMENT ME!
     * 
     * @param command DOCUMENT ME!
     * @param attr DOCUMENT ME!
     */
    private void process(String command, Attributes attr) {
        if (command.equals(CMD_MARSHAL)) {
            in_castor_marshal = true;

            String sourcemap = attr.getValue(ATTRIB_SCOPE);
            String name = attr.getValue(ATTRIB_NAME);
            String mapping = attr.getValue(ATTRIB_MAPPING);
            String commandclass = attr.getValue(ATTRIB_CMD);

            if (name == null) {
                getLogger().error("Attribute to insert not set");
            } else {
                Object toInsert = this.searchBean(objectModel, name, sourcemap);

                if (toInsert != null) {
                    this.marshal(commandclass, toInsert, mapping);

                    return;
                }
            }

            getLogger().error("Bean " + name + " could not be found");

            return;
        } // end CMD_INSERT_BEAN
        else if (command.equals(CMD_UNMARSHAL)) {
            beanScope = attr.getValue(ATTRIB_SCOPE);
            beanName = attr.getValue(ATTRIB_NAME);
            String commandclass = attr.getValue(ATTRIB_CMD);
            
            if (beanScope == null) {
            	getLogger().error("Destination for unmarshalled bean not set");
            	return;
            }
            
            if (beanName == null) {
            	getLogger().error("Name of unmarshalled bean not set");
            	return;
            }
            String mappingpath = attr.getValue(ATTRIB_MAPPING);


            // Create the unmarshaller
            unmarshaller = new Unmarshaller((Class) null);
        // Only set a mapping if one is specified
        if (mappingpath != null) {
            Mapping mapping;

            try {
                mapping = this.mappingLoader(mappingpath);

                unmarshaller.setMapping(mapping);
            } catch (MappingException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            } catch (IOException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            } catch (SourceException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            }
        }

		unmarshalCommand = null;
		if (commandclass != null) {
		try {
			unmarshalCommand = (CastorUnmarshalCommand)Class.forName(commandclass).newInstance();

			unmarshalCommand.pre(unmarshaller, xmlConsumer, objectModel);
		} catch (InstantiationException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		} catch (IllegalAccessException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		} catch (ClassNotFoundException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		}
		}

            // Create the unmarshalhandler and wrap it with a SAX2 contentHandler
            unmarshalHandler = unmarshaller.createHandler();

            try {
                unmarshalContentHandler = unmarshaller.getContentHandler(
                                                  unmarshalHandler);

                unmarshalContentHandler.startDocument();
            } catch (SAXException e) {
                getLogger()
                    .error("Could not get contenthandler from unmarshaller", e);
            }
            return;
        }

        getLogger().error("Unknown command: " + command);
    }

    /**
     * DOCUMENT ME!
     * 
     * @param objectModel DOCUMENT ME!
     * @param name DOCUMENT ME!
     * @param sourcemap DOCUMENT ME!
     * @return DOCUMENT ME! 
     */
    private Object searchBean(Map objectModel, String name, String sourcemap) {
        Request request = ObjectModelHelper.getRequest(objectModel);
        Object bean;

        //
        //  search all maps for the given bean
        //
        if ((sourcemap == null) || VALUE_SITEMAP.equals(sourcemap)) {
            //System.out.println("Searching bean " + name+ " in "+VALUE_SITEMAP);
            bean = objectModel.get(name);

            if (bean != null)
                return bean;
        }

        if ((sourcemap == null) || VALUE_REQUEST.equals(sourcemap)) {
            //System.out.println("Searching bean " + name+ " in "+ VALUE_REQUEST);
            bean = request.getAttribute(name);

            if (bean != null)
                return bean;
        }

        if ((sourcemap == null) || VALUE_SESSION.equals(sourcemap)) {
            //System.out.println("Searching bean " + name+ " in "+VALUE_SESSION);
            Session session = request.getSession(false);

            if (session != null) {
                bean = session.getAttribute(name);

                if (bean != null)
                    return bean;
            }
        }

        return null;
    }
    
    private void storeBean(Map objectModel, String name, String sourcemap, Object bean) {
        Request request = ObjectModelHelper.getRequest(objectModel);

        if (VALUE_SITEMAP.equals(sourcemap)) {
        	objectModel.put(name, bean);
        } else if (VALUE_REQUEST.equals(sourcemap)) {
            request.setAttribute(name, bean);
        } else if (VALUE_SESSION.equals(sourcemap)) {
            Session session = request.getSession(true);

			session.setAttribute(name, bean);
        }
    }

    /**
     * DOCUMENT ME!
     * 
     * @param bean DOCUMENT ME!
     * @param mappingpath DOCUMENT ME!
     */
    private void marshal(String commandclass, Object bean, String mappingpath) {
        if (bean == null) {
            getLogger().debug("no bean found");

            return;
        }

        Marshaller marshaller;

        try {
            marshaller = new Marshaller(new CastorEventAdapter(xmlConsumer));
        } catch (IOException e) {
            getLogger().error("Could not create castor Marshaller", e);

            return;
        }

        // Only set a mapping if one is specified
        if (mappingpath != null) {
            Mapping mapping;

            try {
                mapping = this.mappingLoader(mappingpath);

                marshaller.setMapping(mapping);
            } catch (MappingException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            } catch (IOException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            } catch (SourceException e) {
                getLogger()
                    .error("Could not load mapping file " + mappingpath, e);
            }
        }

		CastorMarshalCommand marshalCommand = null;
		if (commandclass != null) {
		try {
			marshalCommand = (CastorMarshalCommand)Class.forName(commandclass).newInstance();

			marshalCommand.pre(marshaller, xmlConsumer, objectModel);
		} catch (InstantiationException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		} catch (IllegalAccessException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		} catch (ClassNotFoundException e) {
			getLogger().error("Could not instantiate class " + commandclass ,e);
		}
		}
		
        try {
            marshaller.marshal(bean);
            
            if (marshalCommand != null) {
            	marshalCommand.post(marshaller, xmlConsumer, objectModel);
            }
            
        } catch (MarshalException e) {
            getLogger().error("Error marshalling bean", e);
        } catch (ValidationException e) {
            getLogger().error("Validation error during bean marshalling", e);
        }
        
    }

    /**
     * DOCUMENT ME!
     * 
     * @param path DOCUMENT ME!
     * @return DOCUMENT ME! 
     * @throws MappingException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     */
    private Mapping mappingLoader(String path) throws MappingException, 
                                                      IOException, 
                                                      SourceException {
        if (mappingCache == null) {
            mappingCache = new HashMap();
        } else { // cache already exists

            // and contains the mapping already
            if (mappingCache.containsKey(path)) {
                return (Mapping) mappingCache.get(path);
            }
        }

        // mapping not found in cache or the cache is new
        Mapping mapping = new Mapping(getClass().getClassLoader());

        String file;

        file = this.getFile(resolver, path);

        mapping.loadMapping(file);
        mappingCache.put(path, mapping);

        return mapping;
    }

    /**
     * DOCUMENT ME!
     * 
     * @param sr DOCUMENT ME!
     * @param FileName DOCUMENT ME!
     * @return DOCUMENT ME! 
     */
    private String getFile(SourceResolver sr, String FileName)
                    throws SourceException, IOException {
        String path = sr.resolveURI(FileName).getSystemId();

        return path;
    }

    /**
     * This code is only used by the 'static' part because a SAX parser has to be set.
     */
    private static boolean xmlParserFoundAndSet(String qualifiedName) {
        try {
            Class.forName(qualifiedName, false, 
                          Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException cnfe) {
            return false;
        }

        System.setProperty("org.xml.sax.parser", qualifiedName);
        System.out.println("Using SAX XML parser: " + qualifiedName);

        return true;
    }
}
