com.c4om.autoconf.ulysses.extra.svinchangesetgenerator.SVINChangesetGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.c4om.autoconf.ulysses.extra.svinchangesetgenerator.SVINChangesetGenerator.java

Source

/*
Copyright 2014 Universidad Politcnica de Madrid - Center for Open Middleware (http://www.centeropenmiddleware.com)
    
Licensed 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 com.c4om.autoconf.ulysses.extra.svinchangesetgenerator;

import static com.c4om.utils.xmlutils.JDOMUtils.*;
import static com.c4om.utils.xmlutils.XMLLibsShortcuts.*;
import static com.c4om.utils.xmlutils.XPathUtils.*;
import static com.c4om.utils.xmlutils.constants.AutoconfXMLConstants.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;

import net.sf.saxon.s9api.SaxonApiException;

import org.jdom2.Attribute;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;

import com.c4om.autoconf.ulysses.configanalyzer.core.datastructures.Changeset;
import com.c4om.autoconf.ulysses.configanalyzer.core.datastructures.ConfigurationAnalysisContext;
import com.c4om.autoconf.ulysses.configanalyzer.core.datastructures.ExtractedConfiguration;
import com.c4om.autoconf.ulysses.configanalyzer.core.datastructures.ObjectiveSolution;
import com.c4om.autoconf.ulysses.configanalyzer.core.datastructures.impl.XMLDescribedChangeset;
import com.c4om.autoconf.ulysses.configanalyzer.core.exceptions.ConfigurationIntegrationException;
import com.c4om.autoconf.ulysses.interfaces.Target;
import com.c4om.utils.xmlutils.JDOMUtils;
import com.c4om.utils.xmlutils.constants.AutoconfXMLConstants;

/**
 * Main class of the SVINApplier
 */
public class SVINChangesetGenerator {

    /**
     * Key for the settings property with the XPath namespaces list
     */
    private static final String PROPERTY_KEY_NAMESPACES_LIST = "namespacesList";

    /**
     * Key for the settings property with the runtime configuration file/folder name.
     */
    private static final String PROPERTY_KEY_RUNTIME_CONFIGURATION_FILENAME = "runtimeConfiguration.filename";

    /**
     * Key for the settings property with the path to the metamodel values conversion XSLT resource.
     */
    private static final String PROPERTY_KEY_METAMODEL_VALUES_CONVERSION_XSLT = "metamodelValuesConversionXSLT";

    /**
     * XSLT for transforming metamodel info into a suggestable node
     */
    private Document xsltTransformMetamodelDocument;

    /**
     * Cache that contains the JDOM2 {@link Document} objects that represent
     * documents at concrete paths.
     */
    private Map<String, Document> documentCache;

    /**
     * The name of the runtime configuration folder
     */
    private String runtimeConfigurationFolderName;

    /**
     * XPath namespaces to be used at queries.
     */
    private List<Namespace> xpathNamespaces;

    /**
     * Constructor
     * @param pathToSettings the path to the settings properties file resource
     * @throws IOException 
     * 
     */
    public SVINChangesetGenerator(String pathToSettings) throws IOException {
        this.documentCache = new HashMap<>();
        this.xpathNamespaces = new ArrayList<>();

        Properties settings = new Properties();
        settings.load(getResourceStream(pathToSettings));
        String pathToMetamodelConversionXSLT = settings.getProperty(PROPERTY_KEY_METAMODEL_VALUES_CONVERSION_XSLT,
                "xslt/MetamodelTextsInfoToDefaultValues.xsl");
        if (pathToMetamodelConversionXSLT == null) {
            throw new IllegalArgumentException(
                    "'" + PROPERTY_KEY_METAMODEL_VALUES_CONVERSION_XSLT + "' not found at the given properties.");
        }
        try {
            this.xsltTransformMetamodelDocument = loadJDOMDocumentFromStream(
                    getResourceStream(pathToMetamodelConversionXSLT));
        } catch (JDOMException | IOException e) {
            this.xsltTransformMetamodelDocument = null;
        }
        String runtimeConfigurationFolderName = settings.getProperty(PROPERTY_KEY_RUNTIME_CONFIGURATION_FILENAME);
        if (runtimeConfigurationFolderName == null) {
            throw new IllegalArgumentException(
                    "'" + PROPERTY_KEY_RUNTIME_CONFIGURATION_FILENAME + "' not found at the given properties.");
        }
        this.runtimeConfigurationFolderName = runtimeConfigurationFolderName;

        String namespacesListSetting = settings.getProperty(PROPERTY_KEY_NAMESPACES_LIST);
        if (namespacesListSetting != null) {
            String[] namespacesListSettingSplit = namespacesListSetting.split(";");
            Pattern pattern = Pattern.compile("^(?<prefix>[^:]+):(?<uri>.+)$");
            for (String pair : namespacesListSettingSplit) {
                Matcher matcher = pattern.matcher(pair);
                if (!matcher.matches()) {
                    throw new IllegalArgumentException("Pair '" + pair + "' does not follow the specified syntax.");
                }
                String prefix = matcher.group("prefix");
                String uri = matcher.group("uri");
                xpathNamespaces.add(Namespace.getNamespace(prefix, uri));
            }
        }

    }

    /**
     * This method extracts the source configuration of objective solutions, supposed that the solution id is such that it follows this regular expression: 
     * ^<i>extractedConfigurationId</i>\-(?:internalRelationships|weakRelationships|applicationRequirements)$
     * @param context the analysis context
     * @param objectiveSolution the objective
     * @return the source configuration of the objective
     */
    @SuppressWarnings("unchecked")
    private ExtractedConfiguration<File> extractSourceConfigurationOfObjectiveSolution(
            ConfigurationAnalysisContext context, ObjectiveSolution<?> objectiveSolution) {
        ExtractedConfiguration<File> sourceConfiguration = null;
        //We must look for the extracted configuration that lead to this objective.
        for (ExtractedConfiguration<?> extractedConfiguration : context.getExtractedConfigurations()) {
            //if(objectiveSolution.getId().matches("^"+extractedConfiguration.getId()+"\\-(?:internalRelationships|weakRelationships|applicationRequirements)$")){
            if (objectiveSolution.getTarget().equals(extractedConfiguration.getTarget())) {

                sourceConfiguration = (ExtractedConfiguration<File>) extractedConfiguration;
            }
        }
        return sourceConfiguration;
    }

    /**
     * It returns the runtime configuration from a list of files
     * @param extractedConfiguration the extracted configuration
     * @return the runtime configuration {@link File} object
     */
    private File getRuntimeConfiguration(ExtractedConfiguration<File> extractedConfiguration) {
        File runtimeConfigurationFile = null;
        for (File file : extractedConfiguration.getConfigurationData()) {
            if (file.getName().equals(this.runtimeConfigurationFolderName) && file.isDirectory()) {
                runtimeConfigurationFile = file;
                break;
            }
        }
        return runtimeConfigurationFile;
    }

    /**
     * Wrapper method for the typical task of getting an {@link InputStream} for a resource file.
     * @param path the resource path
     * @return the {@link InputStream} to the resource
     */
    private InputStream getResourceStream(String path) {
        try {
            InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(path);
            if (resourceAsStream == null) {
                resourceAsStream = new FileInputStream(path);
            }
            return resourceAsStream;
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * Recursive method that mixes two XML trees in the following way:
     * <ul>
     * <li>If a child exists at the source leaf but not at destination, the
     * element is copied as a child to the destination leaf</li>
     * <li>If a child exists at both the source and the destination leafs and
     * the source child has children, this method is recursively called to mix
     * both children</li>
     * </ul>
     * Some important remarks:
     * <ul>
     * <li>Equality comparison is not made via common methods but via
     * {@link JDOMUtils#elementsEqualAtCNameAndAttributes(Element, Element)}
     * .</li>
     * <li>Results of this method are returned as changes to the
     * destinationLeaf.</li>
     * <li>An attribute may be appended to all the elements added to the
     * destination leaf by this method.</li>
     * <li>Elements of a concrete namespace can be ignored by this method, if desired.</li>
     * </ul>
     * 
     * @param sourceLeaf
     *            The source leaf to mix into the destination leaf. It remains
     *            unchanged.
     * @param destinationLeaf
     *            The destination leaf, where the source leaf will be mixed
     *            into. Results will be returned as changes at this element, so
     *            IT WILL NOT REMAIN UNCHANGED AFTER THIS METHOD (normally). You
     *            should consider using {@link Element#clone()} if necessary.
     * @param metadataAttributeToAppend
     *            an attribute to be appended to each element added by this
     *            method to the destinationLeaf. If a node with descendants is
     *            added, the attribute will be added only to the top-level
     *            element (not to all the descendants). If null, no attribute
     *            will be added.
     * @param ignoreNamespaceURIIfTextPresent any element whose namespace URI matches this one 
     *            will be ignored if the source element has text content. 
     *            If null, no element is ignored.
     */
    private void mixTreesRecursive(Element sourceLeaf, Element destinationLeaf, Attribute metadataAttributeToAppend,
            String ignoreNamespaceURIIfTextPresent) {
        List<Content> sourceLeafContent = sourceLeaf.getContent();
        //j is the index for "only-element" content
        for (int i = 0, j = 0; i < sourceLeafContent.size(); i++) {
            Content currentContent = sourceLeafContent.get(i);
            if (!(currentContent instanceof Element)) {
                continue;
            }

            Element currentSourceChild = (Element) currentContent;

            Element currentDestinationChild = searchElementEqualAtCNameAndAttributes(destinationLeaf.getChildren(),
                    currentSourceChild);

            if (currentDestinationChild == null) {

                if (ignoreNamespaceURIIfTextPresent != null && !destinationLeaf.getTextNormalize().equals("")
                        && ignoreNamespaceURIIfTextPresent.equals(currentSourceChild.getNamespaceURI())) {
                    continue;
                }

                // There is not equivalent node at destination, so we copy the
                // whole currentSourceChild.
                Element elementToAdd = currentSourceChild.clone();
                if (metadataAttributeToAppend != null) {
                    elementToAdd.setAttribute(metadataAttributeToAppend.clone());
                }
                destinationLeaf.addContent(j, elementToAdd);
            } else {
                // Element exists at destination. If it has children, we recurse
                // to fill them (they might be not completely filled).
                if (currentSourceChild.getChildren().size() > 0) {
                    mixTreesRecursive(currentSourceChild, currentDestinationChild, metadataAttributeToAppend,
                            ignoreNamespaceURIIfTextPresent);
                }
            }
            j++;
        }
    }

    /**
     * This method generates a changeset document, which describes what nodes
     * must be added and replaced. It generates it from the SVRLInterpreter
     * report passed at constructor.
     * 
     * @param pathToConfiguration path to the runtime configuration.
     * 
     * @param reportDocument the report document (objective solution description).
     * 
     * @return The generated changeset document.
     * 
     * @throws JDOMException
     *             If there are problems at JDOM2 XML parsings
     * @throws IOException
     *             I/O problems
     * @throws SaxonApiException
     *             problems with Saxon API while transforming metamodel
     *             suggestions into partial autocomplete nodes
     * @throws ParserConfigurationException
     *             problems with javax.xml APIs while transforming metamodel
     *             suggestions into partial autocomplete nodes
     */
    public Document getSingleChangesetDocument(String pathToConfiguration, Document reportDocument)
            throws JDOMException, IOException, SaxonApiException, ParserConfigurationException {
        Element resultRoot = new Element("changeset", AutoconfXMLConstants.NAMESPACE_SVINAPPLIER);
        resultRoot.addNamespaceDeclaration(NAMESPACE_AUTOCONF_METADATA); // To
        // prevent
        // several
        // "xmlns:*****"
        // attributes
        // to
        // appear
        // everywhere
        Document result = new Document(resultRoot);
        Element reportElement = reportDocument.getRootElement();
        for (Element currentDiscrepancyElement : reportElement.getChildren()) {
            boolean isCreate = false;
            Element interestingPathsElement = currentDiscrepancyElement.getChild("interestingPaths",
                    NAMESPACE_SVRL_INTERPETER_REPORT);
            String searchPathText = interestingPathsElement.getAttributeValue("search-path");
            String basePathText = interestingPathsElement.getAttributeValue("base-path");
            String keySubpathText = interestingPathsElement.getAttributeValue("key-subpath");
            // First, we look for a path to search the element where discrepancy
            // took place (if it exists)
            String[] docAndPath;
            String searchPathInternal;
            if (searchPathText == null) {
                docAndPath = divideDocAndPath(basePathText);
                searchPathInternal = docAndPath[1] + "[" + keySubpathText + "]";
            } else {
                docAndPath = divideDocAndPath(searchPathText);
                searchPathInternal = docAndPath[1];
            }
            if (!documentCache.containsKey(docAndPath[0])) {
                documentCache.put(docAndPath[0],
                        loadJDOMDocumentFromFile(new File(pathToConfiguration + "/" + docAndPath[0])));
            }
            Document currentDoc = documentCache.get(docAndPath[0]);
            List<Element> discordingElementAtDocList = performJAXENXPath(searchPathInternal, currentDoc,
                    Filters.element(), xpathNamespaces);
            if (discordingElementAtDocList.size() == 0) {
                isCreate = true;
            }
            if (isCreate) {
                Element nodeToCreate = currentDiscrepancyElement
                        .getChild("suggestedPartialNode", NAMESPACE_SVRL_INTERPETER_REPORT).getChildren().get(0)
                        .clone();
                //Sometimes, svinrep namespace is declared here (it is not clear why). We must remove it.
                nodeToCreate.removeNamespaceDeclaration(NAMESPACE_SVRL_INTERPETER_REPORT);
                boolean thereAreMetamodelSuggestions = currentDiscrepancyElement
                        .getChild("metamodelSuggestions", NAMESPACE_SVRL_INTERPETER_REPORT).getChildren()
                        .size() > 0;
                if (thereAreMetamodelSuggestions) {
                    Element metamodelSuggestionUntransformed = currentDiscrepancyElement
                            .getChild("metamodelSuggestions", NAMESPACE_SVRL_INTERPETER_REPORT).getChildren().get(0)
                            .clone();
                    Document suggestionMiniDocument = new Document(metamodelSuggestionUntransformed);
                    Document suggestionMiniDocumentTransformed = performXSLT(suggestionMiniDocument,
                            xsltTransformMetamodelDocument);
                    Element metamodelSuggestion = suggestionMiniDocumentTransformed.getRootElement();
                    Attribute metadataAttribute = new Attribute("autogen-from", "metamodel",
                            NAMESPACE_AUTOCONF_METADATA);
                    mixTreesRecursive(metamodelSuggestion, nodeToCreate, metadataAttribute,
                            NAMESPACE_AUTOCONF_METADATA.getURI());
                } else {
                    Attribute mayNeedManualCompletion = new Attribute("may-need-completion", "true",
                            NAMESPACE_AUTOCONF_METADATA);
                    nodeToCreate.setAttribute(mayNeedManualCompletion);
                }
                Element createNodeElement = new Element("add-node", AutoconfXMLConstants.NAMESPACE_SVINAPPLIER);
                final String REGEXP_TO_GET_PARENT_PATH = "(.+)(/[^\\[\\]/]+(\\[.+\\])?)$";
                Pattern patternToGetParentPath = Pattern.compile(REGEXP_TO_GET_PARENT_PATH);
                Matcher matcherToGetParentPath = patternToGetParentPath.matcher(searchPathInternal);
                matcherToGetParentPath.matches();
                String pathToParent = matcherToGetParentPath.group(1);
                Attribute pathToParentAttr = new Attribute("underParentAtPath", pathToParent);
                Attribute documentToChangeAttr = new Attribute("atResource", docAndPath[0]);
                createNodeElement.setAttribute(documentToChangeAttr);
                createNodeElement.setAttribute(pathToParentAttr);
                createNodeElement.addContent(nodeToCreate);
                resultRoot.addContent(createNodeElement);

            } else {
                for (int i = 0; i < discordingElementAtDocList.size(); i++) {
                    Element nodeToModify = currentDiscrepancyElement
                            .getChild("suggestedPartialNode", NAMESPACE_SVRL_INTERPETER_REPORT).getChildren().get(0)
                            .clone();
                    //Sometimes, svinrep namespace is declared here (it is not clear why). We must remove it.
                    nodeToModify.removeNamespaceDeclaration(NAMESPACE_SVRL_INTERPETER_REPORT);
                    Element discordingElementAtDoc = discordingElementAtDocList.get(i);
                    mixTreesRecursive(discordingElementAtDoc, nodeToModify, null,
                            NAMESPACE_AUTOCONF_METADATA.getURI());
                    Element replaceNodeElement = new Element("replace-node",
                            AutoconfXMLConstants.NAMESPACE_SVINAPPLIER);
                    Attribute pathToElementAttr = new Attribute("atPath",
                            generateAttributeBasedPath(discordingElementAtDoc));
                    Attribute documentToChangeAttr = new Attribute("atResource", docAndPath[0]);
                    replaceNodeElement.setAttribute(documentToChangeAttr);
                    replaceNodeElement.setAttribute(pathToElementAttr);
                    replaceNodeElement.addContent(nodeToModify);
                    resultRoot.addContent(replaceNodeElement);
                }
            }
        }
        return result;
    }

    /**
     * @see com.c4om.autoconf.ulysses.configanalyzer.integrator.configanalyzer.integrator.ChangesetGenerator#generateChangesets(com.c4om.autoconf.ulysses.interfaces.configanalyzer.core.datastructures.ConfigurationAnalysisContext)
     */
    public void generateChangesets(ConfigurationAnalysisContext context) throws ConfigurationIntegrationException {
        generateChangesets(context, null);
    }

    /**
     * @see com.c4om.autoconf.ulysses.configanalyzer.integrator.configanalyzer.integrator.ChangesetGenerator#generateChangesets(com.c4om.autoconf.ulysses.interfaces.configanalyzer.core.datastructures.ConfigurationAnalysisContext, Target)
     */
    public void generateChangesets(ConfigurationAnalysisContext context, Target target)
            throws ConfigurationIntegrationException {
        try {
            for (ObjectiveSolution<?> solution : context.getSolutionsForObjectives()) {
                if (!(solution.getSolutionData() instanceof Document) || solution.isIntegrated()) {
                    continue;
                }
                if (target != null && !solution.getTarget().equals(target)) {
                    continue;
                }
                ExtractedConfiguration<File> sourceExtractedConfiguration = extractSourceConfigurationOfObjectiveSolution(
                        context, solution);
                if (sourceExtractedConfiguration == null) {
                    throw new IllegalArgumentException(
                            "Source configuration for solution " + solution.getId() + " not found");
                }
                File runtimeConfiguration = getRuntimeConfiguration(sourceExtractedConfiguration);
                if (runtimeConfiguration == null) {
                    throw new IllegalArgumentException("Runtime configuration not found");
                }
                Document report = (Document) solution.getSolutionData();
                Document changesetDocument = getSingleChangesetDocument(runtimeConfiguration.getAbsolutePath(),
                        report);
                Changeset<Document> changeset = new XMLDescribedChangeset(solution.getId(), solution.getTarget(),
                        changesetDocument, solution.isRequired());
                context.getChangesets().add(changeset);
                solution.setIntegrated(true);
            }
        } catch (JDOMException | IOException | SaxonApiException | ParserConfigurationException e) {
            throw new ConfigurationIntegrationException(e);
        }

    }

    /**
     * Method called at application start.
     * @param args The first must point to a svrl interpreter report (objective solution). The second one, to a runtime configuration. The third one, to a settings properties file resource.
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        SVINChangesetGenerator applier = new SVINChangesetGenerator(args[2]);
        Document result = applier.getSingleChangesetDocument(args[1], loadJDOMDocumentFromFile(new File(args[0])));
        System.out.println(convertJDOMDocumentToString(result));
    }
}