/*
 * Copyright (C) 2005-2007 Alfresco Software Limited.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

 * As a special exception to the terms and conditions of version 2.0 of 
 * the GPL, you may redistribute this Program in connection with Free/Libre 
 * and Open Source Software ("FLOSS") applications as described in Alfresco's 
 * FLOSS exception.  You should have recieved a copy of the text describing 
 * the FLOSS exception, and it is also available here: 
 * http://www.alfresco.com/legal/licensing"
 */
package org.alfresco.jcr.importer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Stack;

import javax.jcr.InvalidSerializedDataException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;

import org.alfresco.jcr.dictionary.DataTypeMap;
import org.alfresco.jcr.dictionary.JCRNamespace;
import org.alfresco.jcr.exporter.JCRSystemXMLExporter;
import org.alfresco.jcr.item.property.JCRMixinTypesProperty;
import org.alfresco.jcr.item.property.JCRPrimaryTypeProperty;
import org.alfresco.jcr.item.property.JCRUUIDProperty;
import org.alfresco.jcr.session.SessionImpl;
import org.alfresco.repo.importer.ImportContentHandler;
import org.alfresco.repo.importer.Importer;
import org.alfresco.repo.importer.view.ElementContext;
import org.alfresco.repo.importer.view.NodeContext;
import org.alfresco.repo.importer.view.ParentContext;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.InvalidTypeException;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.view.ImporterException;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Base64;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;


/**
 * Alfresco implementation of a System View Import Content Handler
 * 
 * @author David Caruana
 */
public class JCRSystemXMLHandler implements ImportContentHandler
{
    private Importer importer;
    private SessionImpl session;
    private NamespacePrefixResolver importResolver;
    private Stack<ElementContext> contextStack = new Stack<ElementContext>();
    

    /**
     * Construct
     * 
     * @param session  JCR Session
     * @param importResolver  Namespace Resolver for the Import
     */
    public JCRSystemXMLHandler(SessionImpl session, NamespacePrefixResolver importResolver)
    {
        this.session = session;
        this.importResolver = importResolver;
    }

    /*
     *  (non-Javadoc)
     * @see org.alfresco.repo.importer.ImportContentHandler#setImporter(org.alfresco.repo.importer.Importer)
     */
    public void setImporter(Importer importer)
    {
        this.importer = importer;
    }

    /*
     *  (non-Javadoc)
     * @see org.alfresco.repo.importer.ImportContentHandler#importStream(java.lang.String)
     */
    public InputStream importStream(String content)
    {
        File contentFile = new File(content);
        try
        {
            FileInputStream contentStream = new FileInputStream(contentFile);
            return new Base64.InputStream(contentStream, Base64.DECODE | Base64.DONT_BREAK_LINES);
        }
        catch (FileNotFoundException e)
        {
            throw new ImporterException("Failed to retrieve import input stream on temporary content file " + content);
        }
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
     */
    public void setDocumentLocator(Locator locator)
    {
        // NOOP
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startDocument()
     */
    public void startDocument() throws SAXException
    {
        // NOOP
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endDocument()
     */
    public void endDocument() throws SAXException
    {
        // NOOP
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
     */
    public void startPrefixMapping(String prefix, String uri) throws SAXException
    {
        // NOOP
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
     */
    public void endPrefixMapping(String prefix) throws SAXException
    {
        // NOOP
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException
    {
        try
        {
            // construct qname for element
            QName elementName = QName.createQName(qName, importResolver);
    
            if (JCRSystemXMLExporter.NODE_QNAME.equals(elementName))
            {
                processStartNode(elementName, atts);
            }
            else if (JCRSystemXMLExporter.PROPERTY_QNAME.equals(elementName))
            {
                processStartProperty(elementName, atts);
            }
            else if (JCRSystemXMLExporter.VALUE_QNAME.equals(elementName))
            {
                processStartValue(elementName, atts);
            }
        }
        catch(Exception e)
        {
            throw new SAXException("Failed to process element " + qName, e);
        }
    }
    
    /**
     * Process start of Node Element
     *  
     * @param elementName
     * @param atts
     * @throws InvalidSerializedDataException
     */
    private void processStartNode(QName elementName, Attributes atts)
        throws InvalidSerializedDataException
    {
        ParentContext parentContext = null;
        if (contextStack.empty())
        {
            // create root parent context
            parentContext = new ParentContext(elementName, session.getRepositoryImpl().getServiceRegistry().getDictionaryService(), importer);
        }
        else
        {
            NodeContext parentNode = (NodeContext)contextStack.peek();
            
            // if we haven't yet imported the node before its children, do so now
            if (parentNode.getNodeRef() == null)
            {
                NodeRef nodeRef = importer.importNode(parentNode);
                parentNode.setNodeRef(nodeRef);
            }

            // create parent context
            parentContext = new ParentContext(elementName, parentNode);
        }
        
        // create node context
        // NOTE: we don't yet know its type (we have to wait for a property)
        NodeContext nodeContext = new NodeContext(elementName, parentContext, null);

        // establish node child name
        String name = atts.getValue(JCRSystemXMLExporter.NAME_QNAME.toPrefixString(importResolver));
        if (name == null)
        {
            throw new InvalidSerializedDataException("Mandatory sv:name attribute of element sv:node not present.");
        }
        nodeContext.setChildName(name);

        // record new node
        contextStack.push(nodeContext);
    }

    /**
     * Process start of Property element
     * 
     * @param elementName
     * @param atts
     * @throws InvalidSerializedDataException
     */
    private void processStartProperty(QName elementName, Attributes atts)
        throws InvalidSerializedDataException
    {
        // establish correct context
        ElementContext context = contextStack.peek();
        if (!(context instanceof NodeContext))
        {
            throw new InvalidSerializedDataException("Element " + elementName + " not expected.");
        }
        NodeContext parentNode = (NodeContext)context;
    
        // establish property name
        String name = atts.getValue(JCRSystemXMLExporter.NAME_QNAME.toPrefixString(importResolver));
        if (name == null)
        {
            throw new InvalidSerializedDataException("Mandatory sv:name attribute of element sv:node not present.");
        }
        QName propertyName = QName.createQName(name, importResolver);
        
        // establish property type and validate property type
        QName dataType = null; 
        String type = atts.getValue(JCRSystemXMLExporter.TYPE_QNAME.toPrefixString(importResolver));
        if (type == null)
        {
            throw new InvalidSerializedDataException("Mandatory sv:type attribute of element sv:node not present.");
        }
        try
        {
            dataType = DataTypeMap.convertPropertyTypeToDataType(PropertyType.valueFromName(type));
        }
        catch(IllegalArgumentException e)
        {
            throw new ImporterException("Type " + type + " is not known for property " + name);
        }
        
        // construct property context        
        PropertyContext propertyContext = new PropertyContext(elementName, parentNode, propertyName, dataType);
        contextStack.push(propertyContext);
    }

    /**
     * Process start of Value element
     * 
     * @param elementName
     * @param atts
     * @throws InvalidSerializedDataException
     */
    private void processStartValue(QName elementName, Attributes atts)
        throws InvalidSerializedDataException
    {
        // establish correct context
        ElementContext context = contextStack.peek();
        if (!(context instanceof PropertyContext))
        {
            throw new InvalidSerializedDataException("Element " + elementName + " not expected.");
        }
        PropertyContext property = (PropertyContext)context;
        property.startValue();
        
        ValueContext value = new ValueContext(elementName, property);
        contextStack.push(value);
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
     */
    public void endElement(String uri, String localName, String qName) throws SAXException
    {
        try
        {
            // ensure context matches parse
            ElementContext context = (ElementContext)contextStack.peek();
            QName elementName = QName.createQName(qName, importResolver);
            if (!context.getElementName().equals(elementName))
            {
                throw new InvalidSerializedDataException("Expected element " + context.getElementName() + " but was " + elementName);
            }
    
            if (JCRSystemXMLExporter.NODE_QNAME.equals(elementName))
            {
                processEndNode((NodeContext)context);
            }
            else if (JCRSystemXMLExporter.PROPERTY_QNAME.equals(elementName))
            {
                processEndProperty((PropertyContext)context);
            }
            else if (JCRSystemXMLExporter.VALUE_QNAME.equals(elementName))
            {
                processEndValue((ValueContext)context);
            }
    
            // cleanup
            contextStack.pop();
        }
        catch(Exception e)
        {
            throw new SAXException("Failed to process element " + qName, e);
        }
    }

    /**
     * Process end of Node element
     *
     * @param node
     */
    private void processEndNode(NodeContext node)
    {
        // import node, if not already imported (this will be the case when no child nodes exist)
        NodeRef nodeRef = node.getNodeRef();
        if (nodeRef == null)
        {
            nodeRef = node.getImporter().importNode(node);
            node.setNodeRef(nodeRef);
        }
        
        // signal end of node
        node.getImporter().childrenImported(nodeRef);
    }
    
    /**
     * Process end of Property element
     * 
     * @param context
     * @throws InvalidSerializedDataException
     * @throws RepositoryException
     */
    private void processEndProperty(PropertyContext context)
        throws InvalidSerializedDataException, RepositoryException
    {
        QName propertyName = context.getName();

        // ensure a value has been provided
        if (context.isNull())
        {
            throw new InvalidSerializedDataException("Property " + propertyName + " must have a value");
        }
        
        //
        // process known properties
        //
        
        if (JCRPrimaryTypeProperty.PROPERTY_NAME.equals(propertyName))
        {
            // apply type definition
            if (!context.isNull())
            {
                QName typeQName = QName.createQName(context.getValues().get(0).toString(), importResolver);
                TypeDefinition typeDef = context.getDictionaryService().getType(typeQName);
                if (typeDef == null)
                {
                    throw new InvalidTypeException(typeQName);
                }
                
                // update node context
                context.getNode().setTypeDefinition(typeDef);
            }
        }
        else if (JCRMixinTypesProperty.PROPERTY_NAME.equals(propertyName))
        {
            // apply aspect definitions
            List<StringBuffer> values = context.getValues();
            for (StringBuffer value : values)
            {
                QName aspectQName = QName.createQName(value.toString(), importResolver);
                
                // ignore JCR specific aspects
                if (!(JCRNamespace.JCR_URI.equals(aspectQName.getNamespaceURI()) ||
                      JCRNamespace.MIX_URI.equals(aspectQName.getNamespaceURI())))
                {
                    AspectDefinition aspectDef = context.getDictionaryService().getAspect(aspectQName);
                    if (aspectDef == null)
                    {
                        throw new InvalidTypeException(aspectQName);
                    }
                    context.getNode().addAspect(aspectDef);
                }
            }
        }
        else if (JCRUUIDProperty.PROPERTY_NAME.equals(propertyName))
        {
            StringBuffer value = context.getValues().get(0);
            context.getNode().setUUID(value.toString());
        }   

        //
        // Note: ignore all other JCR specific properties
        //
        
        else if (JCRNamespace.JCR_URI.equals(propertyName.getNamespaceURI()))
        {
        }
        
        //
        // process all other properties
        //
        
        else
        {
            // apply the property values to the node
            NodeContext node = context.getNode();
            if (node.getTypeDefinition() == null)
            {
                throw new InvalidSerializedDataException("Node jcr:primaryType property has not been specified.");
            }
            
            // determine data type of value
            QName dataType = context.getType();
            DataTypeDefinition dataTypeDef = context.getDictionaryService().getDataType(dataType);
            if (dataTypeDef == null)
            {
                throw new InvalidTypeException(dataType);
            }
            node.addDatatype(propertyName, dataTypeDef);

            // determine if multi-value property
            if (context.isMultiValue())
            {
                node.addPropertyCollection(propertyName);
            }
            
            // add each value to the node
            List<StringBuffer> values = context.getValues();
            for (StringBuffer value : values)
            {
                // first, cast value to appropriate type (using JCR converters)
                Serializable objVal = (Serializable)session.getTypeConverter().convert(dataTypeDef, value.toString());
                String strValue = DefaultTypeConverter.INSTANCE.convert(String.class, objVal);
                node.addProperty(propertyName, strValue);
            }
        }
    }

    /**
     * Process end of value element
     * 
     * @param context
     */    
    private void processEndValue(ValueContext context)
    {
        PropertyContext property = context.getProperty();
        property.endValue();
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#characters(char[], int, int)
     */
    public void characters(char[] ch, int start, int length) throws SAXException
    {
        ElementContext context = (ElementContext)contextStack.peek();
        if (context instanceof ValueContext)
        {
            PropertyContext property = ((ValueContext)context).getProperty();
            property.appendCharacters(ch, start, length);
        }
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
     */
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
    {
        ElementContext context = (ElementContext)contextStack.peek();
        if (context instanceof ValueContext)
        {
            PropertyContext property = ((ValueContext)context).getProperty();
            property.appendCharacters(ch, start, length);
        }
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
     */
    public void processingInstruction(String target, String data) throws SAXException
    {
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
     */
    public void skippedEntity(String name) throws SAXException
    {
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
     */
    public void warning(SAXParseException exception) throws SAXException
    {
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
     */
    public void error(SAXParseException exception) throws SAXException
    {
    }

    /*
     *  (non-Javadoc)
     * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
     */
    public void fatalError(SAXParseException exception) throws SAXException
    {
    }

}
