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;
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;
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;
49 import java.awt.event.ActionEvent;
50 import java.awt.event.ActionListener;
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;
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;
67 public enum Mode { SEARCH, DOWNLOAD }
68 private final Project myProject;
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;
85 private final JComboBox myCombobox;
87 private final Map<String, RepositoryArtifactDescription> myCoordinates = new THashMap<>();
88 private final List<String> myShownItems = new ArrayList<>();
89 private final String myDefaultDownloadFolder;
91 private String myFilterString;
92 private boolean myInUpdate;
94 public RepositoryAttachDialog(@NotNull Project project, final @Nullable String initialFilter, @NotNull Mode mode) {
97 setTitle(mode == Mode.DOWNLOAD ? "Download Library from Maven Repository" : "Search Library in Maven Repositories");
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'")
104 myInfoLabel.setPreferredSize(
105 new Dimension(myInfoLabel.getFontMetrics(myInfoLabel.getFont()).stringWidth("Showing: 1000"), myInfoLabel.getPreferredSize().height));
107 myComboComponent.setButtonIcon(AllIcons.Actions.Find);
108 myComboComponent.getButton().addActionListener(new ActionListener() {
110 public void actionPerformed(ActionEvent e) {
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);
122 textField.getDocument().addDocumentListener(new DocumentAdapter() {
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);
133 updateComboboxSelection(false);
137 myCombobox.addActionListener(new ActionListener() {
139 public void actionPerformed(ActionEvent e) {
140 final boolean popupVisible = myCombobox.isPopupVisible();
141 if (!myInUpdate && (!popupVisible || myCoordinates.isEmpty())) {
145 final String item = (String)myCombobox.getSelectedItem();
146 if (StringUtil.isNotEmpty(item)) {
147 ((JTextField)myCombobox.getEditor().getEditorComponent()).setText(item);
152 VirtualFile baseDir = !myProject.isDefault() ? myProject.getBaseDir() : null;
153 myDefaultDownloadFolder = baseDir != null ? FileUtil.toSystemDependentName(baseDir.getPath() + "/lib") : "";
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() {
161 public void actionPerformed(ActionEvent e) {
162 myDirectoryField.setEnabled(myDownloadToCheckBox.isSelected());
165 myJavaDocCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_JAVADOC));
166 mySourcesCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_SOURCES));
167 mySourcesCheckBox.setSelected(storage.isTrueValue(PROPERTY_ATTACH_ANNOTATIONS));
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,
175 myDownloadOptionsPanel.setVisible(mode == Mode.DOWNLOAD);
176 mySearchOptionsPanel.setVisible(mode == Mode.SEARCH);
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);
187 DocumentBuilder builder = factory.newDocumentBuilder();
189 Document document = builder.parse(new InputSource(new StringReader(text)));
190 String mavenCoordinates = extractMavenCoordinates(document);
191 if (mavenCoordinates != null) {
192 textField.setText(mavenCoordinates);
195 catch (SAXException | IOException ignored) {
198 catch (ParserConfigurationException ignored) {
204 public boolean getAttachJavaDoc() {
205 return myJavaDocCheckBox.isSelected();
208 public boolean getAttachSources() {
209 return mySourcesCheckBox.isSelected();
212 public boolean getAttachExternalAnnotations() {
213 return myAnnotationsCheckBox.isSelected();
216 public boolean getIncludeTransitiveDependencies() {
217 return myMode == Mode.DOWNLOAD ? myIncludeTransitiveDepsCheckBox.isSelected() : myIncludeTransitiveDependenciesForSearchCheckBox.isSelected();
221 public String getDirectoryPath() {
222 return myDownloadToCheckBox.isSelected()? myDirectoryField.getText() : null;
226 public JComponent getPreferredFocusedComponent() {
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();
236 if (!force && Comparing.equal(myFilterString, prevFilter)) return;
237 int prevSize = myShownItems.size();
238 myShownItems.clear();
241 final boolean itemSelected = myCoordinates.containsKey(myFilterString) && Comparing.strEqual((String)myCombobox.getSelectedItem(), myFilterString, false);
242 final boolean filtered;
244 myShownItems.addAll(myCoordinates.keySet());
248 final String[] parts = myFilterString.split(" ");
250 for (String coordinate : myCoordinates.keySet()) {
251 for (String part : parts) {
252 if (!StringUtil.containsIgnoreCase(coordinate, part)) {
256 myShownItems.add(coordinate);
258 filtered = !myShownItems.isEmpty();
260 myShownItems.addAll(myCoordinates.keySet());
262 myCombobox.setSelectedItem(null);
265 // use maven version sorter
266 ArrayList<LibItem> items = new ArrayList<>(myShownItems.size());
267 for (String coord : myShownItems) {
268 items.add(new LibItem(coord));
270 Collections.sort(items, (o1, o2) -> Comparing.compare(o1, o2));
271 myShownItems.clear();
272 for (LibItem it : items) {
273 myShownItems.add(it.coord);
276 ((CollectionComboBoxModel)myCombobox.getModel()).update();
278 field.setText(myFilterString);
279 field.setCaretPosition(caret);
282 if (prevSize < 10 && myShownItems.size() > prevSize && myCombobox.isPopupVisible()) {
283 myCombobox.setPopupVisible(false);
285 if (!myCombobox.isPopupVisible()) {
286 myCombobox.setPopupVisible(true);
291 private static final class LibItem implements Comparable<LibItem> {
296 LibItem(String coord) {
298 final JpsMavenRepositoryLibraryDescriptor desc = new JpsMavenRepositoryLibraryDescriptor(coord);
299 prefix = desc.getGroupId() + ":" + desc.getArtifactId();
302 ver = ArtifactRepositoryManager.asVersion(desc.getVersion());
304 catch (InvalidVersionSpecificationException ignored) {
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);
316 private boolean performSearch() {
317 final String text = getCoordinateText();
318 if (myProgressIcon.isRunning() || StringUtil.isEmptyOrSpaces(text) || myCoordinates.containsKey(text)) {
321 myProgressIcon.resume();
322 JarRepositoryManager.searchArtifacts(myProject, text, (pairs) -> {
324 if (myProgressIcon.isDisposed()) {
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);
333 updateComboboxSelection(prevSize != myCoordinates.size());
336 setOKActionEnabled(true);
342 private void updateInfoLabel() {
343 myInfoLabel.setText("<html>Found: " + myCoordinates.size() + "<br>Showing: " + myCombobox.getModel().getSize() + "</html>");
347 protected ValidationInfo doValidate() {
348 if (!isValidCoordinateSelected()) {
349 return new ValidationInfo("Please enter valid coordinate, discover it or select one from the list", myCombobox);
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());
357 return super.doValidate();
361 protected JComponent createCenterPanel() {
366 protected JComponent createNorthPanel() {
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()));
386 protected String getDimensionServiceKey() {
387 return RepositoryAttachDialog.class.getName() + "-" + myMode;
390 private boolean isValidCoordinateSelected() {
391 final String text = getCoordinateText();
392 return text.split(":").length == 3;
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;
402 private String getPackaging() {
403 List<String> parts = StringUtil.split(getFullCoordinateText(), ":");
404 return parts.size() == 4 ? parts.get(2) : JpsMavenRepositoryLibraryDescriptor.DEFAULT_PACKAGING;
407 private String getFullCoordinateText() {
408 return ((JTextField)myCombobox.getEditor().getEditorComponent()).getText();
412 public JpsMavenRepositoryLibraryDescriptor getSelectedLibraryDescriptor() {
413 return new JpsMavenRepositoryLibraryDescriptor(getCoordinateText(), getPackaging(),
414 getIncludeTransitiveDependencies(), Collections.emptyList());
417 private void createUIComponents() {
418 myProgressIcon = new AsyncProcessIcon("Progress");
421 private static boolean isMvnDependency(String text) {
422 String trimmed = text.trim();
423 if (trimmed.startsWith("<dependency>") && trimmed.endsWith("</dependency>")) {
430 private static String extractMavenCoordinates(Document document) {
431 String groupId = getGroupId(document);
432 String artifactId = getArtifactId(document);
433 if (groupId.isEmpty() && artifactId.isEmpty()) {
436 String version = getVersion(document);
437 String classifier = getClassifier(document);
438 String gradleClassifier = classifier.isEmpty() ? "" : ":" + classifier;
439 return groupId + ":" + artifactId + ":" + version + gradleClassifier;
442 private static String getVersion(@NotNull Document document) {
443 return firstOrEmpty(document.getElementsByTagName("version"));
446 private static String getArtifactId(@NotNull Document document) {
447 return firstOrEmpty(document.getElementsByTagName("artifactId"));
450 private static String getGroupId(@NotNull Document document) {
451 return firstOrEmpty(document.getElementsByTagName("groupId"));
454 private static String getClassifier(@NotNull Document document) {
455 return firstOrEmpty(document.getElementsByTagName("classifier"));
458 private static String firstOrEmpty(@NotNull NodeList list) {
459 Node first = list.item(0);
460 return first != null ? first.getTextContent() : "";