/*******************************************************************************
* 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Reassigns comments associated with removed imports (those present before but not present
* after the rewrite) to resultant imports (those present after the rewrite).
* <p>
* Reassigns comments of removed single imports to the first (in iteration order) resultant
* on-demand import having the same container name, if one exists.
* <p>
* Reassigns comments of removed on-demand imports to the first (in iteration order) resultant
* single import having the same container name, if one exists.
* <p>
* Leaves unassigned any removed import comment not matching the above cases.
*/
final class RemovedImportCommentReassigner {
private static Collection<OriginalImportEntry> retainImportsWithComments(Collection<OriginalImportEntry> imports) {
Collection<OriginalImportEntry> importsWithComments = new ArrayList<OriginalImportEntry>(imports.size());
for (OriginalImportEntry currentImport : imports) {
if (!currentImport.comments.isEmpty()) {
importsWithComments.add(currentImport);
}
}
return importsWithComments;
}
private static boolean hasFloatingComment(OriginalImportEntry nextAssignedImport) {
for (ImportComment importComment : nextAssignedImport.comments) {
if (importComment.succeedingLineDelimiters > 1) {
return true;
}
}
return false;
}
private final Collection<OriginalImportEntry> originalImportsWithComments;
RemovedImportCommentReassigner(List<OriginalImportEntry> originalImports) {
this.originalImportsWithComments = retainImportsWithComments(originalImports);
}
/**
* Assigns comments of removed import entries (those in {@code originalImports} but not in
* {@code resultantImports}) to resultant import entries.
* <p>
* Returns a map containing the resulting assignments, where each key is an element of
* {@code resultantImports} and each value is a collection of comments reassigned to that
* resultant import.
*/
Map<ImportEntry, Collection<ImportComment>> reassignComments(Collection<ImportEntry> resultantImports) {
Map<ImportEntry, Collection<OriginalImportEntry>> importAssignments = assignRemovedImports(resultantImports);
Map<ImportEntry, Collection<ImportComment>> commentAssignments =
new HashMap<ImportEntry, Collection<ImportComment>>();
for (Map.Entry<ImportEntry, Collection<OriginalImportEntry>> importAssignment : importAssignments.entrySet()) {
ImportEntry targetImport = importAssignment.getKey();
if (targetImport != null) {
Deque<ImportComment> assignedComments = new ArrayDeque<ImportComment>();
Collection<OriginalImportEntry> assignedImports = importAssignment.getValue();
Iterator<OriginalImportEntry> nextAssignedImportIterator = assignedImports.iterator();
if (nextAssignedImportIterator.hasNext()) {
nextAssignedImportIterator.next();
}
Iterator<OriginalImportEntry> assignedImportIterator = assignedImports.iterator();
while (assignedImportIterator.hasNext()) {
OriginalImportEntry currentAssignedImport = assignedImportIterator.next();
OriginalImportEntry nextAssignedImport =
nextAssignedImportIterator.hasNext() ? nextAssignedImportIterator.next() : null;
assignedComments.addAll(currentAssignedImport.comments);
if (nextAssignedImport != null && hasFloatingComment(nextAssignedImport)) {
// Ensure that a blank line separates this removed import's comments
// from the next removed import's floating comments.
ImportComment lastComment = assignedComments.removeLast();
ImportComment lastCommentWithTrailingBlankLine = new ImportComment(lastComment.region, 2);
assignedComments.add(lastCommentWithTrailingBlankLine);
}
}
commentAssignments.put(targetImport, assignedComments);
}
}
return commentAssignments;
}
private Map<ImportEntry, Collection<OriginalImportEntry>> assignRemovedImports(Collection<ImportEntry> imports) {
Collection<OriginalImportEntry> removedImportsWithComments = identifyRemovedImportsWithComments(imports);
if (removedImportsWithComments.isEmpty()) {
return Collections.emptyMap();
}
Map<ImportName, ImportEntry> firstSingleForOnDemand = identifyFirstSingleForEachOnDemand(imports);
Map<ImportName, ImportEntry> firstOccurrences = identifyFirstOccurrenceOfEachImportName(imports);
Map<ImportEntry, Collection<OriginalImportEntry>> removedImportsForRetainedImport =
new HashMap<ImportEntry, Collection<OriginalImportEntry>>();
for (ImportEntry retainedImport : imports) {
removedImportsForRetainedImport.put(retainedImport, new ArrayList<OriginalImportEntry>());
}
// The null key will map to the removed imports not assigned to any import.
removedImportsForRetainedImport.put(null, new ArrayList<OriginalImportEntry>());
for (OriginalImportEntry removedImport : removedImportsWithComments) {
ImportName removedImportName = removedImport.importName;
final ImportEntry retainedImport;
if (removedImportName.isOnDemand()) {
retainedImport = firstSingleForOnDemand.get(removedImportName);
} else {
retainedImport = firstOccurrences.get(removedImportName.getContainerOnDemand());
}
// retainedImport will be null if there's no corresponding import to which to assign the removed import.
removedImportsForRetainedImport.get(retainedImport).add(removedImport);
}
return removedImportsForRetainedImport;
}
private Collection<OriginalImportEntry> identifyRemovedImportsWithComments(Collection<ImportEntry> imports) {
Collection<OriginalImportEntry> removedImports =
new ArrayList<OriginalImportEntry>(this.originalImportsWithComments);
removedImports.removeAll(imports);
return removedImports;
}
/**
* Assigns each removed on-demand import to the first single import in {@code imports} having
* the same container name.
* <p>
* Returns a map where each key is a single import and each value is the corresponding
* removed on-demand import.
* <p>
* The returned map only contains mappings to removed on-demand imports for which there are
* corresponding single imports in {@code imports}.
*/
private Map<ImportName, ImportEntry> identifyFirstSingleForEachOnDemand(Iterable<ImportEntry> imports) {
Map<ImportName, ImportEntry> firstSingleImportForContainer = new HashMap<ImportName, ImportEntry>();
for (ImportEntry currentImport : imports) {
if (!currentImport.importName.isOnDemand()) {
ImportName containerOnDemand = currentImport.importName.getContainerOnDemand();
if (!firstSingleImportForContainer.containsKey(containerOnDemand)) {
firstSingleImportForContainer.put(containerOnDemand, currentImport);
}
}
}
return firstSingleImportForContainer;
}
private Map<ImportName, ImportEntry> identifyFirstOccurrenceOfEachImportName(Iterable<ImportEntry> imports) {
Map<ImportName, ImportEntry> firstOccurrenceOfImport = new HashMap<ImportName, ImportEntry>();
for (ImportEntry resultantImport : imports) {
if (!firstOccurrenceOfImport.containsKey(resultantImport.importName)) {
firstOccurrenceOfImport.put(resultantImport.importName, resultantImport);
}
}
return firstOccurrenceOfImport;
}
}