org.gradle.plugins.ide.internal.configurer.HierarchicalElementDeduplicator Maven / Gradle / Ivy
Show all versions of gradle-api Show documentation
/*
* Copyright 2016 the original author or authors.
*
* 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 org.gradle.plugins.ide.internal.configurer;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* A generic name de-duplicator for hierarchical elements.
*
* Conflicting sub-elements are de-duplicated by prepending their parent element names, separated by a dash.
* Conflicting root elements are rejected with an {@link IllegalArgumentException}
*
* If a child's simple name already contains the name of its parent, the two prefixes are collapsed to keep names short.
* For example, an elements with the name segments root:impl:impl-simple
would initially get the name
* root-impl-impl-simple
and would then be shortened to root-impl-simple
* This shortening is of course only applied if it does not introduce a new name conflict.
*
* @param the type of element to de-duplicate
*/
public class HierarchicalElementDeduplicator {
private final HierarchicalElementAdapter adapter;
public HierarchicalElementDeduplicator(HierarchicalElementAdapter adapter) {
this.adapter = adapter;
}
/**
* Calculates a set of renamings for each duplicate name in the given set of elements.
*
* @param elements the elements with possibly duplicated names
* @return a Map containing the new name for each element that has to be renamed
*/
public Map deduplicate(Iterable extends T> elements) {
return new StatefulDeduplicator(elements).getNewNames();
}
/*
* This inner class hides the fact that the actual de-duplication algorithm is stateful.
*/
private class StatefulDeduplicator {
private final List elements;
private final Multimap elementsByName;
private final Map newNames;
private final Map prefixes;
private StatefulDeduplicator(Iterable extends T> elements) {
this.elements = Lists.newArrayList(elements);
this.elementsByName = LinkedHashMultimap.create();
this.newNames = Maps.newHashMap();
this.prefixes = Maps.newHashMap();
}
private Map getNewNames() {
if (!elements.isEmpty() && newNames.isEmpty()) {
calculateNewNames();
}
return ImmutableMap.copyOf(newNames);
}
private void calculateNewNames() {
sortElementsByDepth();
for (T element : elements) {
elementsByName.put(getOriginalName(element), element);
prefixes.put(element, getParent(element));
}
while (!getDuplicateNames().isEmpty()) {
deduplicate();
}
simplifyNames();
}
private void deduplicate() {
for (String duplicateName : getDuplicateNames()) {
Collection duplicatedElements = elementsByName.get(duplicateName);
Set reservedNames = ImmutableSet.copyOf(elementsByName.keySet());
Set notYetRenamed = getNotYetRenamedElements(duplicatedElements);
boolean deduplicationSuccessful = false;
Set elementsToRename = notYetRenamed.isEmpty() ? ImmutableSet.copyOf(duplicatedElements) : notYetRenamed;
for (T element : elementsToRename) {
boolean elementRenamed = true;
while (elementRenamed && reservedNames.contains(getCurrentlyAssignedName(element))) {
elementRenamed = renameUsingParentPrefix(element);
deduplicationSuccessful |= elementRenamed;
}
}
if (!deduplicationSuccessful) {
throw new IllegalArgumentException("Duplicate root element " + duplicateName);
}
}
}
private boolean renameUsingParentPrefix(T element) {
T prefixElement = prefixes.get(element);
if (prefixElement != null) {
renameTo(element, getOriginalName(prefixElement) + "-" + getCurrentlyAssignedName(element));
prefixes.put(element, getParent(prefixElement));
return true;
}
return false;
}
private void renameTo(T element, String newName) {
elementsByName.remove(getCurrentlyAssignedName(element), element);
elementsByName.put(newName, element);
newNames.put(element, newName);
}
private void simplifyNames() {
Set deduplicatedNames = elementsByName.keySet();
for (T element : elements) {
String simplifiedName = removeDuplicateWordsFromPrefix(getCurrentlyAssignedName(element), getOriginalName(element));
if (!deduplicatedNames.contains(simplifiedName)) {
renameTo(element, simplifiedName);
}
}
}
private String removeDuplicateWordsFromPrefix(String deduplicatedName, String originalName) {
String prefix = deduplicatedName.substring(0, deduplicatedName.lastIndexOf(originalName));
if (prefix.isEmpty()) {
return deduplicatedName;
}
Splitter splitter = Splitter.on('-').omitEmptyStrings();
List prefixParts = Lists.newArrayList(splitter.split(prefix));
List postfixParts = Lists.newArrayList(splitter.split(originalName));
List words = Lists.newArrayList();
if (postfixParts.size() > 1) {
String postfixHead = postfixParts.get(0);
prefixParts.add(postfixHead);
postfixParts.remove(postfixHead);
}
for (String prefixPart : prefixParts) {
if (!prefixPart.equals(Iterables.getLast(words, null))) {
words.add(prefixPart);
}
}
words.addAll(postfixParts);
return Joiner.on('-').join(words);
}
private Set getDuplicateNames() {
Set duplicates = Sets.newLinkedHashSet();
for (Entry> entry : elementsByName.asMap().entrySet()) {
if (entry.getValue().size() > 1) {
duplicates.add(entry.getKey());
}
}
return duplicates;
}
private Set getNotYetRenamedElements(Collection elementsToRename) {
Set notYetRenamed = Sets.newLinkedHashSet();
for (T element : elementsToRename) {
if (!hasBeenRenamed(element)) {
notYetRenamed.add(element);
}
}
return notYetRenamed;
}
private String getOriginalName(T element) {
return adapter.getName(element);
}
private String getCurrentlyAssignedName(T element) {
if (hasBeenRenamed(element)) {
return newNames.get(element);
} else {
return getOriginalName(element);
}
}
private T getParent(T parent) {
return adapter.getParent(parent);
}
private boolean hasBeenRenamed(T element) {
return newNames.containsKey(element);
}
private void sortElementsByDepth() {
Collections.sort(elements, new Comparator() {
@Override
public int compare(T left, T right) {
return Ints.compare(getDepth(left), getDepth(right));
}
private int getDepth(T element) {
int depth = 0;
T parent = element;
while (parent != null) {
depth++;
parent = getParent(parent);
}
return depth;
}
});
}
}
}