/*
 * 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.jdbc.schema;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.lib.meta.SourceTracker;
import org.apache.openjpa.lib.meta.XMLMetaDataParser;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.util.UserException;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;


/**
 * Custom SAX parser used to parse {@link Schema} objects. The parser
 * will place all parsed schemas into the current {@link SchemaGroup}, set
 * via the {@link #setSchemaGroup} method. This allows parsing of
 * multiple files into a single schema group.
 *  The parser deserializes from the following XML format:<br />
 * <code> &lt;!ELEMENT schemas (schema)+&gt;<br />
 * &lt;!ELEMENT schema (table|sequence)+&gt;<br />
 * &lt;!ATTLIST schema name CDATA #IMPLIED&gt;<br />
 * &lt;!ELEMENT table (column|index|pk|fk|unique)+&gt;<br />
 * &lt;!ATTLIST table name CDATA #REQUIRED&gt;<br />
 * &lt;!ELEMENT column EMPTY&gt;<br />
 * &lt;!ATTLIST column name CDATA #REQUIRED&gt;<br />
 * &lt;!ATTLIST column type (array|bigint|binary|bit|blob|char|clob
 * |date|decimal|distinct|double|float|integer|java_object
 * |longvarbinary|longvarchar|null|numeric|other|real|ref|smallint|struct
 * |time|timstamp|tinyint|varbinary|varchar) #REQUIRED&gt;<br />
 * &lt;!ATTLIST column type-name CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST column size CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST column decimal-digits CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST column not-null (true|false) "false"&gt;<br />
 * &lt;!ATTLIST column default CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST column auto-assign (true|false) "false"&gt;<br />
 * &lt;!ELEMENT index (on)*&gt;<br />
 * &lt;!ATTLIST index name CDATA #REQUIRED&gt;<br />
 * &lt;!ATTLIST index column CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST index unique (true|false) "false"&gt;<br />
 * &lt;!ELEMENT on EMPTY&gt;<br />
 * &lt;!ATTLIST on column CDATA #REQUIRED&gt;<br />
 * &lt;!ELEMENT pk (on)*&gt;<br /> &lt;!ATTLIST pk name CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST pk column CDATA #IMPLIED&gt;<br />
 * &lt;!ELEMENT fk (join)*&gt;<br />
 * &lt;!ATTLIST fk name CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST fk deferred (true|false) "false"&gt;<br />
 * &lt;!ATTLIST fk column CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST fk to-table CDATA #REQUIRED&gt;<br />
 * &lt;!ATTLIST fk delete-action (cascade|default|restrict|none|null)
 * "none"&gt;<br />
 * &lt;!ATTLIST fk update-action (cascade|default|restrict|none|null)
 * "none"&gt;<br /> &lt;!ELEMENT unique (on)*&gt;<br />
 * &lt;!ATTLIST unique name CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST unique column CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST unique deferred (true|false) "false"&gt;<br />
 * &lt;!ELEMENT join EMPTY&gt;<br />
 * &lt;!ATTLIST join column CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST join value CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST join to-column CDATA #REQUIRED&gt;<br />
 * &lt;!ELEMENT sequence EMPTY&gt;<br />
 * &lt;!ATTLIST sequence name CDATA #REQUIRED&gt;<br />
 * &lt;!ATTLIST sequence initial-value CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST sequence increment CDATA #IMPLIED&gt;<br />
 * &lt;!ATTLIST sequence allocate CDATA #IMPLIED&gt;<br />
 * </code>
 *  Schema parsers are not threadsafe.
 *
 * @author Abe White
 */
public class XMLSchemaParser
    extends XMLMetaDataParser
    implements SchemaParser {

    private static final Localizer _loc = Localizer.forPackage
        (XMLSchemaParser.class);

    private final DBDictionary _dict;

    // state for current parse
    private SchemaGroup _group = null;
    private Schema _schema = null;
    private Table _table = null;
    private PrimaryKeyInfo _pk = null;
    private IndexInfo _index = null;
    private UniqueInfo _unq = null;
    private ForeignKeyInfo _fk = null;
    private boolean _delay = false;

    // used to collect info on schema elements before they're resolved
    private final Collection<PrimaryKeyInfo> _pkInfos = new LinkedList<>();
    private final Collection<IndexInfo> _indexInfos = new LinkedList<>();
    private final Collection<UniqueInfo> _unqInfos = new LinkedList<>();
    private final Collection<ForeignKeyInfo> _fkInfos = new LinkedList<>();

    /**
     * Constructor. Supply configuration.
     */
    public XMLSchemaParser(JDBCConfiguration conf) {
        _dict = conf.getDBDictionaryInstance();
        setLog(conf.getLog(JDBCConfiguration.LOG_SCHEMA));
        setParseText(false);
        setSuffix(".schema");
    }

    @Override
    public boolean getDelayConstraintResolve() {
        return _delay;
    }

    @Override
    public void setDelayConstraintResolve(boolean delay) {
        _delay = delay;
    }

    @Override
    public void resolveConstraints() {
        resolvePrimaryKeys();
        resolveIndexes();
        resolveForeignKeys();
        resolveUniques();
        clearConstraintInfo();
    }

    /**
     * Clear constraint infos.
     */
    private void clearConstraintInfo() {
        _pkInfos.clear();
        _indexInfos.clear();
        _fkInfos.clear();
        _unqInfos.clear();
    }

    @Override
    public SchemaGroup getSchemaGroup() {
        if (_group == null)
            _group = new SchemaGroup();
        return _group;
    }

    @Override
    public void setSchemaGroup(SchemaGroup group) {
        _group = group;
    }

    /**
     * Parse the schema relating to the given class. The schemas will
     * be added to the current schema group.
     */
    @Override
    protected void finish() {
        // now resolve pk, idx, fk info
        super.finish();
        if (!_delay)
            resolveConstraints();
    }

    /**
     * Transforms the collected primary key information into actual
     * primary keys on the schema tables.
     */
    private void resolvePrimaryKeys() {
        PrimaryKeyInfo pkInfo;
        String colName;
        Column col;
        for (PrimaryKeyInfo info : _pkInfos) {
            pkInfo = info;
            for (String s : pkInfo.cols) {
                colName = s;
                col = pkInfo.pk.getTable().getColumn(colName);
                if (col == null)
                    throwUserException(_loc.get("pk-resolve", new Object[]
                            {colName, pkInfo.pk.getTable()}));
                pkInfo.pk.addColumn(col);
            }
        }
    }

    /**
     * Transforms the collected index information into actual
     * indexes on the schema tables.
     */
    private void resolveIndexes() {
        IndexInfo indexInfo;
        String colName;
        Column col;
        for (IndexInfo info : _indexInfos) {
            indexInfo = info;
            for (String s : indexInfo.cols) {
                colName = s;
                col = indexInfo.index.getTable().getColumn(colName);
                if (col == null)
                    throwUserException(_loc.get("index-resolve", new Object[]
                            {indexInfo.index, colName,
                                    indexInfo.index.getTable()}));
                indexInfo.index.addColumn(col);
            }
        }
    }

    /**
     * Transforms the collected foreign key information into actual
     * foreign keys on the schema tables.
     */
    private void resolveForeignKeys() {
        ForeignKeyInfo fkInfo;
        Table toTable;
        Column col;
        String colName;
        Column pkCol;
        String pkColName;
        PrimaryKey pk;
        Iterator<String> pks;
        Iterator<String> cols;
        for (ForeignKeyInfo info : _fkInfos) {
            fkInfo = info;
            toTable = _group.findTable(fkInfo.toTable);
            if (toTable == null || toTable.getPrimaryKey() == null)
                throwUserException(_loc.get("fk-totable", new Object[]
                        {fkInfo.fk, fkInfo.toTable, fkInfo.fk.getTable()}));

            // check if only one fk column listed using shortcut
            pk = toTable.getPrimaryKey();
            if (fkInfo.cols.size() == 1 && fkInfo.pks.size() == 0)
                fkInfo.pks.add(pk.getColumns()[0].getName());

            // make joins
            pks = fkInfo.pks.iterator();
            for (cols = fkInfo.cols.iterator(); cols.hasNext(); ) {
                colName = (String) cols.next();
                col = fkInfo.fk.getTable().getColumn(colName);
                if (col == null)
                    throwUserException(_loc.get("fk-nocol",
                            fkInfo.fk, colName, fkInfo.fk.getTable()));

                pkColName = (String) pks.next();
                pkCol = toTable.getColumn(pkColName);
                if (pkCol == null)
                    throwUserException(_loc.get("fk-nopkcol", new Object[]
                            {fkInfo.fk, pkColName, toTable,
                                    fkInfo.fk.getTable()}));

                fkInfo.fk.join(col, pkCol);
            }

            // make constant joins
            cols = fkInfo.constCols.iterator();
            for (Object value : fkInfo.consts) {
                colName = cols.next();
                col = fkInfo.fk.getTable().getColumn(colName);
                if (col == null)
                    throwUserException(_loc.get("fk-nocol",
                            fkInfo.fk, colName, fkInfo.fk.getTable()));

                fkInfo.fk.joinConstant(col, value);
            }

            pks = fkInfo.constColsPK.iterator();
            for (Object o : fkInfo.constsPK) {
                pkColName = pks.next();
                pkCol = toTable.getColumn(pkColName);
                if (pkCol == null)
                    throwUserException(_loc.get("fk-nopkcol", new Object[]
                            {fkInfo.fk, pkColName, toTable,
                                    fkInfo.fk.getTable()}));

                fkInfo.fk.joinConstant(o, pkCol);
            }
        }
    }

    /**
     * Transforms the collected unique constraint information into actual
     * constraints on the schema tables.
     */
    private void resolveUniques() {
        UniqueInfo unqInfo;
        String colName;
        Column col;
        for (UniqueInfo info : _unqInfos) {
            unqInfo = info;
            for (String s : unqInfo.cols) {
                colName = s;
                col = unqInfo.unq.getTable().getColumn(colName);
                if (col == null)
                    throwUserException(_loc.get("unq-resolve", new Object[]
                            {unqInfo.unq, colName, unqInfo.unq.getTable()}));
                unqInfo.unq.addColumn(col);
            }
        }
    }

    @Override
    protected void reset() {
        _schema = null;
        _table = null;
        _pk = null;
        _index = null;
        _fk = null;
        _unq = null;
        if (!_delay)
            clearConstraintInfo();
    }

    @Override
    protected Reader getDocType()
        throws IOException {
        return new InputStreamReader(XMLSchemaParser.class
            .getResourceAsStream("schemas-doctype.rsrc"));
    }

    @Override
    protected boolean startElement(String name, Attributes attrs)
        throws SAXException {
        switch (name.charAt(0)) {
            case's':
                if ("schema".equals(name))
                    startSchema(attrs);
                else if ("sequence".equals(name))
                    startSequence(attrs);
                return true;
            case't':
                startTable(attrs);
                return true;
            case'c':
                startColumn(attrs);
                return true;
            case'p':
                startPrimaryKey(attrs);
                return true;
            case'i':
                startIndex(attrs);
                return true;
            case'u':
                startUnique(attrs);
                return true;
            case'f':
                startForeignKey(attrs);
                return true;
            case'o':
                startOn(attrs);
                return true;
            case'j':
                startJoin(attrs);
                return true;
            default:
                return false;
        }
    }

    @Override
    protected void endElement(String name) {
        switch (name.charAt(0)) {
            case's':
                if ("schema".equals(name))
                    endSchema();
                break;
            case't':
                endTable();
                break;
            case'p':
                endPrimaryKey();
                break;
            case'i':
                endIndex();
                break;
            case'u':
                endUnique();
                break;
            case'f':
                endForeignKey();
                break;
        }
    }

    private void startSchema(Attributes attrs) {
        // creates group if not set
        SchemaGroup group = getSchemaGroup();

        String name = attrs.getValue("name");
        _schema = group.getSchema(name);
        if (_schema == null)
            _schema = group.addSchema(name);
    }

    private void endSchema() {
        _schema = null;
    }

    private void startSequence(Attributes attrs) {
        Sequence seq = _schema.addSequence(attrs.getValue("name"));
        Locator locator = getLocation().getLocator();
        if (locator != null) {
            seq.setLineNumber(locator.getLineNumber());
            seq.setColNumber(locator.getColumnNumber());
        }
        seq.setSource(getSourceFile(), SourceTracker.SRC_XML);
        try {
            String val = attrs.getValue("initial-value");
            if (val != null)
                seq.setInitialValue(Integer.parseInt(val));
            val = attrs.getValue("increment");
            if (val != null)
                seq.setIncrement(Integer.parseInt(val));
            val = attrs.getValue("allocate");
            if (val != null)
                seq.setAllocate(Integer.parseInt(val));
        } catch (NumberFormatException nfe) {
            throwUserException(_loc.get("bad-seq-num", seq.getFullName()));
        }
    }

    private void startTable(Attributes attrs) {
        _table = _schema.addTable(attrs.getValue("name"));
        _table.setSource(getSourceFile(), SourceTracker.SRC_XML);
        Locator locator = getLocation().getLocator();
        if (locator != null) {
            _table.setLineNumber(locator.getLineNumber());
            _table.setColNumber(locator.getColumnNumber());
        }
    }

    private void endTable() {
        _table = null;
    }

    private void startColumn(Attributes attrs) {
        Column col = _table.addColumn(attrs.getValue("name"));
        col.setType(_dict.getPreferredType(Schemas.getJDBCType
            (attrs.getValue("type"))));
        col.setTypeName(attrs.getValue("type-name"));
        String val = attrs.getValue("size");
        if (val != null)
            col.setSize(Integer.parseInt(val));
        val = attrs.getValue("decimal-digits");
        if (val != null)
            col.setDecimalDigits(Integer.parseInt(val));
        col.setNotNull("true".equals(attrs.getValue("not-null")));
        col.setAutoAssigned("true".equals(attrs.getValue("auto-assign"))
            || "true".equals(attrs.getValue("auto-increment"))); // old attr
        col.setDefaultString(attrs.getValue("default"));
    }

    private void startPrimaryKey(Attributes attrs) {
        _pk = new PrimaryKeyInfo();
        _pk.pk = _table.addPrimaryKey(attrs.getValue("name"));
        _pk.pk.setLogical("true".equals(attrs.getValue("logical")));

        String val = attrs.getValue("column");
        if (val != null)
            _pk.cols.add(val);
    }

    private void endPrimaryKey() {
        _pkInfos.add(_pk);
        _pk = null;
    }

    private void startIndex(Attributes attrs) {
        _index = new IndexInfo();
        _index.index = _table.addIndex(attrs.getValue("name"));
        _index.index.setUnique("true".equals(attrs.getValue("unique")));

        String val = attrs.getValue("column");
        if (val != null)
            _index.cols.add(val);
    }

    private void endIndex() {
        _indexInfos.add(_index);
        _index = null;
    }

    private void startUnique(Attributes attrs) {
        _unq = new UniqueInfo();
        _unq.unq = _table.addUnique(attrs.getValue("name"));
        _unq.unq.setDeferred("true".equals(attrs.getValue("deferred")));

        String val = attrs.getValue("column");
        if (val != null)
            _unq.cols.add(val);
    }

    private void endUnique() {
        _unqInfos.add(_unq);
        _unq = null;
    }

    private void startForeignKey(Attributes attrs) {
        _fk = new ForeignKeyInfo();
        _fk.fk = _table.addForeignKey(attrs.getValue("name"));

        if ("true".equals(attrs.getValue("deferred")))
            _fk.fk.setDeferred(true);

        // set update action before delete action in case user incorrectly
        // sets update-action to "none" when there is a delete-action; otherwise
        // setting the update-action to "none" will also automatically set the
        // delete-action to "none", since FKs cannot have one actio be none and
        // the other be non-none
        String action = attrs.getValue("update-action");
        if (action != null)
            _fk.fk.setUpdateAction(ForeignKey.getAction(action));
        action = attrs.getValue("delete-action");
        if (action != null)
            _fk.fk.setDeleteAction(ForeignKey.getAction(action));

        _fk.toTable = attrs.getValue("to-table");
        String val = attrs.getValue("column");
        if (val != null)
            _fk.cols.add(val);
    }

    private void endForeignKey() {
        _fkInfos.add(_fk);
        _fk = null;
    }

    private void startOn(Attributes attrs) {
        String col = attrs.getValue("column");
        if (_pk != null)
            _pk.cols.add(col);
        else if (_index != null)
            _index.cols.add(col);
        else
            _unq.cols.add(col);
    }

    private void startJoin(Attributes attrs) {
        String col = attrs.getValue("column");
        String toCol = attrs.getValue("to-column");
        String val = attrs.getValue("value");
        if (val == null) {
            _fk.cols.add(col);
            _fk.pks.add(toCol);
        } else if (col == null) {
            _fk.constsPK.add(convertConstant(val));
            _fk.constColsPK.add(toCol);
        } else {
            _fk.consts.add(convertConstant(val));
            _fk.constCols.add(col);
        }
    }

    private static Object convertConstant(String val) {
        if ("null".equals(val))
            return null;
        if (val.startsWith("'"))
            return val.substring(1, val.length() - 1);
        if (val.indexOf('.') == -1)
            return new Long(val);
        return new Double(val);
    }

    private void throwUserException(Message msg) {
        throw new UserException(getSourceName() + ": " + msg.getMessage());
    }

    /**
     * Used to hold primary key info before it is resolved.
     */
    private static class PrimaryKeyInfo {

        public PrimaryKey pk = null;
        public Collection<String> cols = new LinkedList<>();
    }

    /**
     * Used to hold index info before it is resolved.
     */
    private static class IndexInfo {

        public Index index = null;
        public Collection<String> cols = new LinkedList<>();
    }

    /**
     * Used to hold unique constraint info before it is resolved.
     */
    public static class UniqueInfo {

        public Unique unq = null;
        public Collection<String> cols = new LinkedList<>();
    }

    /**
     * Used to hold foreign key info before it is resolved.
     */
    private static class ForeignKeyInfo {

        public ForeignKey fk = null;
        public String toTable = null;
        public Collection<String> cols = new LinkedList<>();
        public Collection<String> pks = new LinkedList<>();
        public Collection<Object> consts = new LinkedList<>();
        public Collection<String> constCols = new LinkedList<>();
        public Collection<Object> constsPK = new LinkedList<>();
        public Collection<String> constColsPK = new LinkedList<>();
    }
}
