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;
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;
67 import java.awt.event.ActionEvent;
68 import java.io.IOException;
69 import java.util.List;
71 import java.util.function.Function;
72 import java.util.stream.Collectors;
77 public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsManager {
78 private static final Logger LOG = Logger.getInstance(ExternalAnnotationsManagerImpl.class);
80 private final MessageBus myBus;
82 public ExternalAnnotationsManagerImpl(@NotNull final Project project, final PsiManager psiManager) {
84 myBus = project.getMessageBus();
85 myBus.connect(project).subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
87 public void rootsChanged(@NotNull ModuleRootEvent event) {
92 VirtualFileManager.getInstance().addVirtualFileListener(new MyVirtualFileListener(), project);
93 EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new MyDocumentListener(), project);
96 private void notifyAfterAnnotationChanging(@NotNull PsiModifierListOwner owner, @NotNull String annotationFQName, boolean successful) {
97 myBus.syncPublisher(TOPIC).afterExternalAnnotationChanging(owner, annotationFQName, successful);
98 myPsiManager.dropPsiCaches();
101 private void notifyChangedExternally() {
102 myBus.syncPublisher(TOPIC).externalAnnotationsChangedExternally();
103 myPsiManager.dropPsiCaches();
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());
115 final Project project = myPsiManager.getProject();
116 final PsiFile containingFile = listOwner.getOriginalElement().getContainingFile();
117 if (!(containingFile instanceof PsiJavaFile)) {
118 notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
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);
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);
134 if (roots.length > 0) {
135 chooseRootAndAnnotateExternally(roots, annotation);
138 if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
139 notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
142 DumbService.getInstance(project).setAlternativeResolveEnabled(true);
144 if (!setupRootAndAnnotateExternally(entry, project, annotation)) {
145 throw new CanceledConfigurationException();
149 DumbService.getInstance(project).setAlternativeResolveEnabled(false);
156 private void annotateExternally(@NotNull VirtualFile root, @NotNull ExternalAnnotation annotation) {
157 annotateExternally(root, Collections.singletonList(annotation));
161 * Tries to add external annotations into given root if possible.
162 * Notifies about each addition result separately.
164 public void annotateExternally(@NotNull VirtualFile root, @NotNull List<? extends ExternalAnnotation> annotations) {
165 Project project = myPsiManager.getProject();
167 Map<Optional<XmlFile>, List<ExternalAnnotation>> annotationsByFiles = annotations.stream()
168 .collect(Collectors.groupingBy(annotation -> Optional.ofNullable(getFileForAnnotations(root, annotation.getOwner(), project))));
170 WriteCommandAction.writeCommandAction(project).run(() -> {
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);
178 UndoManager.getInstance(project).undoableActionPerformed(new BasicUndoableAction() {
182 notifyChangedExternally();
188 notifyChangedExternally();
197 private void annotateExternally(@Nullable XmlFile annotationsFile, @NotNull List<ExternalAnnotation> annotations) {
198 XmlTag rootTag = extractRootTag(annotationsFile);
200 TreeMap<String, List<ExternalAnnotation>> ownerToAnnotations = StreamEx.of(annotations)
201 .mapToEntry(annotation -> StringUtil.escapeXmlEntities(getExternalName(annotation.getOwner())), Function.identity())
203 .grouping(() -> new TreeMap<>(Comparator.nullsFirst(Comparator.naturalOrder())));
205 if (rootTag == null) {
206 ownerToAnnotations.values().stream().flatMap(List::stream).forEach(annotation ->
207 notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false));
211 List<ExternalAnnotation> savedAnnotations = new ArrayList<>();
212 XmlTag startTag = null;
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) {
219 if (ownerName == null) {
220 notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
225 startTag = addAnnotation(rootTag, ownerName, annotation, startTag);
226 savedAnnotations.add(annotation);
228 catch (IncorrectOperationException e) {
230 notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
234 markForUndo(annotation.getOwner().getContainingFile());
239 commitChanges(annotationsFile);
240 savedAnnotations.forEach(annotation ->
241 notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), true));
244 @Contract("null -> null")
245 private static XmlTag extractRootTag(XmlFile annotationsFile) {
246 if (annotationsFile == null) {
250 XmlDocument document = annotationsFile.getDocument();
251 if (document == null) {
255 return document.getRootTag();
258 private static void markForUndo(@Nullable PsiFile containingFile) {
259 if (containingFile == null) {
263 VirtualFile virtualFile = containingFile.getVirtualFile();
264 if (virtualFile != null && virtualFile.isInLocalFileSystem()) {
265 UndoUtil.markPsiFileForUndo(containingFile);
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.
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
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);
286 XmlTag prevItem = null;
287 XmlTag curItem = startTag;
289 while (curItem != null) {
290 XmlTag addedItem = addAnnotation(rootTag, ownerName, annotation, curItem, prevItem);
291 if (addedItem != null) {
296 curItem = PsiTreeUtil.getNextSiblingOfType(curItem, XmlTag.class);
299 return addItemTag(rootTag, prevItem, ownerName, annotation);
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.
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
316 private XmlTag addAnnotation(@NotNull XmlTag rootTag, @NotNull String ownerName, @NotNull ExternalAnnotation annotation,
317 @NotNull XmlTag curItem, @Nullable XmlTag prevItem) {
319 @NonNls String curItemName = curItem.getAttributeValue("name");
320 if (curItemName == null) {
325 int compare = ownerName.compareTo(curItemName);
328 //already have external annotations for owner
329 return appendItemAnnotation(curItem, annotation);
333 return addItemTag(rootTag, prevItem, ownerName, annotation);
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));
347 PsiElement addedElement;
348 if (anchor != null) {
349 addedElement = rootTag.addAfter(newItemTag, anchor);
352 addedElement = rootTag.addSubTag(newItemTag, true);
355 if (!(addedElement instanceof XmlTag)) {
356 throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
359 return (XmlTag)addedElement;
363 * Appends annotation sub tag into itemTag. It can happen only if item tag belongs to annotation owner.
365 * @param itemTag item tag with annotations
366 * @param annotation external annotation
368 private XmlTag appendItemAnnotation(@NotNull XmlTag itemTag, @NotNull ExternalAnnotation annotation) {
369 @NonNls String annotationFQName = annotation.getAnnotationFQName();
370 PsiNameValuePair[] values = annotation.getValues();
372 XmlElementFactory elementFactory = XmlElementFactory.getInstance(myPsiManager.getProject());
374 XmlTag anchor = null;
375 for (XmlTag itemAnnotation : itemTag.getSubTags()) {
376 String curAnnotationName = itemAnnotation.getAttributeValue("name");
377 if (curAnnotationName == null) {
378 itemAnnotation.delete();
382 if (annotationFQName.equals(curAnnotationName)) {
383 // found tag for same annotation, replacing
384 itemAnnotation.delete();
388 anchor = itemAnnotation;
391 XmlTag newAnnotationTag = elementFactory.createTagFromText(createAnnotationTag(annotationFQName, values));
393 PsiElement addedElement = itemTag.addAfter(newAnnotationTag, anchor);
394 if (!(addedElement instanceof XmlTag)) {
395 throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
402 private List<XmlFile> findExternalAnnotationsXmlFiles(@NotNull PsiModifierListOwner listOwner) {
403 List<PsiFile> psiFiles = findExternalAnnotationsFiles(listOwner);
404 if (psiFiles == null) {
407 List<XmlFile> xmlFiles = new ArrayList<>();
408 for (PsiFile psiFile : psiFiles) {
409 if (psiFile instanceof XmlFile) {
410 xmlFiles.add((XmlFile)psiFile);
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);
427 WriteCommandAction.writeCommandAction(project).run(() -> appendChosenAnnotationsRoot(entry, newRoot));
428 annotateExternally(newRoot, annotation);
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();
438 if (VfsUtilCore.isAncestor(root, vf, false)) {
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) {
451 public void canceled() {
452 notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
456 public PopupStep onChosen(@NotNull final VirtualFile file, final boolean finalChoice) {
457 annotateExternally(file, annotation);
463 public String getTextFor(@NotNull final VirtualFile value) {
464 return value.getPresentableUrl();
468 public Icon getIconFor(final VirtualFile aValue) {
469 return AllIcons.Modules.Annotation;
471 }).showInBestPositionFor(DataManager.getInstance().getDataContext());
474 annotateExternally(roots[0], annotation);
479 private static VirtualFile[] filterByReadOnliness(@NotNull VirtualFile[] files) {
480 List<VirtualFile> result = ContainerUtil.filter(files, VirtualFile::isInLocalFileSystem);
481 return VfsUtilCore.toVirtualFileArray(result);
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) {
500 public void elementRenamedOrMoved(@NotNull PsiModifierListOwner element, @NotNull String oldExternalName) {
501 ApplicationManager.getApplication().assertIsDispatchThread();
503 final List<XmlFile> files = findExternalAnnotationsXmlFiles(element);
507 for (final XmlFile file : files) {
508 if (!file.isValid()) {
511 final XmlDocument document = file.getDocument();
512 if (document == null) {
515 final XmlTag rootTag = document.getRootTag();
516 if (rootTag == null) {
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)) {
525 .runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
526 PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
528 String name = getExternalName(element);
529 tag.setAttribute("name", name == null ? null : StringUtil.escapeXmlEntities(name));
532 catch (IncorrectOperationException e) {
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)));
558 private boolean processExistingExternalAnnotations(@NotNull final PsiModifierListOwner listOwner,
559 @NotNull final String annotationFQN,
560 @NotNull final Processor<? super XmlTag> annotationTagProcessor) {
562 final List<XmlFile> files = findExternalAnnotationsXmlFiles(listOwner);
564 notifyAfterAnnotationChanging(listOwner, annotationFQN, false);
567 boolean processedAnything = false;
568 for (final XmlFile file : files) {
569 if (!file.isValid()) {
572 if (ReadonlyStatusHandler.getInstance(myPsiManager.getProject())
573 .ensureFilesWritable(Collections.singletonList(file.getVirtualFile())).hasReadonlyFiles()) {
576 final XmlDocument document = file.getDocument();
577 if (document == null) {
580 final XmlTag rootTag = document.getRootTag();
581 if (rootTag == null) {
584 final String externalName = getExternalName(listOwner);
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)) {
593 for (XmlTag annotationTag : tag.getSubTags()) {
594 if (!Comparing.strEqual(annotationTag.getAttributeValue("name"), annotationFQN)) {
597 tagsToProcess.add(annotationTag);
598 processedAnything = true;
601 if (tagsToProcess.isEmpty()) {
605 WriteCommandAction.runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
606 PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
608 for (XmlTag annotationTag : tagsToProcess) {
609 annotationTagProcessor.process(annotationTag);
613 catch (IncorrectOperationException e) {
618 notifyAfterAnnotationChanging(listOwner, annotationFQN, processedAnything);
619 return processedAnything;
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();
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;
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);
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);
676 if (dialog.getExitCode() == 2) {
677 return AnnotationPlace.EXTERNAL;
679 else if (dialog.getExitCode() == 1) {
680 return AnnotationPlace.NOWHERE;
686 HighlightManager.getInstance(project).removeSegmentHighlighter(editor, highlighters.get(0));
690 else if (dialog != null) {
691 dialog.close(DialogWrapper.OK_EXIT_CODE);
694 return AnnotationPlace.IN_CODE;
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());
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()));
711 else if (entry instanceof JdkOrderEntry) {
712 final SdkModificator sdkModificator = ((JdkOrderEntry)entry).getJdk().getSdkModificator();
713 sdkModificator.addRoot(vFile, AnnotationOrderRootType.getInstance());
714 sdkModificator.commitChanges();
719 private static void sortItems(@NotNull XmlFile xmlFile) {
720 XmlDocument document = xmlFile.getDocument();
721 if (document == null) {
724 XmlTag rootTag = document.getRootTag();
725 if (rootTag == null) {
729 List<XmlTag> itemTags = new ArrayList<>();
730 for (XmlTag item : rootTag.getSubTags()) {
731 if (item.getAttributeValue("name") != null) {
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);
746 if (!sorted.equals(itemTags)) {
747 for (XmlTag item : sorted) {
748 rootTag.addAfter(item, null);
754 private void commitChanges(XmlFile xmlFile) {
756 PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
757 Document doc = documentManager.getDocument(xmlFile);
759 documentManager.doPostponedOperationsAndUnblockDocument(doc);
760 FileDocumentManager.getInstance().saveDocument(doc);
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);
773 public static String createAnnotationTag(@NotNull String annotationFQName, @Nullable PsiNameValuePair[] values) {
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>";
783 text = " <annotation name=\'" + annotationFQName + "\'/>\n";
789 private XmlFile createAnnotationsXml(@NotNull VirtualFile root, @NonNls @NotNull String packageName) {
790 return createAnnotationsXml(root, packageName, myPsiManager);
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) {
802 subdir = root.createChildDirectory(null, dir);
804 catch (IOException e) {
811 final PsiDirectory directory = manager.findDirectory(root);
812 if (directory == null) return null;
814 final PsiFile psiFile = directory.findFile(ANNOTATIONS_XML);
815 if (psiFile instanceof XmlFile) {
816 return (XmlFile)psiFile;
820 final PsiFileFactory factory = PsiFileFactory.getInstance(manager.getProject());
821 return (XmlFile)directory.add(factory.createFileFromText(ANNOTATIONS_XML, XmlFileType.INSTANCE, "<root></root>"));
823 catch (IncorrectOperationException e) {
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)) {
836 String packageName = ((PsiJavaFile)containingFile).getPackageName();
838 List<XmlFile> annotationsFiles = findExternalAnnotationsXmlFiles(owner);
840 XmlFile fileInRoot = findXmlFileInRoot(annotationsFiles, root);
841 if (fileInRoot != null && FileModificationService.getInstance().preparePsiElementForWrite(fileInRoot)) {
845 XmlFile newAnnotationsFile = createAnnotationsXml(root, packageName);
846 if (newAnnotationsFile == null) {
850 registerExternalAnnotations(containingFile, newAnnotationsFile);
851 return newAnnotationsFile;
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) {
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()));
874 public static boolean areExternalAnnotationsApplicable(@NotNull PsiModifierListOwner owner) {
875 if (!owner.isPhysical()) {
876 PsiElement originalElement = owner.getOriginalElement();
877 if (!(originalElement instanceof PsiCompiledElement)) {
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;
886 if (!owner.getManager().isInProject(owner)) return true;
887 return JavaCodeStyleSettings.getInstance(owner.getContainingFile()).USE_EXTERNAL_ANNOTATIONS;
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");
895 MyExternalPromptDialog(final Project project) {
896 super(project, MESSAGE, ProjectBundle.message("external.annotation.prompt"), Messages.getQuestionIcon());
902 protected String getOkActionName() {
908 protected String getCancelActionName() {
909 return CommonBundle.getCancelButtonText();
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) {
920 assignMnemonic(externalName, this);
924 public void actionPerformed(final ActionEvent e) {
926 setToBeShown(toBeShown(), true);
930 }, getCancelAction()};
934 protected boolean isToBeShown() {
935 return CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS;
939 protected void setToBeShown(boolean value, boolean onOk) {
940 CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS = value;
945 protected JComponent createNorthPanel() {
946 final JPanel northPanel = (JPanel)super.createNorthPanel();
947 northPanel.add(new JLabel(MESSAGE), BorderLayout.CENTER);
952 protected boolean shouldSaveOptionsOnCancel() {
957 private class MyVirtualFileListener implements VirtualFileListener {
958 private void processEvent(VirtualFileEvent event) {
959 if (event.isFromRefresh() && ANNOTATIONS_XML.equals(event.getFileName())) {
961 notifyChangedExternally();
966 public void contentsChanged(@NotNull VirtualFileEvent event) {
971 public void fileCreated(@NotNull VirtualFileEvent event) {
976 public void fileDeleted(@NotNull VirtualFileEvent event) {
981 public void fileMoved(@NotNull VirtualFileMoveEvent event) {
986 public void fileCopied(@NotNull VirtualFileCopyEvent event) {
991 private class MyDocumentListener implements DocumentListener {
993 final FileDocumentManager myFileDocumentManager = FileDocumentManager.getInstance();
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)) {