076a77fd1e7143dbaefc423d738a8b56c4858bfe
[idea/community.git] / java / idea-ui / src / com / intellij / jarRepository / RepositoryAttachDialog.java
1 // Copyright 2000-2020 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.jarRepository;
3
4 import com.intellij.icons.AllIcons;
5 import com.intellij.ide.JavaUiBundle;
6 import com.intellij.ide.util.PropertiesComponent;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
9 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
10 import com.intellij.openapi.fileChooser.FileChooserDialog;
11 import com.intellij.openapi.project.Project;
12 import com.intellij.openapi.ui.DialogWrapper;
13 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
14 import com.intellij.openapi.ui.ValidationInfo;
15 import com.intellij.openapi.util.Comparing;
16 import com.intellij.openapi.util.Disposer;
17 import com.intellij.openapi.util.NlsSafe;
18 import com.intellij.openapi.util.Pair;
19 import com.intellij.openapi.util.io.FileUtil;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.util.text.Strings;
22 import com.intellij.openapi.vfs.VirtualFile;
23 import com.intellij.ui.CollectionComboBoxModel;
24 import com.intellij.ui.ComboboxWithBrowseButton;
25 import com.intellij.ui.DocumentAdapter;
26 import com.intellij.ui.components.JBCheckBox;
27 import com.intellij.ui.components.JBLabel;
28 import com.intellij.util.ui.AsyncProcessIcon;
29 import com.intellij.xml.util.XmlStringUtil;
30 import gnu.trove.THashMap;
31 import org.eclipse.aether.version.InvalidVersionSpecificationException;
32 import org.eclipse.aether.version.Version;
33 import org.jetbrains.annotations.Nls;
34 import org.jetbrains.annotations.NonNls;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37 import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager;
38 import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor;
39 import org.w3c.dom.Document;
40 import org.w3c.dom.Node;
41 import org.w3c.dom.NodeList;
42 import org.xml.sax.InputSource;
43 import org.xml.sax.SAXException;
44
45 import javax.swing.*;
46 import javax.swing.event.DocumentEvent;
47 import javax.swing.text.JTextComponent;
48 import javax.xml.parsers.DocumentBuilder;
49 import javax.xml.parsers.DocumentBuilderFactory;
50 import javax.xml.parsers.ParserConfigurationException;
51 import java.awt.*;
52 import java.awt.event.ActionEvent;
53 import java.awt.event.ActionListener;
54 import java.io.File;
55 import java.io.IOException;
56 import java.io.StringReader;
57 import java.util.List;
58 import java.util.*;
59
60 public class RepositoryAttachDialog extends DialogWrapper {
61   @NonNls private static final String PROPERTY_DOWNLOAD_TO_PATH = "Downloaded.Files.Path";
62   @NonNls private static final String PROPERTY_DOWNLOAD_TO_PATH_ENABLED = "Downloaded.Files.Path.Enabled";
63   @NonNls private static final String PROPERTY_ATTACH_JAVADOC = "Repository.Attach.JavaDocs";
64   @NonNls private static final String PROPERTY_ATTACH_SOURCES = "Repository.Attach.Sources";
65   @NonNls private static final String PROPERTY_ATTACH_ANNOTATIONS = "Repository.Attach.Annotations";
66   @NotNull private final Mode myMode;
67
68   public enum Mode { SEARCH, DOWNLOAD }
69   private final Project myProject;
70
71   private JBLabel myInfoLabel;
72   private JCheckBox myJavaDocCheckBox;
73   private JCheckBox mySourcesCheckBox;
74   private AsyncProcessIcon myProgressIcon;
75   private ComboboxWithBrowseButton myComboComponent;
76   private JPanel myPanel;
77   private TextFieldWithBrowseButton myDirectoryField;
78   private JBCheckBox myDownloadToCheckBox;
79   private JBLabel myCaptionLabel;
80   private JPanel myDownloadOptionsPanel;
81   private JBCheckBox myIncludeTransitiveDepsCheckBox;
82   private JPanel mySearchOptionsPanel;
83   private JBCheckBox myIncludeTransitiveDependenciesForSearchCheckBox;
84   private JBCheckBox myAnnotationsCheckBox;
85
86   private final JComboBox myCombobox;
87
88   private final Map<String, RepositoryArtifactDescription> myCoordinates = new THashMap<>();
89   private final List<String> myShownItems = new ArrayList<>();
90   private final @NlsSafe String myDefaultDownloadFolder;
91
92   private String myFilterString;
93   private boolean myInUpdate;
94
95   public RepositoryAttachDialog(@NotNull Project project, final @Nullable String initialFilter, @NotNull Mode mode) {
96     super(project, true);
97     myMode = mode;
98     setTitle(mode == Mode.DOWNLOAD ? JavaUiBundle.message("dialog.title.download.library.from.maven.repository")
99                                    : JavaUiBundle.message("dialog.title.search.library.in.maven.repositories"));
100     myProject = project;
101     myProgressIcon.suspend();
102     @Nls final String text = JavaUiBundle.message("repository.attach.dialog.caption.label");
103     myCaptionLabel.setText(XmlStringUtil.wrapInHtml(StringUtil.escapeXmlEntities(text)));
104     myInfoLabel.setPreferredSize(
105       new Dimension(myInfoLabel.getFontMetrics(myInfoLabel.getFont()).stringWidth("Showing: 1000"), myInfoLabel.getPreferredSize().height));
106
107     myComboComponent.setButtonIcon(AllIcons.Actions.Find);
108     myComboComponent.getButton().addActionListener(new ActionListener() {
109       @Override
110       public void actionPerformed(ActionEvent e) {
111         performSearch();
112       }
113     });
114     myCombobox = myComboComponent.getComboBox();
115     myCombobox.setModel(new CollectionComboBoxModel(myShownItems, null));
116     myCombobox.setEditable(true);
117     final JTextField textField = (JTextField)myCombobox.getEditor().getEditorComponent();
118     textField.setColumns(20);
119     if (initialFilter != null) {
120       textField.setText(initialFilter);
121     }
122     textField.getDocument().addDocumentListener(new DocumentAdapter() {
123       @Override
124       protected void textChanged(@NotNull DocumentEvent e) {
125         ApplicationManager.getApplication().invokeLater(() -> {
126           if (myProgressIcon.isDisposed()) return;
127           ApplicationManager.getApplication().invokeLater(() -> {
128             if (myProgressIcon.isDisposed()) return;
129             handleMavenDependencyInsertion(e, textField);
130             updateComboboxSelection(false);
131           });
132
133           updateComboboxSelection(false);
134         });
135       }
136     });
137     myCombobox.addActionListener(new ActionListener() {
138       @Override
139       public void actionPerformed(ActionEvent e) {
140         final boolean popupVisible = myCombobox.isPopupVisible();
141         if (!myInUpdate && (!popupVisible || myCoordinates.isEmpty())) {
142           performSearch();
143         }
144         else {
145           final String item = (String)myCombobox.getSelectedItem();
146           if (StringUtil.isNotEmpty(item)) {
147             ((JTextField)myCombobox.getEditor().getEditorComponent()).setText(item);
148           }
149         }
150       }
151     });
152     VirtualFile baseDir = !myProject.isDefault() ? myProject.getBaseDir() : null;
153     myDefaultDownloadFolder = baseDir != null ? FileUtil.toSystemDependentName(baseDir.getPath() + "/lib") : "";
154
155     PropertiesComponent storage = PropertiesComponent.getInstance(myProject);
156     myDownloadToCheckBox.setSelected(storage.isTrueValue(PROPERTY_DOWNLOAD_TO_PATH_ENABLED));
157     myDirectoryField.setText(getDownloadPath(storage));
158     myDirectoryField.setEnabled(myDownloadToCheckBox.isSelected());
159     myDownloadToCheckBox.addActionListener(new ActionListener() {
160       @Override
161       public void actionPerformed(ActionEvent e) {
162         myDirectoryField.setEnabled(myDownloadToCheckBox.isSelected());
163       }
164     });
165     myJavaDocCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_JAVADOC));
166     mySourcesCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_SOURCES));
167     mySourcesCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_ANNOTATIONS));
168
169     final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
170     descriptor.putUserData(FileChooserDialog.PREFER_LAST_OVER_TO_SELECT, Boolean.TRUE);
171     myDirectoryField.addBrowseFolderListener(JavaUiBundle.message("file.chooser.directory.for.downloaded.libraries.title"),
172                                              JavaUiBundle.message("file.chooser.directory.for.downloaded.libraries.description"), null,
173                                              descriptor);
174     updateInfoLabel();
175     myDownloadOptionsPanel.setVisible(mode == Mode.DOWNLOAD);
176     mySearchOptionsPanel.setVisible(mode == Mode.SEARCH);
177     init();
178   }
179
180   private @NlsSafe String getDownloadPath(@NotNull final PropertiesComponent storage) {
181     final String value = storage.getValue(PROPERTY_DOWNLOAD_TO_PATH);
182     if (Strings.isNotEmpty(value)) return value;
183     return myDefaultDownloadFolder;
184   }
185
186   private static void handleMavenDependencyInsertion(DocumentEvent e, JTextField textField) {
187     if (e.getType() == DocumentEvent.EventType.INSERT) {
188       String text = textField.getText();
189       if (isMvnDependency(text)) {
190         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
191         factory.setValidating(false);
192         try {
193           DocumentBuilder builder = factory.newDocumentBuilder();
194           try {
195             Document document = builder.parse(new InputSource(new StringReader(text)));
196             String mavenCoordinates = extractMavenCoordinates(document);
197             if (mavenCoordinates != null) {
198               textField.setText(mavenCoordinates);
199             }
200           }
201           catch (SAXException | IOException ignored) {
202           }
203         }
204         catch (ParserConfigurationException ignored) {
205         }
206       }
207     }
208   }
209
210   public boolean getAttachJavaDoc() {
211     return myJavaDocCheckBox.isSelected();
212   }
213
214   public boolean getAttachSources() {
215     return mySourcesCheckBox.isSelected();
216   }
217
218   public boolean getAttachExternalAnnotations() {
219     return myAnnotationsCheckBox.isSelected();
220   }
221
222   public boolean getIncludeTransitiveDependencies() {
223     return myMode == Mode.DOWNLOAD ? myIncludeTransitiveDepsCheckBox.isSelected() : myIncludeTransitiveDependenciesForSearchCheckBox.isSelected();
224   }
225
226   @Nullable
227   public String getDirectoryPath() {
228     return myDownloadToCheckBox.isSelected()? myDirectoryField.getText() : null;
229   }
230
231   @Override
232   public JComponent getPreferredFocusedComponent() {
233     return myCombobox;
234   }
235
236   private void updateComboboxSelection(boolean force) {
237     final String prevFilter = myFilterString;
238     final JTextComponent field = (JTextComponent)myCombobox.getEditor().getEditorComponent();
239     final int caret = field.getCaretPosition();
240     myFilterString = field.getText();
241
242     if (!force && Objects.equals(myFilterString, prevFilter)) return;
243     int prevSize = myShownItems.size();
244     myShownItems.clear();
245
246     myInUpdate = true;
247     final boolean itemSelected = myCoordinates.containsKey(myFilterString) && Comparing.strEqual((String)myCombobox.getSelectedItem(), myFilterString, false);
248     final boolean filtered;
249     if (itemSelected) {
250       myShownItems.addAll(myCoordinates.keySet());
251       filtered = false;
252     }
253     else {
254       final String[] parts = myFilterString.split(" ");
255       main:
256       for (String coordinate : myCoordinates.keySet()) {
257         for (String part : parts) {
258           if (!StringUtil.containsIgnoreCase(coordinate, part)) {
259             continue main;
260           }
261         }
262         myShownItems.add(coordinate);
263       }
264       filtered = !myShownItems.isEmpty();
265       if (!filtered) {
266         myShownItems.addAll(myCoordinates.keySet());
267       }
268       myCombobox.setSelectedItem(null);
269     }
270
271     // use maven version sorter
272     ArrayList<LibItem> items = new ArrayList<>(myShownItems.size());
273     for (String coord : myShownItems) {
274       items.add(new LibItem(coord));
275     }
276     items.sort((o1, o2) -> Comparing.compare(o1, o2));
277     myShownItems.clear();
278     for (LibItem it : items) {
279       myShownItems.add(it.coord);
280     }
281
282     ((CollectionComboBoxModel)myCombobox.getModel()).update();
283     myInUpdate = false;
284     field.setText(myFilterString);
285     field.setCaretPosition(caret);
286     updateInfoLabel();
287     if (filtered) {
288       if (prevSize < 10 && myShownItems.size() > prevSize && myCombobox.isPopupVisible()) {
289         myCombobox.setPopupVisible(false);
290       }
291       if (!myCombobox.isPopupVisible()) {
292         myCombobox.setPopupVisible(true);
293       }
294     }
295   }
296
297   private static final class LibItem implements Comparable<LibItem> {
298     final String prefix;
299     final Version ver;
300     final String coord;
301
302     LibItem(String coord) {
303       this.coord = coord;
304       final JpsMavenRepositoryLibraryDescriptor desc = new JpsMavenRepositoryLibraryDescriptor(coord);
305       prefix = desc.getGroupId() + ":" + desc.getArtifactId();
306       Version ver = null;
307       try {
308         ver = ArtifactRepositoryManager.asVersion(desc.getVersion());
309       }
310       catch (InvalidVersionSpecificationException ignored) {
311       }
312       this.ver = ver;
313     }
314
315     @Override
316     public int compareTo(@NotNull LibItem that) {
317       final int prefixCompare = prefix.compareTo(that.prefix);
318       return prefixCompare != 0 ? prefixCompare : Comparing.compare(that.ver, ver);
319     }
320   }
321
322   private boolean performSearch() {
323     final String text = getCoordinateText();
324     if (myProgressIcon.isRunning() || StringUtil.isEmptyOrSpaces(text) || myCoordinates.containsKey(text)) {
325       return false;
326     }
327     myProgressIcon.resume();
328     JarRepositoryManager.searchArtifacts(myProject, text, (pairs) -> {
329       try {
330         if (myProgressIcon.isDisposed()) {
331           return;
332         }
333         myProgressIcon.suspend(); // finished
334         final int prevSize = myCoordinates.size();
335         for (Pair<RepositoryArtifactDescription, RemoteRepositoryDescription> pair : pairs) {
336           final RepositoryArtifactDescription artifact = pair.first;
337           myCoordinates.put(artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion(), artifact);
338         }
339         updateComboboxSelection(prevSize != myCoordinates.size());
340       }
341       finally {
342         setOKActionEnabled(true);
343       }
344     });
345     return true;
346   }
347
348   private void updateInfoLabel() {
349     myInfoLabel.setText(JavaUiBundle.message("info.text.found.0.br.showing.1", myCoordinates.size(), myCombobox.getModel().getSize()));
350   }
351
352   @Override
353   protected ValidationInfo doValidate() {
354     if (!isValidCoordinateSelected()) {
355       return new ValidationInfo(JavaUiBundle.message("error.message.please.enter.valid.coordinate.discover.it.or.select.one.from.the.list"), myCombobox);
356     }
357     else if (myDownloadToCheckBox.isSelected()) {
358       final File dir = new File(myDirectoryField.getText());
359       if (!dir.exists() && !dir.mkdirs() || !dir.isDirectory()) {
360         return new ValidationInfo(JavaUiBundle.message("error.message.please.enter.valid.library.files.path"), myDirectoryField.getTextField());
361       }
362     }
363     return super.doValidate();
364   }
365
366   @Override
367   protected JComponent createCenterPanel() {
368     return null;
369   }
370
371   @Override
372   protected JComponent createNorthPanel() {
373     return myPanel;
374   }
375
376
377   @Override
378   protected void dispose() {
379     Disposer.dispose(myProgressIcon);
380     PropertiesComponent storage = PropertiesComponent.getInstance(myProject);
381     storage.setValue(PROPERTY_DOWNLOAD_TO_PATH_ENABLED, String.valueOf(myDownloadToCheckBox.isSelected()));
382     String downloadPath = myDirectoryField.getText();
383     if (StringUtil.isEmptyOrSpaces(downloadPath)) downloadPath = myDefaultDownloadFolder;
384     storage.setValue(PROPERTY_DOWNLOAD_TO_PATH, downloadPath, myDefaultDownloadFolder);
385     storage.setValue(PROPERTY_ATTACH_JAVADOC, String.valueOf(myJavaDocCheckBox.isSelected()));
386     storage.setValue(PROPERTY_ATTACH_SOURCES, String.valueOf(mySourcesCheckBox.isSelected()));
387     storage.setValue(PROPERTY_ATTACH_ANNOTATIONS, String.valueOf(myAnnotationsCheckBox.isSelected()));
388     super.dispose();
389   }
390
391   @Override
392   protected String getDimensionServiceKey() {
393     return RepositoryAttachDialog.class.getName() + "-" + myMode;
394   }
395
396   private boolean isValidCoordinateSelected() {
397     final String text = getCoordinateText();
398     return text.split(":").length == 3;
399   }
400
401   private String getCoordinateText() {
402     String text = getFullCoordinateText();
403     List<String> parts = StringUtil.split(text, ":");
404     return parts.size() == 4 ? parts.get(0) + ":" + parts.get(1) + ":" + parts.get(3) : text;
405   }
406
407   @NotNull
408   private String getPackaging() {
409     List<String> parts = StringUtil.split(getFullCoordinateText(), ":");
410     return parts.size() == 4 ? parts.get(2) : JpsMavenRepositoryLibraryDescriptor.DEFAULT_PACKAGING;
411   }
412
413   private String getFullCoordinateText() {
414     return ((JTextField)myCombobox.getEditor().getEditorComponent()).getText().trim();
415   }
416
417   @NotNull
418   public JpsMavenRepositoryLibraryDescriptor getSelectedLibraryDescriptor() {
419     return new JpsMavenRepositoryLibraryDescriptor(getCoordinateText(), getPackaging(),
420                                                    getIncludeTransitiveDependencies(), Collections.emptyList());
421   }
422
423   private void createUIComponents() {
424     myProgressIcon = new AsyncProcessIcon("Progress");
425   }
426
427   private static boolean isMvnDependency(String text) {
428     String trimmed = text.trim();
429     if (trimmed.startsWith("<dependency>") && trimmed.endsWith("</dependency>")) {
430       return true;
431     }
432     return false;
433   }
434
435   @Nullable
436   private static String extractMavenCoordinates(Document document) {
437     String groupId = getGroupId(document);
438     String artifactId = getArtifactId(document);
439     if (groupId.isEmpty() && artifactId.isEmpty()) {
440       return null;
441     }
442     String version = getVersion(document);
443     String classifier = getClassifier(document);
444     String gradleClassifier = classifier.isEmpty() ? "" : ":" + classifier;
445     return groupId + ":" + artifactId + ":" + version + gradleClassifier;
446   }
447
448   private static String getVersion(@NotNull Document document) {
449     return firstOrEmpty(document.getElementsByTagName("version"));
450   }
451
452   private static String getArtifactId(@NotNull Document document) {
453     return firstOrEmpty(document.getElementsByTagName("artifactId"));
454   }
455
456   private static String getGroupId(@NotNull Document document) {
457     return firstOrEmpty(document.getElementsByTagName("groupId"));
458   }
459
460   private static String getClassifier(@NotNull Document document) {
461     return firstOrEmpty(document.getElementsByTagName("classifier"));
462   }
463
464   private static String firstOrEmpty(@NotNull NodeList list) {
465     Node first = list.item(0);
466     return first != null ? first.getTextContent() : "";
467   }
468 }