| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * 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 com.intellij.refactoring.move.moveClassesOrPackages; |
| |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.*; |
| import com.intellij.psi.search.searches.ReferencesSearch; |
| import com.intellij.psi.util.*; |
| import com.intellij.refactoring.BaseRefactoringProcessor; |
| import com.intellij.refactoring.PackageWrapper; |
| import com.intellij.refactoring.RefactoringBundle; |
| import com.intellij.refactoring.move.MoveCallback; |
| import com.intellij.refactoring.move.MoveClassesOrPackagesCallback; |
| import com.intellij.refactoring.move.MoveMultipleElementsViewDescriptor; |
| import com.intellij.refactoring.rename.RenameUtil; |
| import com.intellij.refactoring.util.*; |
| import com.intellij.usageView.UsageInfo; |
| import com.intellij.usageView.UsageViewDescriptor; |
| import com.intellij.util.Function; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.Processor; |
| import com.intellij.util.VisibilityUtil; |
| import com.intellij.util.containers.MultiMap; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.util.*; |
| |
| /** |
| * @author yole |
| */ |
| public class MoveClassToInnerProcessor extends BaseRefactoringProcessor { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.move.moveClassesOrPackages.MoveClassToInnerProcessor"); |
| public static final Key<List<NonCodeUsageInfo>> ourNonCodeUsageKey = Key.create("MoveClassToInner.NonCodeUsage"); |
| |
| private PsiClass[] myClassesToMove; |
| private final PsiClass myTargetClass; |
| private PsiPackage[] mySourcePackage; |
| private final PsiPackage myTargetPackage; |
| private String[] mySourceVisibility; |
| private final boolean mySearchInComments; |
| private final boolean mySearchInNonJavaFiles; |
| private NonCodeUsageInfo[] myNonCodeUsages; |
| private final MoveCallback myMoveCallback; |
| |
| public MoveClassToInnerProcessor(Project project, |
| final PsiClass[] classesToMove, |
| @NotNull final PsiClass targetClass, |
| boolean searchInComments, |
| boolean searchInNonJavaFiles, |
| MoveCallback moveCallback) { |
| super(project); |
| setClassesToMove(classesToMove); |
| myTargetClass = targetClass; |
| mySearchInComments = searchInComments; |
| mySearchInNonJavaFiles = searchInNonJavaFiles; |
| myMoveCallback = moveCallback; |
| myTargetPackage = JavaDirectoryService.getInstance().getPackage(myTargetClass.getContainingFile().getContainingDirectory()); |
| } |
| |
| private void setClassesToMove(final PsiClass[] classesToMove) { |
| myClassesToMove = classesToMove; |
| mySourcePackage = new PsiPackage[classesToMove.length]; |
| mySourceVisibility = new String[classesToMove.length]; |
| for (int i = 0; i < classesToMove.length; i++) { |
| PsiClass psiClass = classesToMove[i]; |
| mySourceVisibility[i] = VisibilityUtil.getVisibilityModifier(psiClass.getModifierList()); |
| mySourcePackage[i] = JavaDirectoryService.getInstance().getPackage(psiClass.getContainingFile().getContainingDirectory()); |
| } |
| } |
| |
| @NotNull |
| protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) { |
| return new MoveMultipleElementsViewDescriptor(myClassesToMove, myTargetClass.getQualifiedName()); |
| } |
| |
| @NotNull |
| public UsageInfo[] findUsages() { |
| final List<UsageInfo> usages = new ArrayList<UsageInfo>(); |
| for (PsiClass classToMove : myClassesToMove) { |
| final String newName = myTargetClass.getQualifiedName() + "." + classToMove.getName(); |
| Collections.addAll(usages, MoveClassesOrPackagesUtil.findUsages(classToMove, mySearchInComments, mySearchInNonJavaFiles, newName)); |
| } |
| return usages.toArray(new UsageInfo[usages.size()]); |
| } |
| |
| protected boolean preprocessUsages(final Ref<UsageInfo[]> refUsages) { |
| final UsageInfo[] usages = refUsages.get(); |
| return showConflicts(getConflicts(usages), usages); |
| } |
| |
| protected void refreshElements(final PsiElement[] elements) { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| public void run() { |
| final PsiClass[] classesToMove = new PsiClass[elements.length]; |
| for (int i = 0; i < classesToMove.length; i++) { |
| classesToMove[i] = (PsiClass)elements[i]; |
| } |
| setClassesToMove(classesToMove); |
| } |
| }); |
| } |
| |
| protected void performRefactoring(UsageInfo[] usages) { |
| if (!prepareWritable(usages)) return; |
| |
| MoveClassToInnerHandler[] handlers = MoveClassToInnerHandler.EP_NAME.getExtensions(); |
| |
| ArrayList<UsageInfo> usageList = new ArrayList<UsageInfo>(Arrays.asList(usages)); |
| List<PsiElement> importStatements = new ArrayList<PsiElement>(); |
| for (MoveClassToInnerHandler handler : handlers) { |
| importStatements.addAll(handler.filterImports(usageList, myProject)); |
| } |
| |
| usages = usageList.toArray(new UsageInfo[usageList.size()]); |
| |
| saveNonCodeUsages(usages); |
| final Map<PsiElement, PsiElement> oldToNewElementsMapping = new HashMap<PsiElement, PsiElement>(); |
| try { |
| for (PsiClass classToMove : myClassesToMove) { |
| PsiClass newClass = null; |
| for (MoveClassToInnerHandler handler : handlers) { |
| newClass = handler.moveClass(classToMove, myTargetClass); |
| if (newClass != null) break; |
| } |
| LOG.assertTrue(newClass != null, "There is no appropriate MoveClassToInnerHandler for " + myTargetClass + "; " + classToMove); |
| oldToNewElementsMapping.put(classToMove, newClass); |
| } |
| |
| myNonCodeUsages = CommonMoveUtil.retargetUsages(usages, oldToNewElementsMapping); |
| for (MoveClassToInnerHandler handler : handlers) { |
| handler.retargetNonCodeUsages(oldToNewElementsMapping, myNonCodeUsages); |
| } |
| |
| for (MoveClassToInnerHandler handler : handlers) { |
| handler.retargetClassRefsInMoved(oldToNewElementsMapping); |
| } |
| |
| for (MoveClassToInnerHandler handler : handlers) { |
| handler.removeRedundantImports(myTargetClass.getContainingFile()); |
| } |
| |
| for (PsiClass classToMove : myClassesToMove) { |
| classToMove.delete(); |
| } |
| |
| for (PsiElement element : importStatements) { |
| if (element.isValid()) { |
| element.delete(); |
| } |
| } |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error(e); |
| } |
| } |
| |
| private boolean prepareWritable(final UsageInfo[] usages) { |
| Set<PsiElement> elementsToMakeWritable = new HashSet<PsiElement>(); |
| Collections.addAll(elementsToMakeWritable, myClassesToMove); |
| elementsToMakeWritable.add(myTargetClass); |
| for(UsageInfo usage: usages) { |
| PsiElement element = usage.getElement(); |
| if (element != null) { |
| elementsToMakeWritable.add(element); |
| } |
| } |
| if (!CommonRefactoringUtil.checkReadOnlyStatus(myProject, PsiUtilCore.toPsiElementArray(elementsToMakeWritable))) { |
| return false; |
| } |
| return true; |
| } |
| |
| private void saveNonCodeUsages(final UsageInfo[] usages) { |
| for (PsiClass classToMove : myClassesToMove) { |
| for(UsageInfo usageInfo: usages) { |
| if (usageInfo instanceof NonCodeUsageInfo) { |
| final NonCodeUsageInfo nonCodeUsage = (NonCodeUsageInfo)usageInfo; |
| PsiElement element = nonCodeUsage.getElement(); |
| if (element != null && PsiTreeUtil.isAncestor(classToMove, element, false)) { |
| List<NonCodeUsageInfo> list = element.getCopyableUserData(ourNonCodeUsageKey); |
| if (list == null) { |
| list = new ArrayList<NonCodeUsageInfo>(); |
| element.putCopyableUserData(ourNonCodeUsageKey, list); |
| } |
| list.add(nonCodeUsage); |
| } |
| } |
| } |
| } |
| } |
| |
| protected void performPsiSpoilingRefactoring() { |
| if (myNonCodeUsages != null) { |
| RenameUtil.renameNonCodeUsages(myProject, myNonCodeUsages); |
| } |
| if (myMoveCallback != null) { |
| if (myMoveCallback instanceof MoveClassesOrPackagesCallback) { |
| ((MoveClassesOrPackagesCallback) myMoveCallback).classesMovedToInner(myTargetClass); |
| } |
| myMoveCallback.refactoringCompleted(); |
| } |
| } |
| |
| protected String getCommandName() { |
| return RefactoringBundle.message("move.class.to.inner.command.name", |
| (myClassesToMove.length > 1 ? "classes " : "class ") + StringUtil.join(myClassesToMove, new Function<PsiClass, String>() { |
| public String fun(PsiClass psiClass) { |
| return psiClass.getName(); |
| } |
| }, ", "), |
| myTargetClass.getQualifiedName()); |
| } |
| |
| @NotNull |
| protected Collection<? extends PsiElement> getElementsToWrite(@NotNull final UsageViewDescriptor descriptor) { |
| List<PsiElement> result = new ArrayList<PsiElement>(); |
| result.addAll(super.getElementsToWrite(descriptor)); |
| result.add(myTargetClass); |
| return result; |
| } |
| |
| public MultiMap<PsiElement, String> getConflicts(final UsageInfo[] usages) { |
| MultiMap<PsiElement, String> conflicts = new MultiMap<PsiElement, String>(); |
| |
| for (PsiClass classToMove : myClassesToMove) { |
| final PsiClass innerClass = myTargetClass.findInnerClassByName(classToMove.getName(), false); |
| if (innerClass != null) { |
| conflicts.putValue(innerClass, RefactoringBundle.message("move.to.inner.duplicate.inner.class", |
| CommonRefactoringUtil.htmlEmphasize(myTargetClass.getQualifiedName()), |
| CommonRefactoringUtil.htmlEmphasize(classToMove.getName()))); |
| } |
| } |
| |
| for (int i = 0; i < myClassesToMove.length; i++) { |
| PsiClass classToMove = myClassesToMove[i]; |
| String classToMoveVisibility = VisibilityUtil.getVisibilityModifier(classToMove.getModifierList()); |
| String targetClassVisibility = VisibilityUtil.getVisibilityModifier(myTargetClass.getModifierList()); |
| |
| boolean moveToOtherPackage = !Comparing.equal(mySourcePackage[i], myTargetPackage); |
| if (moveToOtherPackage) { |
| classToMove.accept(new PackageLocalsUsageCollector(myClassesToMove, new PackageWrapper(myTargetPackage), conflicts)); |
| } |
| |
| ConflictsCollector collector = new ConflictsCollector(classToMove, conflicts); |
| if ((moveToOtherPackage && |
| (classToMoveVisibility.equals(PsiModifier.PACKAGE_LOCAL) || targetClassVisibility.equals(PsiModifier.PACKAGE_LOCAL))) || |
| targetClassVisibility.equals(PsiModifier.PRIVATE)) { |
| detectInaccessibleClassUsages(usages, collector, mySourceVisibility[i]); |
| } |
| if (moveToOtherPackage) { |
| detectInaccessibleMemberUsages(collector); |
| } |
| } |
| |
| return conflicts; |
| } |
| |
| private void detectInaccessibleClassUsages(final UsageInfo[] usages, final ConflictsCollector collector, final String visibility) { |
| for(UsageInfo usage: usages) { |
| if (usage instanceof MoveRenameUsageInfo && !(usage instanceof NonCodeUsageInfo)) { |
| PsiElement element = usage.getElement(); |
| if (element == null || PsiTreeUtil.getParentOfType(element, PsiImportStatement.class) != null) continue; |
| if (isInaccessibleFromTarget(element, visibility)) { |
| collector.addConflict(collector.getClassToMove(), element); |
| } |
| } |
| } |
| } |
| |
| private boolean isInaccessibleFromTarget(final PsiElement element, final String visibility) { |
| final PsiPackage elementPackage = JavaDirectoryService.getInstance().getPackage(element.getContainingFile().getContainingDirectory()); |
| return !PsiUtil.isAccessible(myTargetClass, element, null) || |
| (!myTargetClass.isInterface() && visibility.equals(PsiModifier.PACKAGE_LOCAL) && !Comparing.equal(elementPackage, myTargetPackage)); |
| } |
| |
| private void detectInaccessibleMemberUsages(final ConflictsCollector collector) { |
| PsiElement[] members = collectPackageLocalMembers(collector.getClassToMove()); |
| for(PsiElement member: members) { |
| ReferencesSearch.search(member).forEach(new Processor<PsiReference>() { |
| public boolean process(final PsiReference psiReference) { |
| PsiElement element = psiReference.getElement(); |
| for (PsiClass psiClass : myClassesToMove) { |
| if (PsiTreeUtil.isAncestor(psiClass, element, false)) return true; |
| } |
| if (isInaccessibleFromTarget(element, PsiModifier.PACKAGE_LOCAL)) { |
| collector.addConflict(psiReference.resolve(), element); |
| } |
| return true; |
| } |
| }); |
| } |
| } |
| |
| private static PsiElement[] collectPackageLocalMembers(PsiElement classToMove) { |
| return PsiTreeUtil.collectElements(classToMove, new PsiElementFilter() { |
| public boolean isAccepted(final PsiElement element) { |
| if (element instanceof PsiMember) { |
| PsiMember member = (PsiMember) element; |
| if (VisibilityUtil.getVisibilityModifier(member.getModifierList()) == PsiModifier.PACKAGE_LOCAL) { |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| } |
| |
| private static class ConflictsCollector { |
| private final PsiClass myClassToMove; |
| private final MultiMap<PsiElement, String> myConflicts; |
| private final Set<PsiElement> myReportedContainers = new HashSet<PsiElement>(); |
| |
| public ConflictsCollector(PsiClass classToMove, final MultiMap<PsiElement, String> conflicts) { |
| myClassToMove = classToMove; |
| myConflicts = conflicts; |
| } |
| |
| public synchronized void addConflict(final PsiElement targetElement, final PsiElement sourceElement) { |
| PsiElement container = ConflictsUtil.getContainer(sourceElement); |
| if (!myReportedContainers.contains(container)) { |
| myReportedContainers.add(container); |
| String targetDescription = (targetElement == myClassToMove) |
| ? "Class " + CommonRefactoringUtil.htmlEmphasize(myClassToMove.getName()) |
| : StringUtil.capitalize(RefactoringUIUtil.getDescription(targetElement, true)); |
| final String message = RefactoringBundle.message("element.will.no.longer.be.accessible", |
| targetDescription, |
| RefactoringUIUtil.getDescription(container, true)); |
| myConflicts.putValue(targetElement, message); |
| } |
| } |
| |
| public PsiElement getClassToMove() { |
| return myClassToMove; |
| } |
| } |
| } |