/******************************************************************************* * Copyright (c) 2015 Google Inc and others. * 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: * John Glassmyer <jogl@google.com> - import group sorting is broken - https://bugs.eclipse.org/430303 *******************************************************************************/ package org.eclipse.jdt.internal.core.dom.rewrite.imports; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.TreeSet; /** * Keeping existing imports in their existing order, inserts each new import before or after the * import to which it would be adjacent if all (existing and new) imports were totally ordered * together. * <p> * A new import that would sort between two existing imports which are not adjacent in the * existing order will be placed adjacent to the existing import with which it shares a longer * prefix of dot-separated name segments. */ final class OrderPreservingImportAdder implements ImportAdder { static class AdjacentImports { final Collection<ImportName> importsBefore = new ArrayList<ImportName>(); final Collection<ImportName> importsAfter = new ArrayList<ImportName>(); @Override public String toString() { return String.format("(%s, %s)", this.importsBefore.toString(), this.importsAfter.toString()); //$NON-NLS-1$ } } /** * Returns the number of prefixing dot-separated segments shared between the two names. * <p> * For example, {@code countMatchingPrefixSegments("foo.pack1.Class", "foo.pack2.Class")} will * return 1 and {@code countMatchingPrefixSegments("foo.pack1.Class", "com.foo.pack1.Class")} * will return 0. */ private static int countMatchingPrefixSegments(String name1, String name2) { if (name1.isEmpty() || name2.isEmpty()) { return 0; } int matchingSegments = 0; for (int i = 0; i <= name1.length() && i <= name2.length(); i++) { boolean atEndOfName1Segment = i == name1.length() || name1.charAt(i) == '.'; boolean atEndOfName2Segment = i == name2.length() || name2.charAt(i) == '.'; if (atEndOfName1Segment && atEndOfName2Segment) { matchingSegments++; } else if (atEndOfName1Segment || atEndOfName2Segment) { break; } else if (name1.charAt(i) != name2.charAt(i)) { break; } } return matchingSegments; } private final Comparator<ImportName> importComparator; OrderPreservingImportAdder(Comparator<ImportName> importComparator) { this.importComparator = importComparator; } @Override public List<ImportName> addImports(Collection<ImportName> existingImports, Collection<ImportName> importsToAdd) { if (importsToAdd.isEmpty()) { return new ArrayList<ImportName>(existingImports); } List<ImportName> sortedNewImports = new ArrayList<ImportName>(importsToAdd); sortedNewImports.removeAll(new HashSet<ImportName>(existingImports)); Collections.sort(sortedNewImports, this.importComparator); if (existingImports.isEmpty()) { return sortedNewImports; } Map<ImportName, AdjacentImports> adjacentNewImports = determineAdjacentNewImports(new ArrayList<ImportName>(existingImports), sortedNewImports); List<ImportName> importsWithAdditions = new ArrayList<ImportName>(existingImports.size() + sortedNewImports.size()); for (ImportName existingImport : existingImports) { // Remove the adjacent imports so they don't get inserted multiple times in the case // of duplicate imports. AdjacentImports adjacentImports = adjacentNewImports.remove(existingImport); if (adjacentImports != null) { importsWithAdditions.addAll(adjacentImports.importsBefore); } importsWithAdditions.add(existingImport); if (adjacentImports != null) { importsWithAdditions.addAll(adjacentImports.importsAfter); } } return importsWithAdditions; } /** * Determines which new imports to place before and after each existing import. * <p> * Returns a Map where each key is an existing import and each corresponding value is an * AdjacentImports containing those new imports which should be placed before and after that * existing import. Each new import will be placed either before or after exactly one existing * import. * * @param existingImports * Existing imports. * @param sortedNewImports * Imports to be added. Must be in order as if sorted by this.importComparator. */ private Map<ImportName, AdjacentImports> determineAdjacentNewImports( Collection<ImportName> existingImports, Iterable<ImportName> sortedNewImports) { NavigableSet<ImportName> existingImportsTreeSet = new TreeSet<ImportName>(this.importComparator); existingImportsTreeSet.addAll(existingImports); Map<ImportName, AdjacentImports> adjacentNewImports = new HashMap<ImportName, AdjacentImports>(); for (ImportName existingImport : existingImports) { adjacentNewImports.put(existingImport, new AdjacentImports()); } for (ImportName newImport : sortedNewImports) { ImportName precedingExistingImport = existingImportsTreeSet.lower(newImport); ImportName succeedingExistingImport = existingImportsTreeSet.higher(newImport); if (shouldGroupWithSucceeding(newImport, precedingExistingImport, succeedingExistingImport)) { adjacentNewImports.get(succeedingExistingImport).importsBefore.add(newImport); } else { adjacentNewImports.get(precedingExistingImport).importsAfter.add(newImport); } } return adjacentNewImports; } /** * Returns true if the new import should be placed before the existing import that would succeed * it in sorted order, or false if the new import should be placed after the existing import * that would precede it in sorted order. */ private boolean shouldGroupWithSucceeding( ImportName newImport, ImportName precedingExistingImport, ImportName succeedingExistingImport) { if (precedingExistingImport == null) { return true; } else if (succeedingExistingImport == null) { return false; } else { String containerName = newImport.containerName; int prefixSharedWithPreceding = countMatchingPrefixSegments(containerName, precedingExistingImport.containerName); int prefixSharedWithSucceeding = countMatchingPrefixSegments(containerName, succeedingExistingImport.containerName); return prefixSharedWithSucceeding > prefixSharedWithPreceding; } } }