org.splevo.diffing.match.HierarchicalStrategyResourceMatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.splevo.diffing.match.HierarchicalStrategyResourceMatcher.java

Source

/*******************************************************************************
 * Copyright (c) 2013
 *
 * 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.diffing.match;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.MatchResource;
import org.eclipse.emf.compare.match.resource.StrategyResourceMatcher;
import org.eclipse.emf.ecore.resource.Resource;
import org.splevo.diffing.util.NormalizationUtil;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

/**
 * Hierarchical resource matcher to initialize the hierarchical name match strategy and the standard
 * RootIDMatchingStrategy as only matching strategies to be applied by a match engine.
 */
public class HierarchicalStrategyResourceMatcher extends StrategyResourceMatcher {

    /** Index to assign the left resources to their file name. */
    private ListMultimap<String, Resource> filenameResourcesIndexLeft = ArrayListMultimap.create();

    /** Index to assign the right resources to their file name. */
    private ListMultimap<String, Resource> filenameResourcesIndexRight = ArrayListMultimap.create();

    /** Patterns to replace with the defined target string in the URIs string representations. */
    private LinkedHashMap<Pattern, String> uriNormalizationPatterns = Maps.newLinkedHashMap();

    /** Patterns to replace with the defined target string in the URIs string representations. */
    private LinkedHashMap<Pattern, String> filenameNormalizationPatterns = Maps.newLinkedHashMap();

    /**
     * Constructor for default matching strategy without any renaming processing.
     */
    public HierarchicalStrategyResourceMatcher() {
    }

    /**
     * Constructor to specify pattern mappings to handle possible renaming.
     *
     * @param uriNormalizationPatterns
     *            A map with entries having a key representing a regular expression to match and a
     *            replacement string to set in case of a match.
     * @param fileNameNormalizationPatterns
     *            A map with entries having a key of a regular expression to match and a replacement
     *            string to set in case of a match.
     *
     */
    public HierarchicalStrategyResourceMatcher(LinkedHashMap<Pattern, String> uriNormalizationPatterns,
            LinkedHashMap<Pattern, String> fileNameNormalizationPatterns) {
        this.uriNormalizationPatterns = uriNormalizationPatterns;
        this.filenameNormalizationPatterns = fileNameNormalizationPatterns;
    }

    /**
     * Create the mappings between two lists of resources. If both lists contain only one resource,
     * they are always matches as assumed this was triggered explicitly.<br>
     * {@inheritDoc}
     */
    @Override
    public Iterable<MatchResource> createMappings(Iterator<? extends Resource> leftResources,
            Iterator<? extends Resource> rightResources, Iterator<? extends Resource> originResources) {

        final List<MatchResource> mappings = new ArrayList<MatchResource>();

        indexResources(leftResources, filenameResourcesIndexLeft, filenameNormalizationPatterns);
        indexResources(rightResources, filenameResourcesIndexRight, null);

        Set<String> allSegments = Sets.union(filenameResourcesIndexLeft.keySet(),
                filenameResourcesIndexRight.keySet());
        List<String> allSegmentsCopy = Lists.newArrayList(allSegments);

        for (String segment : allSegmentsCopy) {

            List<Resource> leftCandidates = Lists.newArrayList(filenameResourcesIndexLeft.get(segment));
            List<Resource> rightCandidates = Lists.newArrayList(filenameResourcesIndexRight.get(segment));

            if (leftCandidates.size() == 1 && rightCandidates.size() == 1) {
                Resource left = leftCandidates.get(0);
                Resource right = rightCandidates.get(0);
                mappings.add(createMatchResource(left, right, null));
                removeFromIndex(filenameResourcesIndexLeft, left);
                removeFromIndex(filenameResourcesIndexRight, right);

            } else if (leftCandidates.size() != 0 && rightCandidates.size() != 0) {
                matchBestMatches(leftCandidates, rightCandidates, mappings);
            }
        }

        Collection<Resource> remainingLeftResources = Sets.newLinkedHashSet(filenameResourcesIndexLeft.values());
        for (Resource left : remainingLeftResources) {
            mappings.add(createMatchResource(left, null, null));
        }
        Collection<Resource> remainingRightResources = Sets.newLinkedHashSet(filenameResourcesIndexRight.values());
        for (Resource right : remainingRightResources) {
            mappings.add(createMatchResource(null, right, null));
        }

        return mappings;
    }

    /**
     * Create matches for the left and right candidates. A match is only created if a pair is the
     * best match for both sides.
     *
     * Internally, indexes are build to identify the total number of matches and the best matches
     * for both candidate lists.
     *
     * @param leftCandidates
     *            The left candidates to search matches for.
     * @param rightCandidates
     *            The right candidates to search matches for.
     * @param mappings
     *            The list of mappings to fill.
     */
    private void matchBestMatches(List<Resource> leftCandidates, List<Resource> rightCandidates,
            List<MatchResource> mappings) {

        // index for a fast lookup of the highest match score for a resource.
        HashMap<Resource, Integer> bestMatchCountIndex = new HashMap<Resource, Integer>();

        // mappings for a resource to it's best matches
        // This is implemented as multimap to support renaming and derived copies.
        // in such a case, an original resource might map two times:
        // To the still existing same class as well as the modified, derived copy
        // see SPLEVO-181 for details {@link https://sdqbuild.ipd.kit.edu/jira/browse/SPLEVO-181}
        LinkedListMultimap<Resource, Resource> bestMatchIndexLeft = LinkedListMultimap.create();
        LinkedListMultimap<Resource, Resource> bestMatchIndexRight = LinkedListMultimap.create();

        for (Resource leftRes : leftCandidates) {
            for (Resource rightRes : rightCandidates) {
                int matchCount = getMatchingSegmentsPathOnly(leftRes, rightRes);

                if (!bestMatchCountIndex.containsKey(leftRes) || bestMatchCountIndex.get(leftRes) < matchCount) {
                    bestMatchCountIndex.put(leftRes, matchCount);
                    bestMatchIndexLeft.removeAll(leftRes);
                    bestMatchIndexLeft.put(leftRes, rightRes);
                } else if (bestMatchCountIndex.get(leftRes) == matchCount) {
                    bestMatchCountIndex.put(leftRes, matchCount);
                    bestMatchIndexLeft.put(leftRes, rightRes);
                }

                if (!bestMatchCountIndex.containsKey(rightRes) || bestMatchCountIndex.get(rightRes) < matchCount) {
                    bestMatchCountIndex.put(rightRes, matchCount);
                    bestMatchIndexRight.removeAll(rightRes);
                    bestMatchIndexRight.put(rightRes, leftRes);
                } else if (bestMatchCountIndex.get(rightRes) == matchCount) {
                    bestMatchCountIndex.put(rightRes, matchCount);
                    bestMatchIndexRight.put(rightRes, leftRes);
                }
            }
        }

        List<MatchResource> bestMatches = createMatchElementsForBestMatches(bestMatchCountIndex,
                bestMatchIndexLeft);
        mappings.addAll(bestMatches);
    }

    /**
     * Create match elements for valid best matching pairs.
     *
     * For the best match of each left resource, create a match element if this match-pair is also
     * best available match for the right resource in the pair.
     *
     * This supports original resources matched to one or more new resources.<br>
     * This is required to support renaming and derived copies as described in the according Jira
     * Issue:<br>
     * SPLEVO-181 for details {@link https://sdqbuild.ipd.kit.edu/jira/browse/SPLEVO-181}
     *
     * TODO: Check if a match should be prevented if it is only 1<br>
     * 1 means only the filename is the same. The resources are expected to be located relative
     * folders and the URI is an absolute uri. On the other hand, the path to the root folder might
     * be different.<br>
     * subfolderleft/resource.xmi vs. differentsubfolder/resource.xmi<br>
     * vs.<br>
     * rootfolderlef/resource.xmi vs rootsfolderright/resource.xmi<br>
     *
     * @param bestMatchCountIndex
     *            The best match qualifiers for each resource (left and right).
     * @param bestMatchIndexLeft
     *            The pairs of best matches for the left resource.
     * @return The valid resource matches identified.
     */
    private List<MatchResource> createMatchElementsForBestMatches(HashMap<Resource, Integer> bestMatchCountIndex,
            LinkedListMultimap<Resource, Resource> bestMatchIndexLeft) {
        List<MatchResource> mappings = Lists.newArrayList();

        for (Resource leftRes : bestMatchIndexLeft.keySet()) {

            List<Resource> rightRessources = bestMatchIndexLeft.get(leftRes);

            for (Resource rightRes : rightRessources) {
                if (bestMatchCountIndex.get(leftRes) == bestMatchCountIndex.get(rightRes)) {
                    mappings.add(createMatchResource(leftRes, rightRes, null));
                    removeFromIndex(filenameResourcesIndexLeft, leftRes);
                    removeFromIndex(filenameResourcesIndexRight, rightRes);
                }
            }
        }

        mappings = filterDuplicateMappings(mappings);

        return mappings;
    }

    /**
     * Filter duplicate matched compilation units and classes due to derived copy matches.
     *
     * Otherwise the EMF Compare engine would register original elements which are intended to be
     * identified as deleted, have matches due to the orginal class that must be present and they
     * matched to.
     *
     * {@inheritDoc}
     */
    private List<MatchResource> filterDuplicateMappings(List<MatchResource> matches) {

        List<MatchResource> filteredMatches = Lists.newLinkedList(matches);

        // index used to identify duplicate original elements (e.g. for DerivedCopy detection)
        Multimap<Resource, MatchResource> rightMatchedIndex = LinkedHashMultimap.create();
        for (MatchResource match : matches) {
            Resource right = match.getRight();
            if (right != null) {
                rightMatchedIndex.get((Resource) right).add(match);
            }
        }

        // For duplicate matches keep only those with the same name
        for (Resource right : rightMatchedIndex.keySet()) {
            if (rightMatchedIndex.get(right).size() > 1) {
                String rightName = right.getURI().lastSegment();

                for (MatchResource match : rightMatchedIndex.get(right)) {
                    Resource left = (Resource) match.getLeft();

                    String leftName = Strings.nullToEmpty(left.getURI().lastSegment());
                    if (leftName.equals(rightName)) {
                        filteredMatches.remove(match);
                    }
                }
            }
        }

        return filteredMatches;

    }

    /**
     * Remove a resource entry from the index for all segments it has been registered for.
     *
     * @param index
     *            The index to clean.
     * @param resource
     *            The resource to remove
     */
    private void removeFromIndex(ListMultimap<String, Resource> index, Resource resource) {
        List<String> keys = Lists.newArrayList(index.keySet());
        for (String key : keys) {
            index.remove(key, resource);
        }
    }

    /**
     * Count the number of matching segments for two resources.
     *
     * <p>
     * The segment comparison starts from the end of the resources' URIs except of the filename.<br>
     * The filename is expected to match for the resources provided to this method (including
     * renaming awareness etc.). The comparison is done beginning from the end, because the sources
     * of the same software product might be stored at different locations on the disk. So the
     * beginning of the URIs / the locations where the implementations are stored, are expected to
     * be different anyway.
     * </p>
     *
     * @param leftResource
     *            The left resource to compare the uri.
     * @param rightResource
     *            The right resource to compare the uri.
     * @return The number of matching segments of the resources.
     */
    private int getMatchingSegmentsPathOnly(Resource leftResource, Resource rightResource) {

        URI leftURI = leftResource.getURI();
        URI rightUri = rightResource.getURI();
        int count = 0;

        String[] segmentsLeft = processRenamingNormalizations(leftURI.segments());
        String[] segmentsRight = rightUri.segments();

        segmentsLeft = removeLast(segmentsLeft);
        segmentsRight = removeLast(segmentsRight);

        int leftLength = segmentsLeft.length;
        int rightLength = segmentsRight.length;

        for (int i = 0; i < leftLength; i++) {

            if (i >= rightLength) {
                break;
            }

            String refString = segmentsLeft[leftLength - 1 - i];
            String compString = segmentsRight[rightLength - 1 - i];
            if (refString.equals(compString)) {
                count++;
            } else {
                break;
            }
        }

        return count;
    }

    /**
     * Remove the last element of an array.
     *
     * In case of an empty array or null provided, also an empty array will be returned.
     *
     * @param array
     *            The array to remove the last element of.
     * @return A copy of the original array without it's last element.
     */
    private String[] removeLast(String[] array) {
        if (array == null || array.length == 0) {
            return new String[] {};
        }
        return Arrays.copyOfRange(array, 0, array.length - 1);
    }

    /**
     * Resources are identified by URIs consisting of segments (directories and files).<br>
     * To apply a java package renaming normalization that also manifests in the source directories,
     * the normalization pattern provided as string containing "." characters must be mapped to the
     * array of URI segment strings.
     *
     * This is done by merging the URI segments with "." as glue character. As a result also the
     * directory segments of the URI representing the absolute path of the source directory are
     * joined with a dot. Later on, when the joined string is split again using "." as split
     * character, the complete string and also the absolute path part is split. As a result, when
     * the absolute path contained a "." character, a split will be performed for that.<br>
     * For example: An eclipse project named "my.first.project" will become a single segment, but
     * split into three segments when the absolute segment string is split again. if this becomes an
     * issue later on, the logic of this method must be adapted. However, any character potentially
     * lead to the same problem as different file systems also allow for different directory names.
     *
     * Note: The last segment representing the filename will be preserved even if it contains a dot
     * e.g. to separate the file extension.
     *
     * @param segmentsLeft
     *            The source array to process.
     * @return The resulting array after processing.
     */
    private String[] processRenamingNormalizations(String[] segmentsLeft) {
        String leftFilename = segmentsLeft[segmentsLeft.length - 1];

        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < segmentsLeft.length - 1; i++) {
            if (i > 0) {
                sb.append('.');
            }
            sb.append(segmentsLeft[i]);
        }
        String leftSegmentsAsString = NormalizationUtil.normalizeNamespace(sb.toString(), uriNormalizationPatterns);
        List<String> split = Lists.newArrayList(Splitter.on('.').split(leftSegmentsAsString));
        split.add(leftFilename);
        segmentsLeft = Iterables.toArray(split, String.class);
        return segmentsLeft;
    }

    /**
     * Index a set of resources according to their last segment.
     *
     * @param resources
     *            The resources to index.
     * @param index
     *            The index to put them in.
     * @param fileNameNormalizationPatterns
     *            The list of patterns to apply during resource indexing. Null or an empty list if
     *            none should be applied.
     */
    private void indexResources(Iterator<? extends Resource> resources, ListMultimap<String, Resource> index,
            Map<Pattern, String> fileNameNormalizationPatterns) {

        while (resources.hasNext()) {
            Resource res = resources.next();
            String filename = res.getURI().lastSegment();
            index.put(filename, res);

            // handle renaming
            if (fileNameNormalizationPatterns == null) {
                continue;
            }
            for (Pattern pattern : fileNameNormalizationPatterns.keySet()) {
                String replace = fileNameNormalizationPatterns.get(pattern);
                String newFilename = pattern.matcher(filename).replaceAll(replace);
                if (!filename.equals(newFilename)) {
                    index.put(newFilename, res);
                }
            }

        }

    }

}