org.splevo.jamopp.diffing.JaMoPPDiffer.java Source code

Java tutorial

Introduction

Here is the source code for org.splevo.jamopp.diffing.JaMoPPDiffer.java

Source

/*******************************************************************************
 * Copyright (c) 2014
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Benjamin Klatt - initial API and implementation and/or initial documentation
 *******************************************************************************/
package org.splevo.jamopp.diffing;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.EMFCompare;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.diff.DefaultDiffEngine;
import org.eclipse.emf.compare.diff.FeatureFilter;
import org.eclipse.emf.compare.diff.IDiffEngine;
import org.eclipse.emf.compare.match.DefaultMatchEngine;
import org.eclipse.emf.compare.match.IMatchEngine;
import org.eclipse.emf.compare.match.impl.MatchEngineFactoryRegistryImpl;
import org.eclipse.emf.compare.match.resource.StrategyResourceMatcher;
import org.eclipse.emf.compare.postprocessor.BasicPostProcessorDescriptorImpl;
import org.eclipse.emf.compare.postprocessor.IPostProcessor;
import org.eclipse.emf.compare.postprocessor.PostProcessorDescriptorRegistryImpl;
import org.eclipse.emf.compare.scope.IComparisonScope;
import org.eclipse.emf.compare.utils.EqualityHelper;
import org.eclipse.emf.compare.utils.IEqualityHelper;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.emftext.commons.layout.LayoutPackage;
import org.emftext.language.java.JavaPackage;
import org.emftext.language.java.commons.Commentable;
import org.splevo.commons.emf.SPLevoResourceSet;
import org.splevo.diffing.Differ;
import org.splevo.diffing.DiffingException;
import org.splevo.diffing.DiffingNotSupportedException;
import org.splevo.diffing.match.HierarchicalMatchEngine.EqualityStrategy;
import org.splevo.diffing.match.HierarchicalMatchEngine.IgnoreStrategy;
import org.splevo.diffing.match.HierarchicalMatchEngineFactory;
import org.splevo.diffing.match.HierarchicalStrategyResourceMatcher;
import org.splevo.diffing.util.NormalizationUtil;
import org.splevo.extraction.SoftwareModelExtractionException;
import org.splevo.jamopp.diffing.diff.JaMoPPDiffBuilder;
import org.splevo.jamopp.diffing.diff.JaMoPPFeatureFilter;
import org.splevo.jamopp.diffing.match.JaMoPPEqualityHelper;
import org.splevo.jamopp.diffing.match.JaMoPPEqualityStrategy;
import org.splevo.jamopp.diffing.match.JaMoPPIgnoreStrategy;
import org.splevo.jamopp.diffing.postprocessor.JaMoPPPostProcessor;
import org.splevo.jamopp.diffing.scope.JavaModelMatchScope;
import org.splevo.jamopp.diffing.scope.PackageIgnoreChecker;
import org.splevo.jamopp.diffing.similarity.SimilarityChecker;
import org.splevo.jamopp.extraction.JaMoPPSoftwareModelExtractor;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Differ for JaMoPP software models.
 *
 * <p>
 * <strong>Ignored files</strong><br>
 * By default, the differ ignores all package-info.java.xmi model files.<br>
 * The option {@link JaMoPPDiffer#OPTION_JAMOPP_IGNORE_FILES} can be provided as diffing option to
 * the doDiff() method to change this default behavior.
 * </p>
 */
public class JaMoPPDiffer implements Differ {

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /** Option key for java packages to ignore. */
    public static final String OPTION_JAMOPP_IGNORE_FILES = "JaMoPP.Files.to.ignore";

    /**
     * Option key for java package patterns to ignore.<br>
     * The purpose of this option is to exclude source code packages from being considered during
     * the diff analysis.
     *
     */
    public static final String OPTION_JAVA_IGNORE_PACKAGES = "JaMoPP.Java.Packages.to.ignore";

    /**
     * Option key for package normalization.<br>
     * The purpose of this option is to normalize the package of the integrated variant.<br>
     * <p>
     * Example:<br>
     * If the original code contains a package named:<br>
     * <tt>org.example.client</tt><br>
     * and the customized code contains a package named:<br>
     * <tt>org.example.customer.client</tt><br>
     * the mapping can be used to normalize the latter package to the former.
     * </p>
     *
     * <p>
     * A rule is specified per line.<br>
     * Each line contains a pair of search and replace separated with a pipe ("|").<br>
     * For example:<br>
     * org.example.customer.client|org.example.client<br>
     * org.example.customer.server|org.example.server
     * </p>
     */
    public static final String OPTION_JAVA_PACKAGE_NORMALIZATION = "JaMoPP.Java.Package.Normalization.Pattern";

    /**
     * Option key for classifier normalization.<br>
     * The purpose of this option is to normalize the classifier of the integration variant.<br>
     * <p>
     * Example:<br>
     * If the original code contains a classifier named:<br>
     * <tt>MyClass</tt><br>
     * and the customized code contains a classifier named:<br>
     * <tt>MyClassCust</tt><br>
     * the normalization pattern can be used to remove the suffix from the classifier.
     * </p>
     *
     * <p>
     * One rule is specified per line.<br>
     * Each line specifies a prefix or suffix to replace.<br>
     * The arbitrary part of the classifiers name to keep is identified with an astrix '*'.
     * </p>
     * <p>
     * <b>Examples:</b><br>
     * To remove the suffix "Custom" from the name "MyClassCustom" the pattern must be specified as
     * "*Custom".<br>
     * To remove the prefix "My" from the name "MyBaseClass" the pattern must be specified as "My*".
     * </p>
     */
    public static final String OPTION_JAVA_CLASSIFIER_NORMALIZATION = "JaMoPP.Java.Classifier.Normalization.Pattern";

    private static final String LABEL = "JaMoPP Java Differ";
    private static final String ID = "org.splevo.jamopp.differ";
    private static Logger logger = Logger.getLogger(JaMoPPDiffer.class);
    private final JaMoPPSoftwareModelExtractor extractor;

    /**
     * Constructs a new JaMoPPDiffer.
     */
    public JaMoPPDiffer() {
        this(new JaMoPPSoftwareModelExtractor());
    }

    /**
     * Constructs a new JaMoPPDiffer with the given software model extractor.
     * 
     * @param extractor
     *            The extractor to be used during the diffing.
     */
    public JaMoPPDiffer(JaMoPPSoftwareModelExtractor extractor) {
        this.extractor = extractor;
    }

    /**
     * Load the source models from the according directories and perform the difference analysis of
     * the loaded {@link ResourceSet}s. <br>
     * {@inheritDoc}
     *
     * @return null if no supported source models available.
     * @throws DiffingNotSupportedException
     *             Thrown if a diffing cannot be done for the provided models.
     */
    @Override
    public Comparison doDiff(java.net.URI leadingModelDirectory, java.net.URI integrationModelDirectory,
            Map<String, String> diffingOptions) throws DiffingException, DiffingNotSupportedException {

        final List<String> ignoreFiles = loadIgnoreFileConfiguration(diffingOptions);

        logger.info("Load source models");
        ResourceSet resourceSetLeading = loadResourceSetRecursively(leadingModelDirectory, ignoreFiles);
        ResourceSet resourceSetIntegration = loadResourceSetRecursively(integrationModelDirectory, ignoreFiles);

        return doDiff(resourceSetLeading, resourceSetIntegration, diffingOptions);
    }

    /**
     * Get the ignore file configuration for the provided options.
     *
     * @param diffingOptions
     *            The options map.
     * @return The list of file names to ignore.
     */
    private List<String> loadIgnoreFileConfiguration(Map<String, String> diffingOptions) {
        final List<String> ignoreFiles;
        if (diffingOptions.containsKey(OPTION_JAMOPP_IGNORE_FILES)) {
            final String diffingRuleRaw = diffingOptions.get(OPTION_JAMOPP_IGNORE_FILES);
            final String[] parts = diffingRuleRaw.split(LINE_SEPARATOR);
            ignoreFiles = Lists.newArrayList();
            for (final String rule : parts) {
                ignoreFiles.add(rule);
            }
        } else {
            ignoreFiles = Lists.asList("package-info.java", new String[] {});
        }
        return ignoreFiles;
    }

    /**
     * Diffing the models contained in the provided resource sets.<br>
     *
     * {@inheritDoc}
     *
     * @return null if no supported source models available.
     * @throws DiffingNotSupportedException
     *             Thrown if no reasonable JaMoPP model is contained in the resource sets.
     */
    @Override
    public Comparison doDiff(ResourceSet resourceSetLeading, ResourceSet resourceSetIntegration,
            Map<String, String> diffingOptions) throws DiffingException, DiffingNotSupportedException {

        List<String> ignorePackages = buildIgnorePackageList(diffingOptions);
        PackageIgnoreChecker packageIgnoreChecker = new PackageIgnoreChecker(ignorePackages);

        EMFCompare comparator = initCompare(packageIgnoreChecker, diffingOptions);

        // Compare the two models
        // In comparison, the left side is always the changed one.
        // push in the integration model first
        IComparisonScope scope = new JavaModelMatchScope(resourceSetIntegration, resourceSetLeading,
                packageIgnoreChecker);

        Comparison comparisonModel = comparator.compare(scope);

        return comparisonModel;

    }

    /**
     * Diffing JaMoPP elements directly.<br>
     *
     * Note: This method does not take any differ specific configurations (e.g. ignore packages)
     * into account.
     *
     * {@inheritDoc}
     *
     * @return null if no supported source models available.
     */
    public Comparison doDiff(Commentable rightElement, Commentable leftElement,
            Map<String, String> diffingOptions) {

        List<String> ignorePackages = Lists.newArrayList();
        PackageIgnoreChecker packageIgnoreChecker = new PackageIgnoreChecker(ignorePackages);

        EMFCompare comparator = initCompare(packageIgnoreChecker, diffingOptions);

        // Compare the two models
        // In comparison, the left side is always the changed one.
        // push in the integration model first
        IComparisonScope scope = new JavaModelMatchScope(leftElement, rightElement, packageIgnoreChecker);

        Comparison comparisonModel = comparator.compare(scope);

        return comparisonModel;

    }

    /**
     * Build the list of package ignore patterns from the provided diffing options.
     *
     * @param diffingOptions
     *            Diffing options to process.
     * @return The list of patterns, maybe empty but never null.
     */
    private List<String> buildIgnorePackageList(Map<String, String> diffingOptions) {
        String diffingRuleRaw = diffingOptions.get(OPTION_JAVA_IGNORE_PACKAGES);
        List<String> ignorePackages = Lists.newArrayList();
        if (diffingRuleRaw != null) {
            final String[] parts = diffingRuleRaw.split(LINE_SEPARATOR);
            for (final String rule : parts) {
                ignorePackages.add(rule);
            }
        }
        return ignorePackages;
    }

    /**
     * Initialize the compare engine.
     *
     * @param packageIgnoreChecker
     *            The checker to decide if an element is within a package to ignore.
     * @param diffingOptions
     *            The options configuring the comparison.
     * @return The prepared emf compare engine.
     */
    private EMFCompare initCompare(PackageIgnoreChecker packageIgnoreChecker, Map<String, String> diffingOptions) {

        IMatchEngine.Factory.Registry matchEngineRegistry = initMatchEngine(packageIgnoreChecker, diffingOptions);
        IPostProcessor.Descriptor.Registry<?> postProcessorRegistry = initPostProcessors(packageIgnoreChecker,
                diffingOptions);
        IDiffEngine diffEngine = initDiffEngine(packageIgnoreChecker);
        EMFCompare comparator = initComparator(matchEngineRegistry, postProcessorRegistry, diffEngine);
        return comparator;
    }

    /**
     * Initialize a cache to be used by the equality helper.
     *
     * @return The ready to use cache.
     */
    private LoadingCache<EObject, URI> initEqualityCache() {
        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder()
                .maximumSize(DefaultMatchEngine.DEFAULT_EOBJECT_URI_CACHE_MAX_SIZE);
        final LoadingCache<EObject, URI> cache = EqualityHelper.createDefaultCache(cacheBuilder);
        return cache;
    }

    /**
     * Initialize the post processors and build an according registry.
     *
     * @param packageIgnoreChecker
     *            The checker if an element belongs to an ignored package.
     * @param diffingOptions
     *            The options to configure the post processor.
     * @return The prepared registry with references to the post processors.
     */
    private IPostProcessor.Descriptor.Registry<String> initPostProcessors(PackageIgnoreChecker packageIgnoreChecker,
            Map<String, String> diffingOptions) {
        IPostProcessor customPostProcessor = new JaMoPPPostProcessor(diffingOptions);
        Pattern any = Pattern.compile(".*");
        IPostProcessor.Descriptor descriptor = new BasicPostProcessorDescriptorImpl(customPostProcessor, any, any);
        IPostProcessor.Descriptor.Registry<String> postProcessorRegistry = new PostProcessorDescriptorRegistryImpl<String>();
        postProcessorRegistry.put(JaMoPPPostProcessor.class.getName(), descriptor);
        return postProcessorRegistry;
    }

    /**
     * Init the comparator instance to be used for comparison.
     *
     * @param matchEngineRegistry
     *            The registry containing the match engines to be used.
     * @param postProcessorRegistry
     *            Registry for post processors to be executed.
     * @param diffEngine
     *            The diff engine to run.
     * @return The prepared comparator instance.
     */
    private EMFCompare initComparator(IMatchEngine.Factory.Registry matchEngineRegistry,
            IPostProcessor.Descriptor.Registry<?> postProcessorRegistry, IDiffEngine diffEngine) {
        EMFCompare.Builder builder = EMFCompare.builder();
        builder.setDiffEngine(diffEngine);
        builder.setMatchEngineFactoryRegistry(matchEngineRegistry);
        builder.setPostProcessorRegistry(postProcessorRegistry);
        EMFCompare comparator = builder.build();
        return comparator;
    }

    /**
     * Initialize the diff engine with the diff processor and feature filters to be used.
     *
     * @param packageIgnoreChecker
     *            Checker to decide if an element is in a package to ignore.
     * @return The ready-to-use diff engine.
     */
    private IDiffEngine initDiffEngine(final PackageIgnoreChecker packageIgnoreChecker) {
        final JaMoPPDiffBuilder diffProcessor = new JaMoPPDiffBuilder(packageIgnoreChecker);
        IDiffEngine diffEngine = new DefaultDiffEngine(diffProcessor) {
            @Override
            protected FeatureFilter createFeatureFilter() {
                return new JaMoPPFeatureFilter(packageIgnoreChecker);
            }

            @Override
            public void diff(Comparison comparison, Monitor monitor) {
                for (Match rootMatch : comparison.getMatches()) {
                    if (isSingleSideRootMatch(rootMatch)) {
                        diffProcessor.createRootDiff(rootMatch);
                    } else {
                        checkForDifferences(rootMatch, monitor);
                    }
                }
            }

            private boolean isSingleSideRootMatch(Match rootMatch) {
                return rootMatch.getSubmatches().size() == 0
                        && (rootMatch.getLeft() == null || rootMatch.getRight() == null);
            }
        };
        return diffEngine;
    }

    /**
     * Initialize and configure the match engines to be used.
     *
     * @param packageIgnoreChecker
     *            The package ignore checker to use in the match engine.
     * @param diffingOptions
     *            The options configuring the comparison.
     *
     * @return The registry containing all prepared match engines
     */
    private IMatchEngine.Factory.Registry initMatchEngine(PackageIgnoreChecker packageIgnoreChecker,
            Map<String, String> diffingOptions) {

        SimilarityChecker similarityChecker = initSimilarityChecker(diffingOptions);
        IEqualityHelper equalityHelper = initEqualityHelper(similarityChecker);
        EqualityStrategy equalityStrategy = new JaMoPPEqualityStrategy(similarityChecker);
        IgnoreStrategy ignoreStrategy = new JaMoPPIgnoreStrategy(packageIgnoreChecker);
        StrategyResourceMatcher resourceMatcher = initResourceMatcher(diffingOptions);

        IMatchEngine.Factory matchEngineFactory = new HierarchicalMatchEngineFactory(equalityHelper,
                equalityStrategy, ignoreStrategy, resourceMatcher);
        matchEngineFactory.setRanking(20);

        IMatchEngine.Factory.Registry matchEngineRegistry = new MatchEngineFactoryRegistryImpl();
        matchEngineRegistry.add(matchEngineFactory);

        return matchEngineRegistry;
    }

    /**
     * Initialize the similarity checker with the according configurations.
     *
     * @param diffingOptions
     *            The map of configurations.
     * @return The prepared checker.
     */
    private SimilarityChecker initSimilarityChecker(Map<String, String> diffingOptions) {
        String configString = diffingOptions.get(OPTION_JAVA_CLASSIFIER_NORMALIZATION);
        LinkedHashMap<Pattern, String> classifierNorms = NormalizationUtil.loadRemoveNormalizations(configString,
                null);
        LinkedHashMap<Pattern, String> compUnitNorms = NormalizationUtil.loadRemoveNormalizations(configString,
                ".java");

        String configStringPackage = diffingOptions.get(OPTION_JAVA_PACKAGE_NORMALIZATION);
        LinkedHashMap<Pattern, String> packageNorms = NormalizationUtil
                .loadReplaceNormalizations(configStringPackage);

        return new SimilarityChecker(classifierNorms, compUnitNorms, packageNorms);
    }

    /**
     * Init the equality helper to decide about element similarity.
     *
     * @param similarityChecker
     *            The similarity checker to use.
     * @return The prepared equality helper.
     */
    private IEqualityHelper initEqualityHelper(SimilarityChecker similarityChecker) {
        final LoadingCache<EObject, org.eclipse.emf.common.util.URI> cache = initEqualityCache();
        IEqualityHelper equalityHelper = new JaMoPPEqualityHelper(cache, similarityChecker);
        return equalityHelper;
    }

    /**
     * Initialize the resource matcher to be used by the MatchEngine.
     *
     * @param diffingOptions
     *            The configuration map to init based on.
     * @return The prepared resource matcher.
     */
    private HierarchicalStrategyResourceMatcher initResourceMatcher(Map<String, String> diffingOptions) {

        String packageNormConfig = diffingOptions.get(OPTION_JAVA_PACKAGE_NORMALIZATION);
        LinkedHashMap<Pattern, String> uriNormalizations = NormalizationUtil
                .loadReplaceNormalizations(packageNormConfig);

        String classNormConfig = diffingOptions.get(OPTION_JAVA_CLASSIFIER_NORMALIZATION);
        LinkedHashMap<Pattern, String> fileNormalizations = NormalizationUtil
                .loadRemoveNormalizations(classNormConfig, ".java");

        HierarchicalStrategyResourceMatcher resourceMatcher = new HierarchicalStrategyResourceMatcher(
                uriNormalizations, fileNormalizations);

        return resourceMatcher;
    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getLabel() {
        return LABEL;
    }

    @Override
    public void init() {
        JavaPackage.eINSTANCE.eClass();
        LayoutPackage.eINSTANCE.eClass();
    }

    /**
     * Recursively load all files within a directory into a resource set.
     *
     * @param baseDirectory
     *            The root directory to search files in.
     * @param ignoreFiles
     *            A list of filenames to ignore during load.
     * @return The prepared resource set.
     */
    private ResourceSet loadResourceSetRecursively(java.net.URI baseDirectory, List<String> ignoreFiles) {

        List<String> projectPaths = new ArrayList<String>();
        projectPaths.add((new File(baseDirectory).getAbsolutePath()));
        try {
            return extractor.extractSoftwareModel(projectPaths, new NullProgressMonitor());
        } catch (SoftwareModelExtractionException e) {
            logger.error("Failed to load resource set", e);
        }

        return new SPLevoResourceSet();
    }

    @Override
    public Map<String, String> getAvailableConfigurations() {
        Map<String, String> options = Maps.newHashMap();
        options.put(OPTION_JAVA_IGNORE_PACKAGES, "java.*\njavax.*");
        options.put(OPTION_JAMOPP_IGNORE_FILES, "package-info.java");
        options.put(OPTION_JAVA_CLASSIFIER_NORMALIZATION, "");
        options.put(OPTION_JAVA_PACKAGE_NORMALIZATION, "");
        options.put(JaMoPPPostProcessor.OPTION_DIFF_CLEANUP_DERIVED_COPIES, "");
        options.put(JaMoPPPostProcessor.OPTION_DIFF_STATISTICS_LOG_DIR, "");
        return options;
    }

    @Override
    public int getOrderId() {
        return 0;
    }

    @Override
    public Set<String> getRequiredExtractorIds() {
        LinkedHashSet<String> ids = new LinkedHashSet<String>();
        ids.add(JaMoPPSoftwareModelExtractor.EXTRACTOR_ID);
        return ids;
    }
}