EditorConfig documentation test
[idea/community.git] / java / java-impl / src / com / intellij / codeInsight / ExternalAnnotationsManagerImpl.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInsight;
3
4 import com.google.common.annotations.VisibleForTesting;
5 import com.intellij.CommonBundle;
6 import com.intellij.ProjectTopics;
7 import com.intellij.codeInsight.highlighting.HighlightManager;
8 import com.intellij.diagnostic.AttachmentFactory;
9 import com.intellij.icons.AllIcons;
10 import com.intellij.ide.DataManager;
11 import com.intellij.ide.highlighter.XmlFileType;
12 import com.intellij.openapi.application.Application;
13 import com.intellij.openapi.application.ApplicationManager;
14 import com.intellij.openapi.command.WriteCommandAction;
15 import com.intellij.openapi.command.undo.BasicUndoableAction;
16 import com.intellij.openapi.command.undo.UndoManager;
17 import com.intellij.openapi.command.undo.UndoUtil;
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.editor.*;
20 import com.intellij.openapi.editor.colors.EditorColors;
21 import com.intellij.openapi.editor.colors.EditorColorsManager;
22 import com.intellij.openapi.editor.event.DocumentEvent;
23 import com.intellij.openapi.editor.event.DocumentListener;
24 import com.intellij.openapi.editor.markup.RangeHighlighter;
25 import com.intellij.openapi.editor.markup.TextAttributes;
26 import com.intellij.openapi.fileChooser.FileChooser;
27 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
28 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
29 import com.intellij.openapi.fileEditor.FileDocumentManager;
30 import com.intellij.openapi.fileEditor.FileEditorManager;
31 import com.intellij.openapi.project.DumbService;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.project.ProjectBundle;
34 import com.intellij.openapi.projectRoots.SdkModificator;
35 import com.intellij.openapi.roots.*;
36 import com.intellij.openapi.roots.libraries.Library;
37 import com.intellij.openapi.ui.DialogWrapper;
38 import com.intellij.openapi.ui.Messages;
39 import com.intellij.openapi.ui.popup.JBPopupFactory;
40 import com.intellij.openapi.ui.popup.PopupStep;
41 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
42 import com.intellij.openapi.util.Comparing;
43 import com.intellij.openapi.util.TextRange;
44 import com.intellij.openapi.util.text.StringUtil;
45 import com.intellij.openapi.vfs.*;
46 import com.intellij.psi.*;
47 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
48 import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
49 import com.intellij.psi.util.PsiTreeUtil;
50 import com.intellij.psi.xml.XmlDocument;
51 import com.intellij.psi.xml.XmlFile;
52 import com.intellij.psi.xml.XmlTag;
53 import com.intellij.util.ArrayUtil;
54 import com.intellij.util.IncorrectOperationException;
55 import com.intellij.util.Processor;
56 import com.intellij.util.containers.ContainerUtil;
57 import com.intellij.util.messages.MessageBus;
58 import com.intellij.util.ui.OptionsMessageDialog;
59 import one.util.streamex.StreamEx;
60 import org.jetbrains.annotations.Contract;
61 import org.jetbrains.annotations.NonNls;
62 import org.jetbrains.annotations.NotNull;
63 import org.jetbrains.annotations.Nullable;
64
65 import javax.swing.*;
66 import java.awt.*;
67 import java.awt.event.ActionEvent;
68 import java.io.IOException;
69 import java.util.List;
70 import java.util.*;
71 import java.util.function.Function;
72 import java.util.stream.Collectors;
73
74 /**
75  * @author anna
76  */
77 public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsManager {
78   private static final Logger LOG = Logger.getInstance(ExternalAnnotationsManagerImpl.class);
79
80   private final MessageBus myBus;
81
82   public ExternalAnnotationsManagerImpl(@NotNull final Project project, final PsiManager psiManager) {
83     super(psiManager);
84     myBus = project.getMessageBus();
85     myBus.connect(project).subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
86       @Override
87       public void rootsChanged(@NotNull ModuleRootEvent event) {
88         dropCache();
89       }
90     });
91
92     VirtualFileManager.getInstance().addVirtualFileListener(new MyVirtualFileListener(), project);
93     EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new MyDocumentListener(), project);
94   }
95
96   private void notifyAfterAnnotationChanging(@NotNull PsiModifierListOwner owner, @NotNull String annotationFQName, boolean successful) {
97     myBus.syncPublisher(TOPIC).afterExternalAnnotationChanging(owner, annotationFQName, successful);
98     myPsiManager.dropPsiCaches();
99   }
100
101   private void notifyChangedExternally() {
102     myBus.syncPublisher(TOPIC).externalAnnotationsChangedExternally();
103     myPsiManager.dropPsiCaches();
104   }
105
106   @Override
107   public void annotateExternally(@NotNull final PsiModifierListOwner listOwner,
108                                  @NotNull final String annotationFQName,
109                                  @NotNull final PsiFile fromFile,
110                                  @Nullable final PsiNameValuePair[] value) throws CanceledConfigurationException {
111     Application application = ApplicationManager.getApplication();
112     application.assertIsDispatchThread();
113     LOG.assertTrue(!application.isWriteAccessAllowed());
114
115     final Project project = myPsiManager.getProject();
116     final PsiFile containingFile = listOwner.getOriginalElement().getContainingFile();
117     if (!(containingFile instanceof PsiJavaFile)) {
118       notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
119       return;
120     }
121     final VirtualFile containingVirtualFile = containingFile.getVirtualFile();
122     LOG.assertTrue(containingVirtualFile != null);
123     final List<OrderEntry> entries = ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(containingVirtualFile);
124     if (entries.isEmpty()) {
125       notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
126       return;
127     }
128     ExternalAnnotation annotation = new ExternalAnnotation(listOwner, annotationFQName, value);
129     for (final OrderEntry entry : entries) {
130       if (entry instanceof ModuleOrderEntry) continue;
131       VirtualFile[] roots = AnnotationOrderRootType.getFiles(entry);
132       roots = filterByReadOnliness(roots);
133
134       if (roots.length > 0) {
135         chooseRootAndAnnotateExternally(roots, annotation);
136       }
137       else {
138         if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
139           notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
140           return;
141         }
142         DumbService.getInstance(project).setAlternativeResolveEnabled(true);
143         try {
144           if (!setupRootAndAnnotateExternally(entry, project, annotation)) {
145             throw new CanceledConfigurationException();
146           }
147         }
148         finally {
149           DumbService.getInstance(project).setAlternativeResolveEnabled(false);
150         }
151       }
152       break;
153     }
154   }
155
156   private void annotateExternally(@NotNull VirtualFile root, @NotNull ExternalAnnotation annotation) {
157     annotateExternally(root, Collections.singletonList(annotation));
158   }
159
160   /**
161    * Tries to add external annotations into given root if possible.
162    * Notifies about each addition result separately.
163    */
164   public void annotateExternally(@NotNull VirtualFile root, @NotNull List<? extends ExternalAnnotation> annotations) {
165     Project project = myPsiManager.getProject();
166
167     Map<Optional<XmlFile>, List<ExternalAnnotation>> annotationsByFiles = annotations.stream()
168       .collect(Collectors.groupingBy(annotation -> Optional.ofNullable(getFileForAnnotations(root, annotation.getOwner(), project))));
169
170     WriteCommandAction.writeCommandAction(project).run(() -> {
171       try {
172         for (Map.Entry<Optional<XmlFile>, List<ExternalAnnotation>> entry : annotationsByFiles.entrySet()) {
173           XmlFile annotationsFile = entry.getKey().orElse(null);
174           List<ExternalAnnotation> fileAnnotations = entry.getValue();
175           annotateExternally(annotationsFile, fileAnnotations);
176         }
177
178         UndoManager.getInstance(project).undoableActionPerformed(new BasicUndoableAction() {
179           @Override
180           public void undo() {
181             dropCache();
182             notifyChangedExternally();
183           }
184
185           @Override
186           public void redo() {
187             dropCache();
188             notifyChangedExternally();
189           }
190         });
191       } finally {
192         dropCache();
193       }
194     });
195   }
196
197   private void annotateExternally(@Nullable XmlFile annotationsFile, @NotNull List<ExternalAnnotation> annotations) {
198     XmlTag rootTag = extractRootTag(annotationsFile);
199
200     TreeMap<String, List<ExternalAnnotation>> ownerToAnnotations = StreamEx.of(annotations)
201       .mapToEntry(annotation -> StringUtil.escapeXmlEntities(getExternalName(annotation.getOwner())), Function.identity())
202       .distinct()
203       .grouping(() -> new TreeMap<>(Comparator.nullsFirst(Comparator.naturalOrder())));
204
205     if (rootTag == null) {
206       ownerToAnnotations.values().stream().flatMap(List::stream).forEach(annotation ->
207                                   notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false));
208       return;
209     }
210
211     List<ExternalAnnotation> savedAnnotations = new ArrayList<>();
212     XmlTag startTag = null;
213
214     for (Map.Entry<String, List<ExternalAnnotation>> entry : ownerToAnnotations.entrySet()) {
215       @NonNls String ownerName = entry.getKey();
216       List<ExternalAnnotation> annotationList = entry.getValue();
217       for (ExternalAnnotation annotation : annotationList) {
218
219         if (ownerName == null) {
220           notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
221           continue;
222         }
223
224         try {
225           startTag = addAnnotation(rootTag, ownerName, annotation, startTag);
226           savedAnnotations.add(annotation);
227         }
228         catch (IncorrectOperationException e) {
229           LOG.error(e);
230           notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
231         }
232         finally {
233           dropCache();
234           markForUndo(annotation.getOwner().getContainingFile());
235         }
236       }
237     }
238
239     commitChanges(annotationsFile);
240     savedAnnotations.forEach(annotation ->
241                                notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), true));
242   }
243
244   @Contract("null -> null")
245   private static XmlTag extractRootTag(XmlFile annotationsFile) {
246     if (annotationsFile == null) {
247       return null;
248     }
249
250     XmlDocument document = annotationsFile.getDocument();
251     if (document == null) {
252       return null;
253     }
254
255     return document.getRootTag();
256   }
257
258   private static void markForUndo(@Nullable PsiFile containingFile) {
259     if (containingFile == null) {
260       return;
261     }
262
263     VirtualFile virtualFile = containingFile.getVirtualFile();
264     if (virtualFile != null && virtualFile.isInLocalFileSystem()) {
265       UndoUtil.markPsiFileForUndo(containingFile);
266     }
267   }
268
269   /**
270    * Adds annotation sub tag after startTag.
271    * If startTag is {@code null} searches for all sub tags of rootTag and starts from the first.
272    *
273    * @param rootTag root tag to insert subtag into
274    * @param ownerName annotations owner name
275    * @param annotation external annotation
276    * @param startTag start tag
277    * @return added sub tag
278    */
279   @NotNull
280   private XmlTag addAnnotation(@NotNull XmlTag rootTag, @NotNull String ownerName,
281                                @NotNull ExternalAnnotation annotation, @Nullable XmlTag startTag) {
282     if (startTag == null) {
283       startTag = PsiTreeUtil.findChildOfType(rootTag, XmlTag.class);
284     }
285
286     XmlTag prevItem = null;
287     XmlTag curItem = startTag;
288
289     while (curItem != null) {
290       XmlTag addedItem = addAnnotation(rootTag, ownerName, annotation, curItem, prevItem);
291       if (addedItem != null) {
292         return addedItem;
293       }
294
295       prevItem = curItem;
296       curItem = PsiTreeUtil.getNextSiblingOfType(curItem, XmlTag.class);
297     }
298
299     return addItemTag(rootTag, prevItem, ownerName, annotation);
300   }
301
302   /**
303    * Adds annotation sub tag into curItem or between prevItem and curItem.
304    * Adds into curItem if curItem contains external annotations for owner.
305    * Adds between curItem and prevItem if owner's external name < cur item owner external name.
306    * Otherwise does nothing, returns null.
307    *
308    * @param rootTag root tag to insert sub tag into
309    * @param ownerName annotation owner
310    * @param annotation external annotation
311    * @param curItem current item with annotations
312    * @param prevItem previous item with annotations
313    * @return added tag
314    */
315   @Nullable
316   private XmlTag addAnnotation(@NotNull XmlTag rootTag, @NotNull String ownerName, @NotNull ExternalAnnotation annotation,
317                                @NotNull XmlTag curItem, @Nullable XmlTag prevItem) {
318
319     @NonNls String curItemName = curItem.getAttributeValue("name");
320     if (curItemName == null) {
321       curItem.delete();
322       return null;
323     }
324
325     int compare = ownerName.compareTo(curItemName);
326
327     if (compare == 0) {
328       //already have external annotations for owner
329       return appendItemAnnotation(curItem, annotation);
330     }
331
332     if (compare < 0) {
333       return addItemTag(rootTag, prevItem, ownerName, annotation);
334     }
335
336     return null;
337   }
338
339   @NotNull
340   private XmlTag addItemTag(@NotNull XmlTag rootTag,
341                             @Nullable XmlTag anchor,
342                             @NotNull String ownerName,
343                             @NotNull ExternalAnnotation annotation) {
344     XmlElementFactory elementFactory = XmlElementFactory.getInstance(myPsiManager.getProject());
345     XmlTag newItemTag = elementFactory.createTagFromText(createItemTag(ownerName, annotation));
346
347     PsiElement addedElement;
348     if (anchor != null) {
349       addedElement = rootTag.addAfter(newItemTag, anchor);
350     }
351     else {
352       addedElement = rootTag.addSubTag(newItemTag, true);
353     }
354
355     if (!(addedElement instanceof XmlTag)) {
356       throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
357     }
358
359     return (XmlTag)addedElement;
360   }
361
362   /**
363    * Appends annotation sub tag into itemTag. It can happen only if item tag belongs to annotation owner.
364    *
365    * @param itemTag item tag with annotations
366    * @param annotation external annotation
367    */
368   private XmlTag appendItemAnnotation(@NotNull XmlTag itemTag, @NotNull ExternalAnnotation annotation) {
369     @NonNls String annotationFQName = annotation.getAnnotationFQName();
370     PsiNameValuePair[] values = annotation.getValues();
371
372     XmlElementFactory elementFactory = XmlElementFactory.getInstance(myPsiManager.getProject());
373
374     XmlTag anchor = null;
375     for (XmlTag itemAnnotation : itemTag.getSubTags()) {
376       String curAnnotationName = itemAnnotation.getAttributeValue("name");
377       if (curAnnotationName == null) {
378         itemAnnotation.delete();
379         continue;
380       }
381
382       if (annotationFQName.equals(curAnnotationName)) {
383         // found tag for same annotation, replacing
384         itemAnnotation.delete();
385         break;
386       }
387
388       anchor = itemAnnotation;
389     }
390
391     XmlTag newAnnotationTag = elementFactory.createTagFromText(createAnnotationTag(annotationFQName, values));
392
393     PsiElement addedElement = itemTag.addAfter(newAnnotationTag, anchor);
394     if (!(addedElement instanceof XmlTag)) {
395       throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
396     }
397
398     return itemTag;
399   }
400
401   @Nullable
402   private List<XmlFile> findExternalAnnotationsXmlFiles(@NotNull PsiModifierListOwner listOwner) {
403     List<PsiFile> psiFiles = findExternalAnnotationsFiles(listOwner);
404     if (psiFiles == null) {
405       return null;
406     }
407     List<XmlFile> xmlFiles = new ArrayList<>();
408     for (PsiFile psiFile : psiFiles) {
409       if (psiFile instanceof XmlFile) {
410         xmlFiles.add((XmlFile)psiFile);
411       }
412     }
413     return xmlFiles;
414   }
415
416   private boolean setupRootAndAnnotateExternally(@NotNull final OrderEntry entry,
417                                                  @NotNull final Project project,
418                                                  @NotNull final ExternalAnnotation annotation) {
419     final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
420     descriptor.setTitle(ProjectBundle.message("external.annotations.root.chooser.title", entry.getPresentableName()));
421     descriptor.setDescription(ProjectBundle.message("external.annotations.root.chooser.description"));
422     final VirtualFile newRoot = FileChooser.chooseFile(descriptor, project, null);
423     if (newRoot == null) {
424       notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
425       return false;
426     }
427     WriteCommandAction.writeCommandAction(project).run(() -> appendChosenAnnotationsRoot(entry, newRoot));
428     annotateExternally(newRoot, annotation);
429     return true;
430   }
431
432   @Nullable
433   private static XmlFile findXmlFileInRoot(@Nullable List<? extends XmlFile> xmlFiles, @NotNull VirtualFile root) {
434     if (xmlFiles != null) {
435       for (XmlFile xmlFile : xmlFiles) {
436         VirtualFile vf = xmlFile.getVirtualFile();
437         if (vf != null) {
438           if (VfsUtilCore.isAncestor(root, vf, false)) {
439             return xmlFile;
440           }
441         }
442       }
443     }
444     return null;
445   }
446
447   private void chooseRootAndAnnotateExternally(@NotNull VirtualFile[] roots, @NotNull ExternalAnnotation annotation) {
448     if (roots.length > 1) {
449       JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<VirtualFile>("Annotation Roots", roots) {
450         @Override
451         public void canceled() {
452           notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
453         }
454
455         @Override
456         public PopupStep onChosen(@NotNull final VirtualFile file, final boolean finalChoice) {
457           annotateExternally(file, annotation);
458           return FINAL_CHOICE;
459         }
460
461         @NotNull
462         @Override
463         public String getTextFor(@NotNull final VirtualFile value) {
464           return value.getPresentableUrl();
465         }
466
467         @Override
468         public Icon getIconFor(final VirtualFile aValue) {
469           return AllIcons.Modules.Annotation;
470         }
471       }).showInBestPositionFor(DataManager.getInstance().getDataContext());
472     }
473     else {
474       annotateExternally(roots[0], annotation);
475     }
476   }
477
478   @NotNull
479   private static VirtualFile[] filterByReadOnliness(@NotNull VirtualFile[] files) {
480     List<VirtualFile> result = ContainerUtil.filter(files, VirtualFile::isInLocalFileSystem);
481     return VfsUtilCore.toVirtualFileArray(result);
482   }
483
484   @Override
485   public boolean deannotate(@NotNull final PsiModifierListOwner listOwner, @NotNull final String annotationFQN) {
486     ApplicationManager.getApplication().assertIsDispatchThread();
487     return processExistingExternalAnnotations(listOwner, annotationFQN, annotationTag -> {
488       PsiElement parent = annotationTag.getParent();
489       annotationTag.delete();
490       if (parent instanceof XmlTag) {
491         if (((XmlTag)parent).getSubTags().length == 0) {
492           parent.delete();
493         }
494       }
495       return true;
496     });
497   }
498
499   @Override
500   public void elementRenamedOrMoved(@NotNull PsiModifierListOwner element, @NotNull String oldExternalName) {
501     ApplicationManager.getApplication().assertIsDispatchThread();
502     try {
503       final List<XmlFile> files = findExternalAnnotationsXmlFiles(element);
504       if (files == null) {
505         return;
506       }
507       for (final XmlFile file : files) {
508         if (!file.isValid()) {
509           continue;
510         }
511         final XmlDocument document = file.getDocument();
512         if (document == null) {
513           continue;
514         }
515         final XmlTag rootTag = document.getRootTag();
516         if (rootTag == null) {
517           continue;
518         }
519
520         for (XmlTag tag : rootTag.getSubTags()) {
521           String nameValue = tag.getAttributeValue("name");
522           String className = nameValue == null ? null : StringUtil.unescapeXmlEntities(nameValue);
523           if (Comparing.strEqual(className, oldExternalName)) {
524             WriteCommandAction
525               .runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
526                 PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
527                 try {
528                   String name = getExternalName(element);
529                   tag.setAttribute("name", name == null ? null : StringUtil.escapeXmlEntities(name));
530                   commitChanges(file);
531                 }
532                 catch (IncorrectOperationException e) {
533                   LOG.error(e);
534                 }
535               }, file);
536           }
537         }
538       }
539     }
540     finally {
541       dropCache();
542     }
543   }
544
545
546   @Override
547   public boolean editExternalAnnotation(@NotNull PsiModifierListOwner listOwner,
548                                         @NotNull final String annotationFQN,
549                                         @Nullable final PsiNameValuePair[] value) {
550     ApplicationManager.getApplication().assertIsDispatchThread();
551     return processExistingExternalAnnotations(listOwner, annotationFQN, annotationTag -> {
552       annotationTag.replace(XmlElementFactory.getInstance(myPsiManager.getProject()).createTagFromText(
553         createAnnotationTag(annotationFQN, value)));
554       return true;
555     });
556   }
557
558   private boolean processExistingExternalAnnotations(@NotNull final PsiModifierListOwner listOwner,
559                                                      @NotNull final String annotationFQN,
560                                                      @NotNull final Processor<? super XmlTag> annotationTagProcessor) {
561     try {
562       final List<XmlFile> files = findExternalAnnotationsXmlFiles(listOwner);
563       if (files == null) {
564         notifyAfterAnnotationChanging(listOwner, annotationFQN, false);
565         return false;
566       }
567       boolean processedAnything = false;
568       for (final XmlFile file : files) {
569         if (!file.isValid()) {
570           continue;
571         }
572         if (ReadonlyStatusHandler.getInstance(myPsiManager.getProject())
573           .ensureFilesWritable(Collections.singletonList(file.getVirtualFile())).hasReadonlyFiles()) {
574           continue;
575         }
576         final XmlDocument document = file.getDocument();
577         if (document == null) {
578           continue;
579         }
580         final XmlTag rootTag = document.getRootTag();
581         if (rootTag == null) {
582           continue;
583         }
584         final String externalName = getExternalName(listOwner);
585
586         final List<XmlTag> tagsToProcess = new ArrayList<>();
587         for (XmlTag tag : rootTag.getSubTags()) {
588           String nameValue = tag.getAttributeValue("name");
589           String className = nameValue == null ? null : StringUtil.unescapeXmlEntities(nameValue);
590           if (!Comparing.strEqual(className, externalName)) {
591             continue;
592           }
593           for (XmlTag annotationTag : tag.getSubTags()) {
594             if (!Comparing.strEqual(annotationTag.getAttributeValue("name"), annotationFQN)) {
595               continue;
596             }
597             tagsToProcess.add(annotationTag);
598             processedAnything = true;
599           }
600         }
601         if (tagsToProcess.isEmpty()) {
602           continue;
603         }
604
605         WriteCommandAction.runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
606           PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
607           try {
608             for (XmlTag annotationTag : tagsToProcess) {
609               annotationTagProcessor.process(annotationTag);
610             }
611             commitChanges(file);
612           }
613           catch (IncorrectOperationException e) {
614             LOG.error(e);
615           }
616         });
617       }
618       notifyAfterAnnotationChanging(listOwner, annotationFQN, processedAnything);
619       return processedAnything;
620     }
621     finally {
622       dropCache();
623     }
624   }
625
626   @Override
627   @NotNull
628   public AnnotationPlace chooseAnnotationsPlace(@NotNull final PsiElement element) {
629     ApplicationManager.getApplication().assertIsDispatchThread();
630     if (!element.isPhysical() && !(element.getOriginalElement() instanceof PsiCompiledElement)) return AnnotationPlace.IN_CODE; //element just created
631     if (!element.getManager().isInProject(element)) return AnnotationPlace.EXTERNAL;
632     final Project project = myPsiManager.getProject();
633
634     //choose external place iff USE_EXTERNAL_ANNOTATIONS option is on,
635     //otherwise external annotations should be read-only
636     final PsiFile containingFile = element.getContainingFile();
637     if (JavaCodeStyleSettings.getInstance(containingFile).USE_EXTERNAL_ANNOTATIONS) {
638       final VirtualFile virtualFile = containingFile.getVirtualFile();
639       LOG.assertTrue(virtualFile != null);
640       final List<OrderEntry> entries = ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(virtualFile);
641       if (!entries.isEmpty()) {
642         for (OrderEntry entry : entries) {
643           if (!(entry instanceof ModuleOrderEntry)) {
644             if (AnnotationOrderRootType.getUrls(entry).length > 0) {
645               return AnnotationPlace.EXTERNAL;
646             }
647             break;
648           }
649         }
650       }
651
652       final MyExternalPromptDialog dialog = ApplicationManager.getApplication().isUnitTestMode() ||
653                                             ApplicationManager.getApplication().isHeadlessEnvironment() ? null : new MyExternalPromptDialog(project);
654       if (dialog != null && dialog.isToBeShown()) {
655         final PsiElement highlightElement = element instanceof PsiNameIdentifierOwner
656                                             ? ((PsiNameIdentifierOwner)element).getNameIdentifier()
657                                             : element.getNavigationElement();
658         LOG.assertTrue(highlightElement != null);
659         final Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
660         final List<RangeHighlighter> highlighters = new ArrayList<>();
661         final boolean highlight =
662           editor != null && editor.getDocument() == PsiDocumentManager.getInstance(project).getDocument(containingFile);
663         try {
664           if (highlight) { //do not highlight for batch inspections
665             final EditorColorsManager colorsManager = EditorColorsManager.getInstance();
666             final TextAttributes attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
667             final TextRange textRange = highlightElement.getTextRange();
668             HighlightManager.getInstance(project).addRangeHighlight(editor,
669                                                                     textRange.getStartOffset(), textRange.getEndOffset(),
670                                                                     attributes, true, highlighters);
671             final LogicalPosition logicalPosition = editor.offsetToLogicalPosition(textRange.getStartOffset());
672             editor.getScrollingModel().scrollTo(logicalPosition, ScrollType.CENTER);
673           }
674
675           dialog.show();
676           if (dialog.getExitCode() == 2) {
677             return AnnotationPlace.EXTERNAL;
678           }
679           else if (dialog.getExitCode() == 1) {
680             return AnnotationPlace.NOWHERE;
681           }
682
683         }
684         finally {
685           if (highlight) {
686             HighlightManager.getInstance(project).removeSegmentHighlighter(editor, highlighters.get(0));
687           }
688         }
689       }
690       else if (dialog != null) {
691         dialog.close(DialogWrapper.OK_EXIT_CODE);
692       }
693     }
694     return AnnotationPlace.IN_CODE;
695   }
696
697   private void appendChosenAnnotationsRoot(@NotNull final OrderEntry entry, @NotNull final VirtualFile vFile) {
698     if (entry instanceof LibraryOrderEntry) {
699       Library library = ((LibraryOrderEntry)entry).getLibrary();
700       LOG.assertTrue(library != null);
701       final Library.ModifiableModel model = library.getModifiableModel();
702       model.addRoot(vFile, AnnotationOrderRootType.getInstance());
703       model.commit();
704     }
705     else if (entry instanceof ModuleSourceOrderEntry) {
706       final ModifiableRootModel model = ModuleRootManager.getInstance(entry.getOwnerModule()).getModifiableModel();
707       final JavaModuleExternalPaths extension = model.getModuleExtension(JavaModuleExternalPaths.class);
708       extension.setExternalAnnotationUrls(ArrayUtil.mergeArrays(extension.getExternalAnnotationsUrls(), vFile.getUrl()));
709       model.commit();
710     }
711     else if (entry instanceof JdkOrderEntry) {
712       final SdkModificator sdkModificator = ((JdkOrderEntry)entry).getJdk().getSdkModificator();
713       sdkModificator.addRoot(vFile, AnnotationOrderRootType.getInstance());
714       sdkModificator.commitChanges();
715     }
716     dropCache();
717   }
718
719   private static void sortItems(@NotNull XmlFile xmlFile) {
720     XmlDocument document = xmlFile.getDocument();
721     if (document == null) {
722       return;
723     }
724     XmlTag rootTag = document.getRootTag();
725     if (rootTag == null) {
726       return;
727     }
728
729     List<XmlTag> itemTags = new ArrayList<>();
730     for (XmlTag item : rootTag.getSubTags()) {
731       if (item.getAttributeValue("name") != null) {
732         itemTags.add(item);
733       }
734       else {
735         item.delete();
736       }
737     }
738
739     List<XmlTag> sorted = new ArrayList<>(itemTags);
740     Collections.sort(sorted, (item1, item2) -> {
741       String externalName1 = item1.getAttributeValue("name");
742       String externalName2 = item2.getAttributeValue("name");
743       assert externalName1 != null && externalName2 != null; // null names were not added
744       return externalName1.compareTo(externalName2);
745     });
746     if (!sorted.equals(itemTags)) {
747       for (XmlTag item : sorted) {
748         rootTag.addAfter(item, null);
749         item.delete();
750       }
751     }
752   }
753
754   private void commitChanges(XmlFile xmlFile) {
755     sortItems(xmlFile);
756     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
757     Document doc = documentManager.getDocument(xmlFile);
758     assert doc != null;
759     documentManager.doPostponedOperationsAndUnblockDocument(doc);
760     FileDocumentManager.getInstance().saveDocument(doc);
761   }
762
763   @NonNls
764   @NotNull
765   private static String createItemTag(@NotNull String ownerName, @NotNull ExternalAnnotation annotation) {
766     String annotationTag = createAnnotationTag(annotation.getAnnotationFQName(), annotation.getValues());
767     return String.format("<item name=\'%s\'>%s</item>", ownerName, annotationTag);
768   }
769
770   @NonNls
771   @NotNull
772   @VisibleForTesting
773   public static String createAnnotationTag(@NotNull String annotationFQName, @Nullable PsiNameValuePair[] values) {
774     @NonNls String text;
775     if (values != null && values.length != 0) {
776       text = "  <annotation name=\'" + annotationFQName + "\'>\n";
777       text += StringUtil.join(values, pair -> "<val" +
778                                               (pair.getName() != null ? " name=\"" + pair.getName() + "\"" : "") +
779                                               " val=\"" + StringUtil.escapeXmlEntities(pair.getValue().getText()) + "\"/>", "    \n");
780       text += "  </annotation>";
781     }
782     else {
783       text = "  <annotation name=\'" + annotationFQName + "\'/>\n";
784     }
785     return text;
786   }
787
788   @Nullable
789   private XmlFile createAnnotationsXml(@NotNull VirtualFile root, @NonNls @NotNull String packageName) {
790     return createAnnotationsXml(root, packageName, myPsiManager);
791   }
792
793   @Nullable
794   @VisibleForTesting
795   public static XmlFile createAnnotationsXml(@NotNull VirtualFile root, @NonNls @NotNull String packageName, PsiManager manager) {
796     final String[] dirs = packageName.split("\\.");
797     for (String dir : dirs) {
798       if (dir.isEmpty()) break;
799       VirtualFile subdir = root.findChild(dir);
800       if (subdir == null) {
801         try {
802           subdir = root.createChildDirectory(null, dir);
803         }
804         catch (IOException e) {
805           LOG.error(e);
806           return null;
807         }
808       }
809       root = subdir;
810     }
811     final PsiDirectory directory = manager.findDirectory(root);
812     if (directory == null) return null;
813
814     final PsiFile psiFile = directory.findFile(ANNOTATIONS_XML);
815     if (psiFile instanceof XmlFile) {
816       return (XmlFile)psiFile;
817     }
818
819     try {
820       final PsiFileFactory factory = PsiFileFactory.getInstance(manager.getProject());
821       return (XmlFile)directory.add(factory.createFileFromText(ANNOTATIONS_XML, XmlFileType.INSTANCE, "<root></root>"));
822     }
823     catch (IncorrectOperationException e) {
824       LOG.error(e);
825     }
826     return null;
827   }
828
829   @Nullable
830   private XmlFile getFileForAnnotations(@NotNull VirtualFile root, @NotNull PsiModifierListOwner owner, Project project) {
831     return WriteCommandAction.writeCommandAction(project).compute(() -> {
832       final PsiFile containingFile = owner.getOriginalElement().getContainingFile();
833       if (!(containingFile instanceof PsiJavaFile)) {
834         return null;
835       }
836       String packageName = ((PsiJavaFile)containingFile).getPackageName();
837
838       List<XmlFile> annotationsFiles = findExternalAnnotationsXmlFiles(owner);
839
840       XmlFile fileInRoot = findXmlFileInRoot(annotationsFiles, root);
841       if (fileInRoot != null && FileModificationService.getInstance().preparePsiElementForWrite(fileInRoot)) {
842         return fileInRoot;
843       }
844
845         XmlFile newAnnotationsFile = createAnnotationsXml(root, packageName);
846         if (newAnnotationsFile == null) {
847           return null;
848         }
849
850       registerExternalAnnotations(containingFile, newAnnotationsFile);
851       return newAnnotationsFile;
852     });
853   }
854
855   @Override
856   public boolean hasAnnotationRootsForFile(@NotNull VirtualFile file) {
857     if (hasAnyAnnotationsRoots()) {
858       ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myPsiManager.getProject()).getFileIndex();
859       for (OrderEntry entry : fileIndex.getOrderEntriesForFile(file)) {
860         if (!(entry instanceof ModuleOrderEntry) && AnnotationOrderRootType.getUrls(entry).length > 0) {
861           return true;
862         }
863       }
864     }
865     return false;
866   }
867
868   @Override
869   protected void duplicateError(@NotNull PsiFile file, @NotNull String externalName, @NotNull String text) {
870     String message = text + "; for signature: '" + externalName + "' in the file " + file.getName();
871     LOG.error(message, new Throwable(), AttachmentFactory.createAttachment(file.getVirtualFile()));
872   }
873
874   public static boolean areExternalAnnotationsApplicable(@NotNull PsiModifierListOwner owner) {
875     if (!owner.isPhysical()) {
876       PsiElement originalElement = owner.getOriginalElement();
877       if (!(originalElement instanceof PsiCompiledElement)) {
878         return false;
879       }
880     }
881     if (owner instanceof PsiLocalVariable) return false;
882     if (owner instanceof PsiParameter) {
883       PsiElement parent = owner.getParent();
884       if (parent == null || !(parent.getParent() instanceof PsiMethod)) return false;
885     }
886     if (!owner.getManager().isInProject(owner)) return true;
887     return JavaCodeStyleSettings.getInstance(owner.getContainingFile()).USE_EXTERNAL_ANNOTATIONS;
888   }
889
890   private static class MyExternalPromptDialog extends OptionsMessageDialog {
891     private final Project myProject;
892     private static final String ADD_IN_CODE = ProjectBundle.message("external.annotations.in.code.option");
893     private static final String MESSAGE = ProjectBundle.message("external.annotations.suggestion.message");
894
895     MyExternalPromptDialog(final Project project) {
896       super(project, MESSAGE, ProjectBundle.message("external.annotation.prompt"), Messages.getQuestionIcon());
897       myProject = project;
898       init();
899     }
900
901     @Override
902     protected String getOkActionName() {
903       return ADD_IN_CODE;
904     }
905
906     @Override
907     @NotNull
908     protected String getCancelActionName() {
909       return CommonBundle.getCancelButtonText();
910     }
911
912     @Override
913     @NotNull
914     protected Action[] createActions() {
915       final Action okAction = getOKAction();
916       assignMnemonic(ADD_IN_CODE, okAction);
917       final String externalName = ProjectBundle.message("external.annotations.external.option");
918       return new Action[]{okAction, new AbstractAction(externalName) {
919         {
920           assignMnemonic(externalName, this);
921         }
922
923         @Override
924         public void actionPerformed(final ActionEvent e) {
925           if (canBeHidden()) {
926             setToBeShown(toBeShown(), true);
927           }
928           close(2);
929         }
930       }, getCancelAction()};
931     }
932
933     @Override
934     protected boolean isToBeShown() {
935       return CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS;
936     }
937
938     @Override
939     protected void setToBeShown(boolean value, boolean onOk) {
940       CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS = value;
941     }
942
943     @NotNull
944     @Override
945     protected JComponent createNorthPanel() {
946       final JPanel northPanel = (JPanel)super.createNorthPanel();
947       northPanel.add(new JLabel(MESSAGE), BorderLayout.CENTER);
948       return northPanel;
949     }
950
951     @Override
952     protected boolean shouldSaveOptionsOnCancel() {
953       return true;
954     }
955   }
956
957   private class MyVirtualFileListener implements VirtualFileListener {
958     private void processEvent(VirtualFileEvent event) {
959       if (event.isFromRefresh() && ANNOTATIONS_XML.equals(event.getFileName())) {
960         dropCache();
961         notifyChangedExternally();
962       }
963     }
964
965     @Override
966     public void contentsChanged(@NotNull VirtualFileEvent event) {
967       processEvent(event);
968     }
969
970     @Override
971     public void fileCreated(@NotNull VirtualFileEvent event) {
972       processEvent(event);
973     }
974
975     @Override
976     public void fileDeleted(@NotNull VirtualFileEvent event) {
977       processEvent(event);
978     }
979
980     @Override
981     public void fileMoved(@NotNull VirtualFileMoveEvent event) {
982       processEvent(event);
983     }
984
985     @Override
986     public void fileCopied(@NotNull VirtualFileCopyEvent event) {
987       processEvent(event);
988     }
989   }
990
991   private class MyDocumentListener implements DocumentListener {
992
993     final FileDocumentManager myFileDocumentManager = FileDocumentManager.getInstance();
994
995     @Override
996     public void documentChanged(@NotNull DocumentEvent event) {
997       final VirtualFile file = myFileDocumentManager.getFile(event.getDocument());
998       if (file != null && ANNOTATIONS_XML.equals(file.getName()) && isUnderAnnotationRoot(file)) {
999         dropCache();
1000       }
1001     }
1002   }
1003 }