EditorConfig documentation test
[idea/community.git] / java / idea-ui / src / com / intellij / jarRepository / RepositoryAttachDialog.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.jarRepository;
3
4 import com.intellij.icons.AllIcons;
5 import com.intellij.ide.util.PropertiesComponent;
6 import com.intellij.openapi.application.ApplicationManager;
7 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
8 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
9 import com.intellij.openapi.fileChooser.FileChooserDialog;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.project.ProjectBundle;
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.Pair;
18 import com.intellij.openapi.util.io.FileUtil;
19 import com.intellij.openapi.util.text.StringUtil;
20 import com.intellij.openapi.vfs.VirtualFile;
21 import com.intellij.ui.CollectionComboBoxModel;
22 import com.intellij.ui.ComboboxWithBrowseButton;
23 import com.intellij.ui.DocumentAdapter;
24 import com.intellij.ui.components.JBCheckBox;
25 import com.intellij.ui.components.JBLabel;
26 import com.intellij.util.ui.AsyncProcessIcon;
27 import com.intellij.xml.util.XmlStringUtil;
28 import gnu.trove.THashMap;
29 import org.eclipse.aether.version.InvalidVersionSpecificationException;
30 import org.eclipse.aether.version.Version;
31 import org.jetbrains.annotations.NonNls;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34 import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager;
35 import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor;
36 import org.w3c.dom.Document;
37 import org.w3c.dom.Node;
38 import org.w3c.dom.NodeList;
39 import org.xml.sax.InputSource;
40 import org.xml.sax.SAXException;
41
42 import javax.swing.*;
43 import javax.swing.event.DocumentEvent;
44 import javax.swing.text.JTextComponent;
45 import javax.xml.parsers.DocumentBuilder;
46 import javax.xml.parsers.DocumentBuilderFactory;
47 import javax.xml.parsers.ParserConfigurationException;
48 import java.awt.*;
49 import java.awt.event.ActionEvent;
50 import java.awt.event.ActionListener;
51 import java.io.File;
52 import java.io.IOException;
53 import java.io.StringReader;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.List;
57 import java.util.Map;
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 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 ? "Download Library from Maven Repository" : "Search Library in Maven Repositories");
98     myProject = project;
99     myProgressIcon.suspend();
100     myCaptionLabel.setText(
101       XmlStringUtil.wrapInHtml(StringUtil.escapeXmlEntities("keyword or class name to search by or exact Maven coordinates, " +
102                                                             "i.e. 'spring', 'Logger' or 'ant:ant-junit:1.6.5'")
103       ));
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(StringUtil.notNullize(StringUtil.nullize(storage.getValue(PROPERTY_DOWNLOAD_TO_PATH)), myDefaultDownloadFolder));
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(ProjectBundle.message("file.chooser.directory.for.downloaded.libraries.title"),
172                                              ProjectBundle.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 static void handleMavenDependencyInsertion(DocumentEvent e, JTextField textField) {
181     if (e.getType() == DocumentEvent.EventType.INSERT) {
182       String text = textField.getText();
183       if (isMvnDependency(text)) {
184         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
185         factory.setValidating(false);
186         try {
187           DocumentBuilder builder = factory.newDocumentBuilder();
188           try {
189             Document document = builder.parse(new InputSource(new StringReader(text)));
190             String mavenCoordinates = extractMavenCoordinates(document);
191             if (mavenCoordinates != null) {
192               textField.setText(mavenCoordinates);
193             }
194           }
195           catch (SAXException | IOException ignored) {
196           }
197         }
198         catch (ParserConfigurationException ignored) {
199         }
200       }
201     }
202   }
203
204   public boolean getAttachJavaDoc() {
205     return myJavaDocCheckBox.isSelected();
206   }
207
208   public boolean getAttachSources() {
209     return mySourcesCheckBox.isSelected();
210   }
211
212   public boolean getAttachExternalAnnotations() {
213     return myAnnotationsCheckBox.isSelected();
214   }
215
216   public boolean getIncludeTransitiveDependencies() {
217     return myMode == Mode.DOWNLOAD ? myIncludeTransitiveDepsCheckBox.isSelected() : myIncludeTransitiveDependenciesForSearchCheckBox.isSelected();
218   }
219
220   @Nullable
221   public String getDirectoryPath() {
222     return myDownloadToCheckBox.isSelected()? myDirectoryField.getText() : null;
223   }
224
225   @Override
226   public JComponent getPreferredFocusedComponent() {
227     return myCombobox;
228   }
229
230   private void updateComboboxSelection(boolean force) {
231     final String prevFilter = myFilterString;
232     final JTextComponent field = (JTextComponent)myCombobox.getEditor().getEditorComponent();
233     final int caret = field.getCaretPosition();
234     myFilterString = field.getText();
235
236     if (!force && Comparing.equal(myFilterString, prevFilter)) return;
237     int prevSize = myShownItems.size();
238     myShownItems.clear();
239
240     myInUpdate = true;
241     final boolean itemSelected = myCoordinates.containsKey(myFilterString) && Comparing.strEqual((String)myCombobox.getSelectedItem(), myFilterString, false);
242     final boolean filtered;
243     if (itemSelected) {
244       myShownItems.addAll(myCoordinates.keySet());
245       filtered = false;
246     }
247     else {
248       final String[] parts = myFilterString.split(" ");
249       main:
250       for (String coordinate : myCoordinates.keySet()) {
251         for (String part : parts) {
252           if (!StringUtil.containsIgnoreCase(coordinate, part)) {
253             continue main;
254           }
255         }
256         myShownItems.add(coordinate);
257       }
258       filtered = !myShownItems.isEmpty();
259       if (!filtered) {
260         myShownItems.addAll(myCoordinates.keySet());
261       }
262       myCombobox.setSelectedItem(null);
263     }
264
265     // use maven version sorter
266     ArrayList<LibItem> items = new ArrayList<>(myShownItems.size());
267     for (String coord : myShownItems) {
268       items.add(new LibItem(coord));
269     }
270     Collections.sort(items, (o1, o2) -> Comparing.compare(o1, o2));
271     myShownItems.clear();
272     for (LibItem it : items) {
273       myShownItems.add(it.coord);
274     }
275
276     ((CollectionComboBoxModel)myCombobox.getModel()).update();
277     myInUpdate = false;
278     field.setText(myFilterString);
279     field.setCaretPosition(caret);
280     updateInfoLabel();
281     if (filtered) {
282       if (prevSize < 10 && myShownItems.size() > prevSize && myCombobox.isPopupVisible()) {
283         myCombobox.setPopupVisible(false);
284       }
285       if (!myCombobox.isPopupVisible()) {
286         myCombobox.setPopupVisible(true);
287       }
288     }
289   }
290
291   private static final class LibItem implements Comparable<LibItem> {
292     final String prefix;
293     final Version ver;
294     final String coord;
295
296     LibItem(String coord) {
297       this.coord = coord;
298       final JpsMavenRepositoryLibraryDescriptor desc = new JpsMavenRepositoryLibraryDescriptor(coord);
299       prefix = desc.getGroupId() + ":" + desc.getArtifactId();
300       Version ver = null;
301       try {
302         ver = ArtifactRepositoryManager.asVersion(desc.getVersion());
303       }
304       catch (InvalidVersionSpecificationException ignored) {
305       }
306       this.ver = ver;
307     }
308
309     @Override
310     public int compareTo(@NotNull LibItem that) {
311       final int prefixCompare = prefix.compareTo(that.prefix);
312       return prefixCompare != 0 ? prefixCompare : Comparing.compare(that.ver, ver);
313     }
314   }
315
316   private boolean performSearch() {
317     final String text = getCoordinateText();
318     if (myProgressIcon.isRunning() || StringUtil.isEmptyOrSpaces(text) || myCoordinates.containsKey(text)) {
319       return false;
320     }
321     myProgressIcon.resume();
322     JarRepositoryManager.searchArtifacts(myProject, text, (pairs) -> {
323       try {
324         if (myProgressIcon.isDisposed()) {
325           return;
326         }
327         myProgressIcon.suspend(); // finished
328         final int prevSize = myCoordinates.size();
329         for (Pair<RepositoryArtifactDescription, RemoteRepositoryDescription> pair : pairs) {
330           final RepositoryArtifactDescription artifact = pair.first;
331           myCoordinates.put(artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion(), artifact);
332         }
333         updateComboboxSelection(prevSize != myCoordinates.size());
334       }
335       finally {
336         setOKActionEnabled(true);
337       }
338     });
339     return true;
340   }
341
342   private void updateInfoLabel() {
343     myInfoLabel.setText("<html>Found: " + myCoordinates.size() + "<br>Showing: " + myCombobox.getModel().getSize() + "</html>");
344   }
345
346   @Override
347   protected ValidationInfo doValidate() {
348     if (!isValidCoordinateSelected()) {
349       return new ValidationInfo("Please enter valid coordinate, discover it or select one from the list", myCombobox);
350     }
351     else if (myDownloadToCheckBox.isSelected()) {
352       final File dir = new File(myDirectoryField.getText());
353       if (!dir.exists() && !dir.mkdirs() || !dir.isDirectory()) {
354         return new ValidationInfo("Please enter valid library files path", myDirectoryField.getTextField());
355       }
356     }
357     return super.doValidate();
358   }
359
360   @Override
361   protected JComponent createCenterPanel() {
362     return null;
363   }
364
365   @Override
366   protected JComponent createNorthPanel() {
367     return myPanel;
368   }
369
370
371   @Override
372   protected void dispose() {
373     Disposer.dispose(myProgressIcon);
374     PropertiesComponent storage = PropertiesComponent.getInstance(myProject);
375     storage.setValue(PROPERTY_DOWNLOAD_TO_PATH_ENABLED, String.valueOf(myDownloadToCheckBox.isSelected()));
376     String downloadPath = myDirectoryField.getText();
377     if (StringUtil.isEmptyOrSpaces(downloadPath)) downloadPath = myDefaultDownloadFolder;
378     storage.setValue(PROPERTY_DOWNLOAD_TO_PATH, downloadPath, myDefaultDownloadFolder);
379     storage.setValue(PROPERTY_ATTACH_JAVADOC, String.valueOf(myJavaDocCheckBox.isSelected()));
380     storage.setValue(PROPERTY_ATTACH_SOURCES, String.valueOf(mySourcesCheckBox.isSelected()));
381     storage.setValue(PROPERTY_ATTACH_ANNOTATIONS, String.valueOf(myAnnotationsCheckBox.isSelected()));
382     super.dispose();
383   }
384
385   @Override
386   protected String getDimensionServiceKey() {
387     return RepositoryAttachDialog.class.getName() + "-" + myMode;
388   }
389
390   private boolean isValidCoordinateSelected() {
391     final String text = getCoordinateText();
392     return text.split(":").length == 3;
393   }
394
395   private String getCoordinateText() {
396     String text = getFullCoordinateText();
397     List<String> parts = StringUtil.split(text, ":");
398     return parts.size() == 4 ? parts.get(0) + ":" + parts.get(1) + ":" + parts.get(3) : text;
399   }
400
401   @NotNull
402   private String getPackaging() {
403     List<String> parts = StringUtil.split(getFullCoordinateText(), ":");
404     return parts.size() == 4 ? parts.get(2) : JpsMavenRepositoryLibraryDescriptor.DEFAULT_PACKAGING;
405   }
406
407   private String getFullCoordinateText() {
408     return ((JTextField)myCombobox.getEditor().getEditorComponent()).getText();
409   }
410
411   @NotNull
412   public JpsMavenRepositoryLibraryDescriptor getSelectedLibraryDescriptor() {
413     return new JpsMavenRepositoryLibraryDescriptor(getCoordinateText(), getPackaging(),
414                                                    getIncludeTransitiveDependencies(), Collections.emptyList());
415   }
416
417   private void createUIComponents() {
418     myProgressIcon = new AsyncProcessIcon("Progress");
419   }
420
421   private static boolean isMvnDependency(String text) {
422     String trimmed = text.trim();
423     if (trimmed.startsWith("<dependency>") && trimmed.endsWith("</dependency>")) {
424       return true;
425     }
426     return false;
427   }
428
429   @Nullable
430   private static String extractMavenCoordinates(Document document) {
431     String groupId = getGroupId(document);
432     String artifactId = getArtifactId(document);
433     if (groupId.isEmpty() && artifactId.isEmpty()) {
434       return null;
435     }
436     String version = getVersion(document);
437     String classifier = getClassifier(document);
438     String gradleClassifier = classifier.isEmpty() ? "" : ":" + classifier;
439     return groupId + ":" + artifactId + ":" + version + gradleClassifier;
440   }
441
442   private static String getVersion(@NotNull Document document) {
443     return firstOrEmpty(document.getElementsByTagName("version"));
444   }
445
446   private static String getArtifactId(@NotNull Document document) {
447     return firstOrEmpty(document.getElementsByTagName("artifactId"));
448   }
449
450   private static String getGroupId(@NotNull Document document) {
451     return firstOrEmpty(document.getElementsByTagName("groupId"));
452   }
453
454   private static String getClassifier(@NotNull Document document) {
455     return firstOrEmpty(document.getElementsByTagName("classifier"));
456   }
457
458   private static String firstOrEmpty(@NotNull NodeList list) {
459     Node first = list.item(0);
460     return first != null ? first.getTextContent() : "";
461   }
462 }