/*
 * 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.openjpa.lib.meta;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.transform.Result;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.xml.Commentable;
import org.apache.openjpa.lib.xml.XMLWriter;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

/**
 * Abstract base type for serlializers that transfer groups of objects
 * to XML. Includes a way of serializing objects back to the XML files
 * they were parsed from. Serializers are not thread safe.
 *
 * @author Abe White
 */
public abstract class XMLMetaDataSerializer implements MetaDataSerializer {

    private static final Localizer _loc = Localizer.forPackage
        (XMLMetaDataSerializer.class);
    private static final SAXTransformerFactory _factory =
        (SAXTransformerFactory) TransformerFactory.newInstance();

    private Log _log = null;

    // current serialization state
    private final AttributesImpl _attrs = new AttributesImpl();
    private ContentHandler _handler = null;
    private int _flags = 0;
    private File _backup = null;

    /**
     * The log to write to.
     */
    public Log getLog() {
        return _log;
    }

    /**
     * The log to write to.
     */
    public void setLog(Log log) {
        _log = log;
    }

    @Override
    public void serialize(int flags) throws IOException {
        serialize((Map) null, flags);
    }

    @Override
    public void serialize(Map output, int flags)
        throws IOException {
        Map<File, Collection<Object>> files = getFileMap();
        if (files == null)
            return;

        // for each file, serialize objects
        for (Entry<File, Collection<Object>> entry : files.entrySet()) {
            File file = entry.getKey();
            Collection<Object> fileObjs = entry.getValue();
            if (_log != null && _log.isInfoEnabled())
                _log.info(_loc.get("ser-file", file));

            try {
                TransformerHandler trans = _factory.newTransformerHandler();
                Writer writer;
                if (output == null) {
                    _backup = prepareWrite(file);
                    writer = new FileWriter(file);
                } else
                    writer = new StringWriter();

                Writer xml = writer;
                if ((flags & PRETTY) > 0)
                    xml = new XMLWriter(writer);
                trans.setResult(new StreamResult(xml));
                serialize(fileObjs, trans, flags);

                if (output != null)
                    output.put(file, ((StringWriter) writer).toString());
            } catch (SAXException | TransformerConfigurationException se) {
                throw new IOException(se.toString());
            }
        }
    }

    /**
     * Prepare to write to the given file. Back up the file and make sure the
     * path to it is created.
     */
    protected File prepareWrite(File file) throws IOException {
        File backup = Files.backup(file, false);
        if (backup == null) {
            File parent = file.getParentFile();
            if (parent != null && !AccessController.doPrivileged(
                    J2DoPrivHelper.existsAction(parent)))
                AccessController.doPrivileged(
                    J2DoPrivHelper.mkdirsAction(parent));
        }
        return backup;
    }

    /**
     * Returns a {@link Map} with keys of the {@link File} to be
     * written to, and values of a {@link Collection} of
     * {@link SourceTracker} instances.
     */
    protected Map<File, Collection<Object>> getFileMap() {
        Collection<Object> objs = getObjects();
        if (objs == null || objs.isEmpty())
            return null;

        // create a map of files to lists of objects
        Map<File, Collection<Object>> files =
            new HashMap<>();
        File file;
        Collection<Object> fileObjs;
        for(Object obj : objs) {
            file = getSourceFile(obj);
            if (file == null) {
                if (_log != null && _log.isTraceEnabled())
                    _log.trace(_loc.get("no-file", obj));
                continue;
            }

            fileObjs = (Collection<Object>) files.get(file);
            if (fileObjs == null) {
                fileObjs = new LinkedList<>();
                files.put(file, fileObjs);
            }
            fileObjs.add(obj);
        }

        return files;
    }

    /**
     * Return the source file for the given instance. By default, checks
     * to see if the instance implements {@link SourceTracker}.
     */
    protected File getSourceFile(Object obj) {
        if (obj instanceof SourceTracker)
            return ((SourceTracker) obj).getSourceFile();
        return null;
    }

    @Override
    public void serialize(File file, int flags) throws IOException {
        if (_log != null)
            _log.info(_loc.get("ser-file", file));

        _backup = prepareWrite(file);
        try {
            FileWriter out = new FileWriter(
                AccessController.doPrivileged(
                    J2DoPrivHelper.getCanonicalPathAction(file)),
                (flags & APPEND) > 0);
            serialize(out, flags);
            out.close();
        } catch (PrivilegedActionException pae) {
            throw (IOException) pae.getException();
        }
    }

    @Override
    public void serialize(Writer out, int flags) throws IOException {
        try {
            if ((flags & PRETTY) > 0)
                serialize(new StreamResult(new XMLWriter(out)), flags);
            else
                serialize(new StreamResult(out), flags);
        } catch (SAXException se) {
            throw new IOException(se.toString());
        }
    }

    /**
     * Serialize the current set of objects to the given result.
     */
    public void serialize(Result result, int flags) throws SAXException {
        try {
            TransformerHandler trans = _factory.newTransformerHandler();
            trans.setResult(result);
            serialize(trans, flags);
        } catch (TransformerConfigurationException tce) {
            throw new SAXException(tce);
        }
    }

    /**
     * Serilize the current set of objects to a series of SAX events on the
     * given handler.
     */
    public void serialize(ContentHandler handler, int flags)
        throws SAXException {
        serialize(getObjects(), handler, flags);
    }

    /**
     * Serialize the given collection of objects to the given handler.
     */
    private void serialize(Collection<Object> objs, ContentHandler handler,
        int flags) throws SAXException {
        if (_log != null && _log.isTraceEnabled())
            _log.trace(_loc.get("ser-objs", objs));

        _handler = handler;
        _flags = flags;
        try {
            if (!objs.isEmpty()) {
                handler.startDocument();
                serialize(objs);
                handler.endDocument();
            }
        }
        finally {
            reset();
        }
    }

    /**
     * Whether this serialization is in verbose mode.
     */
    protected boolean isVerbose() {
        return (_flags & VERBOSE) > 0;
    }

    /**
     * The backup file made for the current file being parsed.
     */
    protected File currentBackupFile() {
        return _backup;
    }

    /**
     * Start an element with the current attribute settings. Clears the
     * attributes as well.
     */
    protected void startElement(String name) throws SAXException {
        _handler.startElement("", name, name, _attrs);
        _attrs.clear();
    }

    /**
     * End the current element.
     */
    protected void endElement(String name) throws SAXException {
        _handler.endElement("", name, name);
    }

    /**
     * Add text to the current element.
     */
    protected void addText(String text) throws SAXException {
        _handler.characters(text.toCharArray(), 0, text.length());
    }

    /**
     * Add an attribute to the current group.
     */
    protected void addAttribute(String name, String value) {
        _attrs.addAttribute("", name, name, "CDATA", value);
    }

    /**
     * The current attributes.
     */
    protected Attributes getAttributes() {
        return _attrs;
    }

    /**
     * Add a comment to the stream.
     */
    protected void addComments(String[] comments) throws SAXException {
        if (comments == null || comments.length == 0
            || !(_handler instanceof LexicalHandler))
            return;

        LexicalHandler lh = (LexicalHandler) _handler;
        char[] chars;
        for (String comment : comments) {
            chars = comment.toCharArray();
            lh.comment(chars, 0, chars.length);
        }
    }

    /**
     * Write the given entity's comments. By default, tests if entity is
     * {@link Commentable}.
     */
    protected void addComments(Object obj) throws SAXException {
        if (obj instanceof Commentable)
            addComments(((Commentable) obj).getComments());
    }

    /**
     * Reset serialization state for the next document.
     */
    protected void reset() {
        _attrs.clear();
        _handler = null;
        _flags = 0;
        _backup = null;
    }

    /**
     * Serialize the given set of objects.
     */
    protected abstract void serialize(Collection<Object> objs) throws
            SAXException;

    /**
     * Return the current set of objects for serialization.
     */
    protected abstract Collection<Object> getObjects();
}
