Skip to content
XMLModelsLoader.java 7.76 KiB
Newer Older
/*
 * _____________________________________________________________________________
 * 
 * INAF - OATS National Institute for Astrophysics - Astronomical Observatory of
 * Trieste INAF - IA2 Italian Center for Astronomical Archives
 * _____________________________________________________________________________
 * 
 * Copyright (C) 2017 Istituto Nazionale di Astrofisica
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License Version 3 as published by the
 * Free Software Foundation.
 * 
 * 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.
 */
package it.inaf.ia2.tsm.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXB;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Class used to parse TASMAN XML configuration files used to defining schemata
 * structure and convert them into instances of {@link SchemaModel}.
 *
 * @author Sonia Zorba {@literal <zorba at oats.inaf.it>}
 */
public class XMLModelsLoader {

    private static final Logger LOG = LoggerFactory.getLogger(XMLModelsLoader.class);

    private final String[] xmlModelFileNames;
    private final File[] xmlModelExternalFiles;
    // key: doc version
    private final Map<String, Document> documents;
    private final Map<String, String> inheritanceGraph;
    private final Map<Integer, List<Document>> inheritanceLevels;

    public XMLModelsLoader(String[] xmlModelFileNames) {
        this(xmlModelFileNames, new File[]{});
    }

    public XMLModelsLoader(String[] xmlModelFileNames, File[] xmlModelExternalFiles) {
        this.xmlModelFileNames = xmlModelFileNames;
        this.xmlModelExternalFiles = xmlModelExternalFiles;
        this.documents = new HashMap<>();
        inheritanceGraph = new HashMap<>();
        inheritanceLevels = new HashMap<>();
    }

    /**
     * Loads the XML files and obtains schemata models.
     *
     * @return a {@code Map} having versions as keys and {@link SchemaModel}s
     * as values.
     */
    public Map<String, SchemaModel> load() {
        return load(XMLModelsLoader.class.getClassLoader());
    }

    /**
     * This variant is used for testing (specifying a {@code ClassLoader} it is
     * possible to use test resource files instead of real application files).
     */
    public Map<String, SchemaModel> load(ClassLoader classLoader) {

        try {

            loadDocumentsMap(classLoader);

            // It is necessary to apply inheritance in specific level order. (Example:
            // if v2 extends from v1 and v1 extends from v0 it is necessary to merge v1
            // with v0 first and then merge the result with v2).
            // So this inheritance data structures are built.
            buildInheritanceGraph();
            buildInheritanceLevels();

            for (int i = 0; i < inheritanceLevels.size(); i++) {
                for (Document doc : inheritanceLevels.get(i)) {
                    applyInheritance(doc, null);
                }
            }

            return getModelsMap();

        } catch (IOException | ParserConfigurationException | SAXException e) {
            throw new ModelLoadingException(e);
        }
    }

    private void loadDocumentsMap(ClassLoader classLoader) throws IOException, SAXException, ParserConfigurationException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = dbf.newDocumentBuilder();

        for (String xmlModelFileName : xmlModelFileNames) {
            try (InputStream in = classLoader.getResourceAsStream(xmlModelFileName)) {
                loadFile(builder, in);
            }
        }

        for (File xmlModelExternalFile : xmlModelExternalFiles) {
            try (FileInputStream fis = new FileInputStream(xmlModelExternalFile)) {
                loadFile(builder, fis);
    private void loadFile(DocumentBuilder builder, InputStream in) throws IOException, SAXException {
        Document doc = builder.parse(in);
        Element root = doc.getDocumentElement();
        String version = root.getAttribute("version");
        // TODO: XML Model validation

        // Documents loaded in a single XMLModelsLoader instance must
        // have different versions.
        assert documents.get(version) == null;
        documents.put(version, doc);
    }

    private void buildInheritanceGraph() {
        for (Document document : documents.values()) {
            Element root = document.getDocumentElement();
            String version = root.getAttribute("version");
            String extendsFrom = root.getAttribute("extends");
            if (extendsFrom == null || extendsFrom.isEmpty()) {
                extendsFrom = null;
            }
            inheritanceGraph.put(version, extendsFrom);
        }
    }

    private void buildInheritanceLevels() {
        for (Document document : documents.values()) {
            String version = document.getDocumentElement().getAttribute("version");
            int level = getInheritanceLevel(version, 0);
            List<Document> levelDocs = inheritanceLevels.get(level);
            if (levelDocs == null) {
                levelDocs = new ArrayList<>();
                inheritanceLevels.put(level, levelDocs);
            }
            levelDocs.add(document);
        }
    }

    private int getInheritanceLevel(String version, int count) {
        String inheritsFrom = inheritanceGraph.get(version);
        if (inheritsFrom == null) {
            return count;
        } else {
            return getInheritanceLevel(inheritsFrom, count + 1);
        }
    }

    private void applyInheritance(Document doc, Set<String> applied) throws ParserConfigurationException {
        String version = doc.getDocumentElement().getAttribute("version");
        String inheritFrom = inheritanceGraph.get(version);
        if (inheritFrom != null) {
            if (applied == null) {
                applied = new HashSet<>();
            }
            if (!applied.contains(inheritFrom)) {
                Document inheritedDocument = documents.get(inheritFrom);
                XMLMerger merger = new XMLMerger(inheritedDocument, doc);
                Document merged = merger.getMergedDocument();
                documents.put(version, merged);
                applied.add(inheritFrom);
                applyInheritance(merged, applied);
            }
        }
    }

    /**
     * Public exposed only for testing.
     */
    public Map<String, Document> getDocuments() {
        return documents;
    }

    private Map<String, SchemaModel> getModelsMap() {
        Map<String, SchemaModel> models = new HashMap<>();

        for (Map.Entry<String, Document> entry : documents.entrySet()) {
            DOMSource source = new DOMSource(entry.getValue().getDocumentElement());
            SchemaModel model = JAXB.unmarshal(source, SchemaModel.class);
            models.put(entry.getKey(), model);
        }

        return models;
    }
}