// Licensed to the Apache Software Foundation (ASF) under one or more contributor
// license agreements.  See the NOTICE.txt 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.oodt.cas.metadata;

//JDK imports

import org.apache.oodt.commons.xml.XMLUtils;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URLEncoder;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 * @author mattmann
 * @author bfoster
 * @version $Revision$
 * 
 * <p>
 * {@link Metadata} that's {@link Serializable}.
 * </p>.
 */
public class SerializableMetadata extends Metadata implements Serializable {

    private static Logger LOG = Logger.getLogger(SerializableMetadata.class.getName());
    private static final long serialVersionUID = 6863087581652632499L;

    private String xmlEncoding;

    private boolean useCDATA;
    
    public SerializableMetadata() {
    	super();
    	this.xmlEncoding = "UTF-8";
    	this.useCDATA = false;
    }
    
    /**
     * Accepts any encoding which is supported by java.net.URLEncoder If
     * useCDATA is set true then element text will be wrapped in a CDATA tag.
     * 
     * @param xmlEncoding
     *            The encoding to use when generating XML version of a
     *            SerializableMetadata
     * @param useCDATA
     *            whether or not to use CDATA tags around an element's text.
     * @throws InstantiationException
     *             if xmlEncoding equals null
     */
    public SerializableMetadata(String xmlEncoding, boolean useCDATA)
            throws InstantiationException {
        super();
        if (xmlEncoding == null) {
            throw new InstantiationException("xmlEncoding cannot be null");
        }
        this.xmlEncoding = xmlEncoding;
        this.useCDATA = useCDATA;
    }

    public SerializableMetadata(Metadata metadata) {
    	this(metadata, "UTF-8", false);
	}
    
    public SerializableMetadata(InputStream inputStream) throws IOException {
    	this(inputStream, "UTF-8", false);
    }
    
    public SerializableMetadata(InputStream inputStream, String xmlEncoding,
            boolean useCDATA) throws IOException {
        this.xmlEncoding = xmlEncoding;
        this.useCDATA = useCDATA;
    	this.loadMetadataFromXmlStream(inputStream);
    }

    
    /**
     * Builds a SerializableMetadata object from a Metadata object
     * 
     * @param metadata
     * @param xmlEncoding
     */
    public SerializableMetadata(Metadata metadata, String xmlEncoding,
            boolean useCDATA) {
        this.replaceMetadata(metadata);
        this.xmlEncoding = xmlEncoding;
        this.useCDATA = useCDATA;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.writeObject(this.xmlEncoding);
        out.writeBoolean(useCDATA);
        this.writeMetadataToXmlStream(out);
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        this.xmlEncoding = (String) in.readObject();
        this.useCDATA = in.readBoolean();
        this.loadMetadataFromXmlStream(in);
    }

    public String getEncoding() {
        return this.xmlEncoding;
    }

    public boolean isUsingCDATA() {
        return this.useCDATA;
    }

    /**
     * Writes out this SerializableMetadata object in XML format to the
     * OutputStream provided
     * 
     * @param os
     *            The OutputStream this method writes to
     * @throws IOException
     *             for any Exception
     */
    public void writeMetadataToXmlStream(OutputStream os) throws IOException {
        try {
            // Prepare the DOM document for writing
            Source source = new DOMSource(this.toXML());
            Result result = new StreamResult(os);

            // Write the DOM document to the file
            Transformer xformer = TransformerFactory.newInstance()
                    .newTransformer();
            xformer.setOutputProperty(OutputKeys.ENCODING, this.xmlEncoding);
            xformer.setOutputProperty(OutputKeys.INDENT, "yes");
            xformer.transform(source, result);

        } catch (Exception e) {
            LOG.log(Level.SEVERE, e.getMessage());
            throw new IOException("Error generating metadata xml file!: "
                    + e.getMessage());
        }
    }

    public Document toXML() throws IOException {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory
                    .newInstance();
            factory.setNamespaceAware(true);
            Document document = factory.newDocumentBuilder().newDocument();

            Element root = document.createElementNS("http://oodt.jpl.nasa.gov/1.0/cas", "metadata");
            root.setPrefix("cas");
            document.appendChild(root);

            // now add the set of metadata elements in the properties object
            for (String key : this.getAllKeys()) {
                Element metadataElem = document.createElement("keyval");
                Element keyElem = document.createElement("key");
                if (this.useCDATA) {
                    keyElem.appendChild(document.createCDATASection(key));
                } else {
                    keyElem.appendChild(document.createTextNode(URLEncoder.encode(key, this.xmlEncoding)));
                }
                
                metadataElem.appendChild(keyElem);

                metadataElem.setAttribute("type", "vector");

                for (String value : this.getAllMetadata(key)) {
                    Element valElem = document.createElement("val");
                    if (value == null) {
                        throw new Exception("Attempt to write null value "
                                + "for property: [" + key + "]: val: [null]");
                    }
                    if (this.useCDATA) {
                        valElem.appendChild(document
                            .createCDATASection(value));
                    } else {
                        valElem.appendChild(document.createTextNode(URLEncoder
                            .encode(value, this.xmlEncoding)));
                    }
                    metadataElem.appendChild(valElem);
                }
                root.appendChild(metadataElem);
            }
            return document;
        } catch (Exception e) {
            LOG.log(Level.SEVERE, e.getMessage());
            throw new IOException(
                    "Failed to create XML DOM Document for SerializableMetadata : "
                            + e.getMessage());
        }
    }

    /**
     * Reloads this SerializableMetadata from an InputStream in the format
     * created by writeMetadataToXmlStream(OutputStream).
     * 
     * @param in
     *            The InputStream which this object is loaded from
     * @throws IOException
     *             for any exception
     */
    public void loadMetadataFromXmlStream(InputStream in) throws IOException {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory
                    .newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder parser = factory.newDocumentBuilder();
            Element root = parser.parse(new InputSource(in))
                    .getDocumentElement();

            NodeList keyValElems = root.getElementsByTagName("keyval");

            for (int i = 0; i < keyValElems.getLength(); i++) {
                Element keyValElem = (Element) keyValElems.item(i);

                String elemName = XMLUtils.read(keyValElem, "key",
                        this.xmlEncoding);
                List<String> elemValues = XMLUtils.readMany(keyValElem, "val",
                        this.xmlEncoding);
                this.addMetadata(elemName, elemValues);
            }
        } catch (Exception e) {
            throw new IOException(
                    "Failed to load SerializableMetadata from ObjectInputStream : "
                            + e.getMessage());
        }
    }

    /**
     * Converts SerializableMetadata into a plain metadata object
     * 
     * @return Metadata object with the same metadata that this
     *         SerializableMetadata contains
     */
    public Metadata getMetadata() {
        Metadata metadata = new Metadata();
        metadata.addMetadata(this.getMap());
        return metadata;
    }

}