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