avoid dlg from write action
[idea/community.git] / java / java-impl / src / com / intellij / refactoring / copy / CopyClassesHandler.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.refactoring.copy;
17
18 import com.intellij.codeInsight.actions.OptimizeImportsProcessor;
19 import com.intellij.featureStatistics.FeatureUsageTracker;
20 import com.intellij.ide.util.EditorHelper;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.application.WriteAction;
23 import com.intellij.openapi.command.CommandProcessor;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.roots.JavaProjectRootsUtil;
27 import com.intellij.openapi.roots.ProjectRootManager;
28 import com.intellij.openapi.ui.Messages;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vfs.VirtualFile;
31 import com.intellij.openapi.wm.ToolWindowManager;
32 import com.intellij.psi.*;
33 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
34 import com.intellij.psi.search.LocalSearchScope;
35 import com.intellij.psi.search.searches.ReferencesSearch;
36 import com.intellij.psi.util.PsiUtilCore;
37 import com.intellij.refactoring.MoveDestination;
38 import com.intellij.refactoring.RefactoringBundle;
39 import com.intellij.refactoring.move.moveClassesOrPackages.MoveDirectoryWithClassesProcessor;
40 import com.intellij.util.ArrayUtil;
41 import com.intellij.util.ArrayUtilRt;
42 import com.intellij.util.IncorrectOperationException;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45
46 import java.io.IOException;
47 import java.util.*;
48
49 public class CopyClassesHandler extends CopyHandlerDelegateBase {
50   private static final Logger LOG = Logger.getInstance("#" + CopyClassesHandler.class.getName());
51
52   @Override
53   public boolean forbidToClone(PsiElement[] elements, boolean fromUpdate) {
54     final Map<PsiFile, PsiClass[]> fileMap = convertToTopLevelClasses(elements, fromUpdate, null, null);
55     if (fileMap != null && fileMap.size() == 1) {
56       final PsiClass[] psiClasses = fileMap.values().iterator().next();
57       return psiClasses != null && psiClasses.length > 1;
58     }
59     return true;
60   }
61
62   @Override
63   public boolean canCopy(PsiElement[] elements, boolean fromUpdate) {
64     return canCopyClass(fromUpdate, elements);
65   }
66   public static boolean canCopyClass(PsiElement... elements) {
67     return canCopyClass(false, elements);
68   }
69   public static boolean canCopyClass(boolean fromUpdate, PsiElement... elements) {
70     if (fromUpdate && elements.length > 0 && elements[0] instanceof PsiDirectory) return true;
71     return convertToTopLevelClasses(elements, fromUpdate, null, null) != null;
72   }
73
74   @Nullable
75   private static Map<PsiFile, PsiClass[]> convertToTopLevelClasses(final PsiElement[] elements,
76                                                                    final boolean fromUpdate,
77                                                                    String relativePath,
78                                                                    Map<PsiFile, String> relativeMap) {
79     final Map<PsiFile, PsiClass[]> result = new HashMap<>();
80     for (PsiElement element : elements) {
81       final PsiElement navigationElement = element.getNavigationElement();
82       LOG.assertTrue(navigationElement != null, element);
83       final PsiFile containingFile = navigationElement.getContainingFile();
84       if (!(containingFile instanceof PsiClassOwner &&
85             JavaProjectRootsUtil.isOutsideJavaSourceRoot(containingFile))) {
86         PsiClass[] topLevelClasses = getTopLevelClasses(element);
87         if (topLevelClasses == null) {
88           if (element instanceof PsiDirectory) {
89             if (!fromUpdate) {
90               final String name = ((PsiDirectory)element).getName();
91               final String path = relativePath != null ? (relativePath.length() > 0 ? (relativePath + "/") : "") + name : null;
92               final Map<PsiFile, PsiClass[]> map = convertToTopLevelClasses(element.getChildren(), fromUpdate, path, relativeMap);
93               if (map == null) return null;
94               for (Map.Entry<PsiFile, PsiClass[]> entry : map.entrySet()) {
95                 fillResultsMap(result, entry.getKey(), entry.getValue());
96               }
97             }
98             continue;
99           }
100           if (!(element instanceof PsiFileSystemItem)) return null;
101         }
102         fillResultsMap(result, containingFile, topLevelClasses);
103         if (relativeMap != null) {
104           relativeMap.put(containingFile, relativePath);
105         }
106       }
107     }
108     if (result.isEmpty()) {
109       return null;
110     }
111     else {
112       boolean hasClasses = false;
113       for (PsiClass[] classes : result.values()) {
114         if (classes != null) {
115           hasClasses = true;
116           break;
117         }
118       }
119       return hasClasses ? result : null;
120     }
121   }
122
123   @Nullable
124   private static String normalizeRelativeMap(Map<PsiFile, String> relativeMap) {
125     String vector = null;
126     for (String relativePath : relativeMap.values()) {
127       if (vector == null) {
128         vector = relativePath;
129       } else if (vector.startsWith(relativePath + "/")) {
130         vector = relativePath;
131       } else if (!relativePath.startsWith(vector + "/") && !relativePath.equals(vector)) {
132         return null;
133       }
134     }
135     if (vector != null) {
136       for (PsiFile psiFile : relativeMap.keySet()) {
137         final String path = relativeMap.get(psiFile);
138         relativeMap.put(psiFile, path.equals(vector) ? "" : path.substring(vector.length() + 1));
139       }
140     }
141     return vector;
142   }
143
144   private static void fillResultsMap(Map<PsiFile, PsiClass[]> result, PsiFile containingFile, PsiClass[] topLevelClasses) {
145     PsiClass[] classes = result.get(containingFile);
146     if (topLevelClasses != null) {
147       if (classes != null) {
148         topLevelClasses = ArrayUtil.mergeArrays(classes, topLevelClasses, PsiClass.ARRAY_FACTORY);
149       }
150       result.put(containingFile, topLevelClasses);
151     } else {
152       result.put(containingFile, classes);
153     }
154   }
155
156   public void doCopy(PsiElement[] elements, PsiDirectory defaultTargetDirectory) {
157     FeatureUsageTracker.getInstance().triggerFeatureUsed("refactoring.copyClass");
158     final HashMap<PsiFile, String> relativePathsMap = new HashMap<>();
159     final Map<PsiFile, PsiClass[]> classes = convertToTopLevelClasses(elements, false, "", relativePathsMap);
160     assert classes != null;
161     if (defaultTargetDirectory == null) {
162       final PsiFile psiFile = classes.keySet().iterator().next();
163       defaultTargetDirectory = psiFile.getContainingDirectory();
164       LOG.assertTrue(defaultTargetDirectory != null, psiFile);
165     }
166     Project project = defaultTargetDirectory.getProject();
167     VirtualFile sourceRootForFile = ProjectRootManager.getInstance(project).getFileIndex().getSourceRootForFile(defaultTargetDirectory.getVirtualFile());
168     if (sourceRootForFile == null) {
169       final List<PsiElement> files = new ArrayList<>();
170       for (PsiElement element : elements) {
171         PsiFile containingFile = element.getContainingFile();
172         if (containingFile != null) {
173           files.add(containingFile);
174         }
175         else if (element instanceof PsiDirectory) {
176           files.add(element);
177         }
178       }
179       CopyFilesOrDirectoriesHandler.copyAsFiles(files.toArray(new PsiElement[files.size()]), defaultTargetDirectory, project);
180       return;
181     }
182     Object targetDirectory = null;
183     String className = null;
184     boolean openInEditor = true;
185     if (copyOneClass(classes)) {
186       final String commonPath =
187         ArrayUtilRt.find(elements, classes.values().iterator().next()) == -1 ? normalizeRelativeMap(relativePathsMap) : null;
188       CopyClassDialog dialog = new CopyClassDialog(classes.values().iterator().next()[0], defaultTargetDirectory, project, false) {
189         @Override
190         protected String getQualifiedName() {
191           final String qualifiedName = super.getQualifiedName();
192           if (commonPath != null && !commonPath.isEmpty() && !qualifiedName.endsWith(commonPath)) {
193             return StringUtil.getQualifiedName(qualifiedName, commonPath.replaceAll("/", "."));
194           }
195           return qualifiedName;
196         }
197       };
198       dialog.setTitle(RefactoringBundle.message("copy.handler.copy.class"));
199       if (dialog.showAndGet()) {
200         openInEditor = dialog.openInEditor();
201         targetDirectory = dialog.getTargetDirectory();
202         className = dialog.getClassName();
203         if (className == null || className.length() == 0) return;
204       }
205     } else {
206       if (ApplicationManager.getApplication().isUnitTestMode()) {
207         targetDirectory = defaultTargetDirectory;
208       } else {
209         defaultTargetDirectory = CopyFilesOrDirectoriesHandler.resolveDirectory(defaultTargetDirectory);
210         if (defaultTargetDirectory == null) return;
211         PsiElement[] files = PsiUtilCore.toPsiFileArray(classes.keySet());
212         if (classes.keySet().size() == 1) {
213           //do not choose a new name for a file when multiple classes exist in one file
214           final PsiClass[] psiClasses = classes.values().iterator().next();
215           if (psiClasses != null) {
216             files = psiClasses;
217           }
218         }
219         final CopyFilesOrDirectoriesDialog dialog = new CopyFilesOrDirectoriesDialog(files, defaultTargetDirectory, project, false);
220         if (dialog.showAndGet()) {
221           targetDirectory = dialog.getTargetDirectory();
222           className = dialog.getNewName();
223           openInEditor = dialog.openInEditor();
224         }
225       }
226     }
227     if (targetDirectory != null) {
228       copyClassesImpl(className, project, classes, relativePathsMap, targetDirectory, defaultTargetDirectory, RefactoringBundle.message(
229         "copy.handler.copy.class"), false, openInEditor);
230     }
231   }
232
233   private static boolean copyOneClass(Map<PsiFile, PsiClass[]> classes) {
234     if (classes.size() == 1){
235       final PsiClass[] psiClasses = classes.values().iterator().next();
236       return psiClasses != null && psiClasses.length == 1;
237     }
238     return false;
239   }
240
241   public void doClone(PsiElement element) {
242     FeatureUsageTracker.getInstance().triggerFeatureUsed("refactoring.copyClass");
243     PsiClass[] classes = getTopLevelClasses(element);
244     if (classes == null) {
245       CopyFilesOrDirectoriesHandler.doCloneFile(element);
246       return;
247     }
248     Project project = element.getProject();
249
250     CopyClassDialog dialog = new CopyClassDialog(classes[0], null, project, true);
251     dialog.setTitle(RefactoringBundle.message("copy.handler.clone.class"));
252     if (dialog.showAndGet()) {
253       String className = dialog.getClassName();
254       PsiDirectory targetDirectory = element.getContainingFile().getContainingDirectory();
255       copyClassesImpl(className, project, Collections.singletonMap(classes[0].getContainingFile(), classes), null, targetDirectory,
256                       targetDirectory, RefactoringBundle.message("copy.handler.clone.class"), true, true);
257     }
258   }
259
260   private static void copyClassesImpl(final String copyClassName,
261                                       final Project project,
262                                       final Map<PsiFile, PsiClass[]> classes,
263                                       final HashMap<PsiFile, String> map,
264                                       final Object targetDirectory,
265                                       final PsiDirectory defaultTargetDirectory,
266                                       final String commandName,
267                                       final boolean selectInActivePanel, 
268                                       final boolean openInEditor) {
269     final boolean[] result = new boolean[] {false};
270     Runnable command = () -> {
271       PsiDirectory target;
272       if (targetDirectory instanceof PsiDirectory) {
273         target = (PsiDirectory)targetDirectory;
274       } else {
275         target = WriteAction.compute(() -> (MoveDestination)targetDirectory).getTargetDirectory(defaultTargetDirectory);
276       }
277       try {
278         Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project);
279         if (files != null) {
280           if (openInEditor) {
281             for (PsiFile file : files) {
282               CopyHandler.updateSelectionInActiveProjectView(file, project, selectInActivePanel);
283             }
284             EditorHelper.openFilesInEditor(files.toArray(new PsiFile[files.size()]));
285           }
286         }
287       }
288       catch (IncorrectOperationException ex) {
289         Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon());
290       }
291     };
292     CommandProcessor processor = CommandProcessor.getInstance();
293     processor.executeCommand(project, command, commandName, null);
294
295     if (result[0]) {
296       ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project).activateEditorComponent());
297     }
298   }
299
300    @Nullable
301   public static Collection<PsiFile> doCopyClasses(final Map<PsiFile, PsiClass[]> fileToClasses,
302                                          final String copyClassName,
303                                          final PsiDirectory targetDirectory,
304                                          final Project project) throws IncorrectOperationException {
305      return doCopyClasses(fileToClasses, null, copyClassName, targetDirectory, project);
306    }
307
308   @Nullable
309   public static Collection<PsiFile> doCopyClasses(final Map<PsiFile, PsiClass[]> fileToClasses,
310                                                      @Nullable HashMap<PsiFile, String> map, final String copyClassName,
311                                                      final PsiDirectory targetDirectory,
312                                                      final Project project) throws IncorrectOperationException {
313     PsiElement newElement = null;
314     final Map<PsiClass, PsiElement> oldToNewMap = new HashMap<>();
315     for (final PsiClass[] psiClasses : fileToClasses.values()) {
316       if (psiClasses != null) {
317         for (PsiClass aClass : psiClasses) {
318           if (isSynthetic(aClass)) {
319             continue;
320           }
321           oldToNewMap.put(aClass, null);
322         }
323       }
324     }
325     final List<PsiFile> createdFiles = new ArrayList<>(fileToClasses.size());
326     int[] choice = fileToClasses.size() > 1 ? new int[]{-1} : null;
327     List<PsiFile> files = new ArrayList<>();
328     for (final Map.Entry<PsiFile, PsiClass[]> entry : fileToClasses.entrySet()) {
329       final PsiFile psiFile = entry.getKey();
330       final PsiClass[] sources = entry.getValue();
331       if (psiFile instanceof PsiClassOwner && sources != null) {
332         final PsiFile createdFile = copy(psiFile, targetDirectory, copyClassName, map == null ? null : map.get(psiFile), choice);
333         if (createdFile == null) {
334           //do not touch unmodified classes
335           for (PsiClass aClass : ((PsiClassOwner)psiFile).getClasses()) {
336             oldToNewMap.remove(aClass);
337           }
338           continue;
339         }
340         for (final PsiClass destination : ((PsiClassOwner)createdFile).getClasses()) {
341           if (isSynthetic(destination)) {
342             continue;
343           }
344           PsiClass source = findByName(sources, destination.getName());
345           if (source != null) {
346             final PsiClass copy = copy(source, copyClassName);
347             newElement = WriteAction.compute(() -> destination.replace(copy));
348             oldToNewMap.put(source, newElement);
349           }
350           else {
351             WriteAction.run(() -> destination.delete());
352           }
353         }
354         createdFiles.add(createdFile);
355       } else {
356         files.add(psiFile);
357       }
358     }
359
360     
361     for (PsiFile file : files) {
362       try {
363         PsiDirectory finalTarget = targetDirectory;
364         final String relativePath = map != null ? map.get(file) : null;
365         if (relativePath != null && !relativePath.isEmpty()) {
366           finalTarget = WriteAction.compute(() -> buildRelativeDir(targetDirectory, relativePath).findOrCreateTargetDirectory());
367         }
368         final PsiFile fileCopy = CopyFilesOrDirectoriesHandler.copyToDirectory(file, getNewFileName(file, copyClassName), finalTarget, choice, null);
369         if (fileCopy != null) {
370           createdFiles.add(fileCopy);
371         }
372       }
373       catch (IOException e) {
374         throw new IncorrectOperationException(e.getMessage());
375       }
376     }
377
378     WriteAction.run(() -> {
379       final Set<PsiElement> rebindExpressions = new HashSet<>();
380       for (PsiElement element : oldToNewMap.values()) {
381         if (element == null) {
382           LOG.error(oldToNewMap.keySet());
383           continue;
384         }
385         decodeRefs(element, oldToNewMap, rebindExpressions);
386       }
387
388       final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project);
389       for (PsiFile psiFile : createdFiles) {
390         if (psiFile instanceof PsiJavaFile) {
391           codeStyleManager.removeRedundantImports((PsiJavaFile)psiFile);
392         }
393       }
394       for (PsiElement expression : rebindExpressions) {
395         //filter out invalid elements which are produced by nested elements:
396         //new expressions/type elements, like: List<List<String>>; new Foo(new Foo()), etc
397         if (expression.isValid()) {
398           codeStyleManager.shortenClassReferences(expression);
399         }
400       }
401     });
402
403     new OptimizeImportsProcessor(project, createdFiles.toArray(new PsiFile[createdFiles.size()]), null).run();
404     return createdFiles;
405   }
406
407   protected static boolean isSynthetic(PsiClass aClass) {
408     return aClass instanceof SyntheticElement || !aClass.isPhysical();
409   }
410
411   private static PsiFile copy(@NotNull PsiFile file, PsiDirectory directory, String name, String relativePath, int[] choice) {
412     final String fileName = getNewFileName(file, name);
413     if (relativePath != null && !relativePath.isEmpty()) {
414       return WriteAction.compute(() -> buildRelativeDir(directory, relativePath).findOrCreateTargetDirectory().copyFileFrom(fileName, file));
415     }
416     if (CopyFilesOrDirectoriesHandler.checkFileExist(directory, choice, file, fileName, "Copy")) return null;
417     return WriteAction.compute(() -> directory.copyFileFrom(fileName, file));
418   }
419
420   private static String getNewFileName(PsiFile file, String name) {
421     if (name != null) {
422       if (file instanceof PsiClassOwner) {
423         for (final PsiClass psiClass : ((PsiClassOwner)file).getClasses()) {
424           if (!isSynthetic(psiClass)) {
425             return name + "." + file.getViewProvider().getVirtualFile().getExtension();
426           }
427         }
428       }
429       return name;
430     }
431     return file.getName();
432   }
433
434   @NotNull
435   private static MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper buildRelativeDir(final @NotNull PsiDirectory directory,
436                                                                                            final @NotNull String relativePath) {
437     MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper current = null;
438     for (String pathElement : relativePath.split("/")) {
439       if (current == null) {
440         current = new MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper(directory, pathElement);
441       } else {
442         current = new MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper(current, pathElement);
443       }
444     }
445     LOG.assertTrue(current != null);
446     return current;
447   }
448
449   private static PsiClass copy(PsiClass aClass, String name) {
450     final PsiClass classNavigationElement = (PsiClass)aClass.getNavigationElement();
451     final PsiClass classCopy = (PsiClass)classNavigationElement.copy();
452     if (name != null) {
453       classCopy.setName(name);
454     }
455     return classCopy;
456   }
457
458   @Nullable
459   private static PsiClass findByName(PsiClass[] classes, String name) {
460     if (name != null) {
461       for (PsiClass aClass : classes) {
462         if (name.equals(aClass.getName())) {
463           return aClass;
464         }
465       }
466     }
467     return null;
468   }
469
470   private static void rebindExternalReferences(PsiElement element,
471                                                Map<PsiClass, PsiElement> oldToNewMap,
472                                                Set<PsiElement> rebindExpressions) {
473      final LocalSearchScope searchScope = new LocalSearchScope(element);
474      for (PsiClass aClass : oldToNewMap.keySet()) {
475        final PsiElement newClass = oldToNewMap.get(aClass);
476        for (PsiReference reference : ReferencesSearch.search(aClass, searchScope)) {
477          rebindExpressions.add(reference.bindToElement(newClass));
478        }
479      }
480    }
481
482
483   private static void decodeRefs(@NotNull PsiElement element, final Map<PsiClass, PsiElement> oldToNewMap, final Set<PsiElement> rebindExpressions) {
484     final Map<PsiJavaCodeReferenceElement, PsiElement> rebindMap = new LinkedHashMap<>();
485     element.accept(new JavaRecursiveElementVisitor(){
486       @Override
487       public void visitReferenceElement(PsiJavaCodeReferenceElement reference) {
488         super.visitReferenceElement(reference);
489         decodeRef(reference, oldToNewMap, rebindMap);
490       }
491     });
492     for (Map.Entry<PsiJavaCodeReferenceElement, PsiElement> entry : rebindMap.entrySet()) {
493       rebindExpressions.add(entry.getKey().bindToElement(entry.getValue()));
494     }
495     rebindExternalReferences(element, oldToNewMap, rebindExpressions);
496   }
497
498   private static void decodeRef(final PsiJavaCodeReferenceElement expression,
499                                 final Map<PsiClass, PsiElement> oldToNewMap,
500                                 Map<PsiJavaCodeReferenceElement, PsiElement> rebindExpressions) {
501     final PsiElement resolved = expression.resolve();
502     if (resolved instanceof PsiClass) {
503       final PsiClass psiClass = (PsiClass)resolved;
504       if (oldToNewMap.containsKey(psiClass)) {
505         rebindExpressions.put(expression, oldToNewMap.get(psiClass));
506       }
507     }
508   }
509
510   @Nullable
511   private static PsiClass[] getTopLevelClasses(PsiElement element) {
512     while (true) {
513       if (element == null || element instanceof PsiFile) break;
514       if (element instanceof PsiClass && element.getParent() != null && ((PsiClass)element).getContainingClass() == null && !(element instanceof PsiAnonymousClass)) break;
515       element = element.getParent();
516     }
517     //if (element instanceof PsiCompiledElement) return null;
518     if (element instanceof PsiClassOwner) {
519       PsiClass[] classes = ((PsiClassOwner)element).getClasses();
520       ArrayList<PsiClass> buffer = new ArrayList<>();
521       for (final PsiClass aClass : classes) {
522         if (isSynthetic(aClass)) {
523           return null;
524         }
525         buffer.add(aClass);
526       }
527       return buffer.toArray(new PsiClass[buffer.size()]);
528     }
529     return element instanceof PsiClass ? new PsiClass[]{(PsiClass)element} : null;
530   }
531 }