/* * Copyright 2000-2014 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; import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; import com.intellij.history.LocalHistory; import com.intellij.history.LocalHistoryAction; import com.intellij.ide.DataManager; import com.intellij.lang.Language; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.command.undo.BasicUndoableAction; import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.command.undo.UndoableAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.EmptyRunnable; import com.intellij.openapi.util.Factory; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.impl.status.StatusBarUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiUtilCore; import com.intellij.refactoring.listeners.RefactoringEventData; import com.intellij.refactoring.listeners.RefactoringEventListener; import com.intellij.refactoring.listeners.RefactoringListenerManager; import com.intellij.refactoring.listeners.impl.RefactoringListenerManagerImpl; import com.intellij.refactoring.listeners.impl.RefactoringTransaction; import com.intellij.refactoring.ui.ConflictsDialog; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.refactoring.util.MoveRenameUsageInfo; import com.intellij.ui.GuiUtils; import com.intellij.usageView.UsageInfo; import com.intellij.usageView.UsageViewDescriptor; import com.intellij.usageView.UsageViewUtil; import com.intellij.usages.*; import com.intellij.usages.rules.PsiElementUsage; import com.intellij.util.Processor; import com.intellij.util.containers.HashSet; import com.intellij.util.containers.MultiMap; import com.intellij.util.ui.UIUtil; import gnu.trove.THashSet; import gnu.trove.TObjectHashingStrategy; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.InvocationTargetException; import java.util.*; public abstract class BaseRefactoringProcessor implements Runnable { private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.BaseRefactoringProcessor"); @NotNull protected final Project myProject; private RefactoringTransaction myTransaction; private boolean myIsPreviewUsages; protected Runnable myPrepareSuccessfulSwingThreadCallback = EmptyRunnable.INSTANCE; protected BaseRefactoringProcessor(@NotNull Project project) { this(project, null); } protected BaseRefactoringProcessor(@NotNull Project project, @Nullable Runnable prepareSuccessfulCallback) { myProject = project; myPrepareSuccessfulSwingThreadCallback = prepareSuccessfulCallback; } @NotNull protected abstract UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages); /** * Is called inside atomic action. */ @NotNull protected abstract UsageInfo[] findUsages(); /** * is called when usage search is re-run. * * @param elements - refreshed elements that are returned by UsageViewDescriptor.getElements() */ protected void refreshElements(PsiElement[] elements) {} /** * Is called inside atomic action. * * @param refUsages usages to be filtered * @return true if preprocessed successfully */ protected boolean preprocessUsages(Ref refUsages) { prepareSuccessful(); return true; } /** * Is called inside atomic action. */ protected boolean isPreviewUsages(UsageInfo[] usages) { return myIsPreviewUsages; } protected boolean isPreviewUsages() { return myIsPreviewUsages; } public void setPreviewUsages(boolean isPreviewUsages) { myIsPreviewUsages = isPreviewUsages; } public void setPrepareSuccessfulSwingThreadCallback(Runnable prepareSuccessfulSwingThreadCallback) { myPrepareSuccessfulSwingThreadCallback = prepareSuccessfulSwingThreadCallback; } protected RefactoringTransaction getTransaction() { return myTransaction; } /** * Is called in a command and inside atomic action. */ protected abstract void performRefactoring(UsageInfo[] usages); protected abstract String getCommandName(); protected void doRun() { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); final Ref refUsages = new Ref(); final Ref refErrorLanguage = new Ref(); final Ref refProcessCanceled = new Ref(); final Ref dumbModeOccurred = new Ref(); final Ref anyException = new Ref(); final Runnable findUsagesRunnable = new Runnable() { @Override public void run() { try { refUsages.set(ApplicationManager.getApplication().runReadAction(new Computable() { @Override public UsageInfo[] compute() { return findUsages(); } })); } catch (UnknownReferenceTypeException e) { refErrorLanguage.set(e.getElementLanguage()); } catch (ProcessCanceledException e) { refProcessCanceled.set(Boolean.TRUE); } catch (IndexNotReadyException e) { dumbModeOccurred.set(Boolean.TRUE); } catch (Throwable e) { anyException.set(Boolean.TRUE); LOG.error(e); } } }; if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(findUsagesRunnable, RefactoringBundle.message("progress.text"), true, myProject)) { return; } if (!refErrorLanguage.isNull()) { Messages.showErrorDialog(myProject, RefactoringBundle.message("unsupported.refs.found", refErrorLanguage.get().getDisplayName()), RefactoringBundle.message("error.title")); return; } if (!dumbModeOccurred.isNull()) { DumbService.getInstance(myProject).showDumbModeNotification("Usage search is not available until indices are ready"); return; } if (!refProcessCanceled.isNull()) { Messages.showErrorDialog(myProject, "Index corruption detected. Please retry the refactoring - indexes will be rebuilt automatically", RefactoringBundle.message("error.title")); return; } if (!anyException.isNull()) { //do not proceed if find usages fails return; } assert !refUsages.isNull(): "Null usages from processor " + this; if (!preprocessUsages(refUsages)) return; final UsageInfo[] usages = refUsages.get(); assert usages != null; UsageViewDescriptor descriptor = createUsageViewDescriptor(usages); boolean isPreview = isPreviewUsages(usages); if (!isPreview) { isPreview = !ensureElementsWritable(usages, descriptor) || UsageViewUtil.hasReadOnlyUsages(usages); if (isPreview) { StatusBarUtil.setStatusBarInfo(myProject, RefactoringBundle.message("readonly.occurences.found")); } } if (isPreview) { previewRefactoring(usages); } else { execute(usages); } } protected void previewRefactoring(final UsageInfo[] usages) { if (ApplicationManager.getApplication().isUnitTestMode()) { execute(usages); return; } final UsageViewDescriptor viewDescriptor = createUsageViewDescriptor(usages); final PsiElement[] elements = viewDescriptor.getElements(); final PsiElement2UsageTargetAdapter[] targets = PsiElement2UsageTargetAdapter.convert(elements); Factory factory = new Factory() { @Override public UsageSearcher create() { return new UsageInfoSearcherAdapter() { @Override public void generate(@NotNull final Processor processor) { ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { for (int i = 0; i < elements.length; i++) { elements[i] = targets[i].getElement(); } refreshElements(elements); } }); processUsages(processor, myProject); } @Override protected UsageInfo[] findUsages() { return BaseRefactoringProcessor.this.findUsages(); } }; } }; showUsageView(viewDescriptor, factory, usages); } protected boolean skipNonCodeUsages() { return false; } private boolean ensureElementsWritable(@NotNull final UsageInfo[] usages, final UsageViewDescriptor descriptor) { Set elements = new THashSet(TObjectHashingStrategy.IDENTITY); // protect against poorly implemented equality for (UsageInfo usage : usages) { assert usage != null: "Found null element in usages array"; if (skipNonCodeUsages() && usage.isNonCodeUsage()) continue; PsiElement element = usage.getElement(); if (element != null) elements.add(element); } elements.addAll(getElementsToWrite(descriptor)); return ensureFilesWritable(myProject, elements); } private static boolean ensureFilesWritable(final Project project, Collection elements) { PsiElement[] psiElements = PsiUtilCore.toPsiElementArray(elements); return CommonRefactoringUtil.checkReadOnlyStatus(project, psiElements); } protected void execute(final UsageInfo[] usages) { CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { Collection usageInfos = new LinkedHashSet(Arrays.asList(usages)); doRefactoring(usageInfos); if (isGlobalUndoAction()) CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject); } }, getCommandName(), null, getUndoConfirmationPolicy()); } protected boolean isGlobalUndoAction() { return CommonDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext()) == null; } @SuppressWarnings("MethodMayBeStatic") protected UndoConfirmationPolicy getUndoConfirmationPolicy() { return UndoConfirmationPolicy.DEFAULT; } private static UsageViewPresentation createPresentation(UsageViewDescriptor descriptor, final Usage[] usages) { UsageViewPresentation presentation = new UsageViewPresentation(); presentation.setTabText(RefactoringBundle.message("usageView.tabText")); presentation.setTargetsNodeText(descriptor.getProcessedElementsHeader()); presentation.setShowReadOnlyStatusAsRed(true); presentation.setShowCancelButton(true); presentation.setUsagesString(RefactoringBundle.message("usageView.usagesText")); int codeUsageCount = 0; int nonCodeUsageCount = 0; int dynamicUsagesCount = 0; Set codeFiles = new HashSet(); Set nonCodeFiles = new HashSet(); Set dynamicUsagesCodeFiles = new HashSet(); for (Usage usage : usages) { if (usage instanceof PsiElementUsage) { final PsiElementUsage elementUsage = (PsiElementUsage)usage; final PsiElement element = elementUsage.getElement(); if (element == null) continue; final PsiFile containingFile = element.getContainingFile(); if (elementUsage.isNonCodeUsage()) { nonCodeUsageCount++; nonCodeFiles.add(containingFile); } else { codeUsageCount++; codeFiles.add(containingFile); } if (usage instanceof UsageInfo2UsageAdapter) { final UsageInfo usageInfo = ((UsageInfo2UsageAdapter)usage).getUsageInfo(); if (usageInfo instanceof MoveRenameUsageInfo && usageInfo.isDynamicUsage()) { dynamicUsagesCount++; dynamicUsagesCodeFiles.add(containingFile); } } } } codeFiles.remove(null); nonCodeFiles.remove(null); dynamicUsagesCodeFiles.remove(null); String codeReferencesText = descriptor.getCodeReferencesText(codeUsageCount, codeFiles.size()); presentation.setCodeUsagesString(codeReferencesText); final String commentReferencesText = descriptor.getCommentReferencesText(nonCodeUsageCount, nonCodeFiles.size()); if (commentReferencesText != null) { presentation.setNonCodeUsagesString(commentReferencesText); } presentation.setDynamicUsagesString("Dynamic " + StringUtil.decapitalize(descriptor.getCodeReferencesText(dynamicUsagesCount, dynamicUsagesCodeFiles.size()))); String generatedCodeString; if (codeReferencesText.contains("in code")) { generatedCodeString = StringUtil.replace(codeReferencesText, "in code", "in generated code"); } else { generatedCodeString = codeReferencesText + " in generated code"; } presentation.setUsagesInGeneratedCodeString(generatedCodeString); return presentation; } private void showUsageView(final UsageViewDescriptor viewDescriptor, final Factory factory, final UsageInfo[] usageInfos) { UsageViewManager viewManager = UsageViewManager.getInstance(myProject); final PsiElement[] initialElements = viewDescriptor.getElements(); final UsageTarget[] targets = PsiElement2UsageTargetAdapter.convert(initialElements); final Ref convertUsagesRef = new Ref(); if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { @Override public void run() { ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { convertUsagesRef.set(UsageInfo2UsageAdapter.convert(usageInfos)); } }); } }, "Preprocess usages", true, myProject)) return; if (convertUsagesRef.isNull()) return; final Usage[] usages = convertUsagesRef.get(); final UsageViewPresentation presentation = createPresentation(viewDescriptor, usages); final UsageView usageView = viewManager.showUsages(targets, usages, presentation, factory); final Runnable refactoringRunnable = new Runnable() { @Override public void run() { Set usagesToRefactor = UsageViewUtil.getNotExcludedUsageInfos(usageView); final UsageInfo[] infos = usagesToRefactor.toArray(new UsageInfo[usagesToRefactor.size()]); if (ensureElementsWritable(infos, viewDescriptor)) { execute(infos); } } }; String canNotMakeString = RefactoringBundle.message("usageView.need.reRun"); addDoRefactoringAction(usageView, refactoringRunnable, canNotMakeString); } protected void addDoRefactoringAction(UsageView usageView, Runnable refactoringRunnable, String canNotMakeString) { usageView.addPerformOperationAction(refactoringRunnable, getCommandName(), canNotMakeString, RefactoringBundle.message("usageView.doAction"), false); } private void doRefactoring(@NotNull final Collection usageInfoSet) { for (Iterator iterator = usageInfoSet.iterator(); iterator.hasNext();) { UsageInfo usageInfo = iterator.next(); final PsiElement element = usageInfo.getElement(); if (element == null || !isToBeChanged(usageInfo)) { iterator.remove(); } } LocalHistoryAction action = LocalHistory.getInstance().startAction(getCommandName()); final UsageInfo[] writableUsageInfos = usageInfoSet.toArray(new UsageInfo[usageInfoSet.size()]); try { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); RefactoringListenerManagerImpl listenerManager = (RefactoringListenerManagerImpl)RefactoringListenerManager.getInstance(myProject); myTransaction = listenerManager.startTransaction(); final Map preparedData = new LinkedHashMap(); final Runnable prepareHelpersRunnable = new Runnable() { @Override public void run() { for (final RefactoringHelper helper : Extensions.getExtensions(RefactoringHelper.EP_NAME)) { Object operation = ApplicationManager.getApplication().runReadAction(new Computable() { @Override public Object compute() { return helper.prepareOperation(writableUsageInfos); } }); preparedData.put(helper, operation); } } }; ProgressManager.getInstance().runProcessWithProgressSynchronously(prepareHelpersRunnable, "Prepare ...", false, myProject); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { final String refactoringId = getRefactoringId(); if (refactoringId != null) { RefactoringEventData data = getBeforeData(); if (data != null) { data.addUsages(usageInfoSet); } myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).refactoringStarted(refactoringId, data); } try { if (refactoringId != null) { UndoableAction action = new BasicUndoableAction() { @Override public void undo() { myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).undoRefactoring(refactoringId); } @Override public void redo() { } }; UndoManager.getInstance(myProject).undoableActionPerformed(action); } performRefactoring(writableUsageInfos); } finally { if (refactoringId != null) { myProject.getMessageBus() .syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).refactoringDone(refactoringId, getAfterData(writableUsageInfos)); } } } }); for(Map.Entry e: preparedData.entrySet()) { //noinspection unchecked e.getKey().performOperation(myProject, e.getValue()); } myTransaction.commit(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { performPsiSpoilingRefactoring(); } }); } finally { action.finish(); } int count = writableUsageInfos.length; if (count > 0) { StatusBarUtil.setStatusBarInfo(myProject, RefactoringBundle.message("statusBar.refactoring.result", count)); } else { if (!isPreviewUsages(writableUsageInfos)) { StatusBarUtil.setStatusBarInfo(myProject, RefactoringBundle.message("statusBar.noUsages")); } } } protected boolean isToBeChanged(UsageInfo usageInfo) { return usageInfo.isWritable(); } /** * Refactorings that spoil PSI (write something directly to documents etc.) should * do that in this method.
* This method is called immediately after * {@link #performRefactoring(UsageInfo[])}. */ protected void performPsiSpoilingRefactoring() { } protected void prepareSuccessful() { if (myPrepareSuccessfulSwingThreadCallback != null) { // make sure that dialog is closed in swing thread try { GuiUtils.runOrInvokeAndWait(myPrepareSuccessfulSwingThreadCallback); } catch (InterruptedException e) { LOG.error(e); } catch (InvocationTargetException e) { LOG.error(e); } } } @Override public final void run() { if (ApplicationManager.getApplication().isUnitTestMode()) { ApplicationManager.getApplication().assertIsDispatchThread(); doRun(); UIUtil.dispatchAllInvocationEvents(); UIUtil.dispatchAllInvocationEvents(); return; } if (ApplicationManager.getApplication().isWriteAccessAllowed()) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { doRun(); } }, myProject.getDisposed()); } else { doRun(); } } public static class ConflictsInTestsException extends RuntimeException { private final Collection messages; private static boolean myTestIgnore = false; public ConflictsInTestsException(Collection messages) { this.messages = messages; } public static void setTestIgnore(boolean myIgnore) { myTestIgnore = myIgnore; } public static boolean isTestIgnore() { return myTestIgnore; } public Collection getMessages() { List result = new ArrayList(messages); for (int i = 0; i < messages.size(); i++) { result.set(i, result.get(i).replaceAll("<[^>]+>", "")); } return result; } @Override public String getMessage() { return StringUtil.join(messages, "\n"); } } @Deprecated protected boolean showConflicts(final MultiMap conflicts) { return showConflicts(conflicts, null); } protected boolean showConflicts(final MultiMap conflicts, @Nullable final UsageInfo[] usages) { if (!conflicts.isEmpty() && ApplicationManager.getApplication().isUnitTestMode()) { throw new ConflictsInTestsException(conflicts.values()); } if (myPrepareSuccessfulSwingThreadCallback != null && !conflicts.isEmpty()) { final String refactoringId = getRefactoringId(); if (refactoringId != null) { RefactoringEventData conflictUsages = new RefactoringEventData(); conflictUsages.putUserData(RefactoringEventData.CONFLICTS_KEY, conflicts.values()); myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC).conflictsDetected(refactoringId, conflictUsages); } final ConflictsDialog conflictsDialog = prepareConflictsDialog(conflicts, usages); conflictsDialog.show(); if (!conflictsDialog.isOK()) { if (conflictsDialog.isShowConflicts()) prepareSuccessful(); return false; } } prepareSuccessful(); return true; } @NotNull protected ConflictsDialog prepareConflictsDialog(MultiMap conflicts, @Nullable final UsageInfo[] usages) { final ConflictsDialog conflictsDialog = createConflictsDialog(conflicts, usages); conflictsDialog.setCommandName(getCommandName()); return conflictsDialog; } @Nullable protected RefactoringEventData getBeforeData() { return null; } @Nullable protected RefactoringEventData getAfterData(UsageInfo[] usages) { return null; } @Nullable protected String getRefactoringId() { return null; } @NotNull protected ConflictsDialog createConflictsDialog(MultiMap conflicts, @Nullable final UsageInfo[] usages) { return new ConflictsDialog(myProject, conflicts, usages == null ? null : new Runnable() { @Override public void run() { execute(usages); } }, false, true); } @NotNull protected Collection getElementsToWrite(@NotNull UsageViewDescriptor descriptor) { return Arrays.asList(descriptor.getElements()); } public static class UnknownReferenceTypeException extends RuntimeException { private final Language myElementLanguage; public UnknownReferenceTypeException(final Language elementLanguage) { myElementLanguage = elementLanguage; } public Language getElementLanguage() { return myElementLanguage; } } }