Java tutorial
/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.idea.gradle.structure.editors; import com.android.SdkConstants; import com.android.builder.model.ApiVersion; import com.android.ide.common.repository.GradleCoordinate; import com.android.sdklib.AndroidVersion; import com.android.tools.idea.gradle.project.model.AndroidModuleModel; import com.android.tools.idea.templates.RepositoryUrlManager; import com.android.tools.idea.templates.SupportLibrary; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.icons.AllIcons; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.ui.ValidationInfo; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.CollectionComboBoxModel; import com.intellij.ui.components.JBList; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.io.HttpRequests; import com.intellij.util.ui.AsyncProcessIcon; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jdom.xpath.XPath; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutorService; import static com.android.tools.idea.templates.RepositoryUrlManager.REVISION_ANY; public class MavenDependencyLookupDialog extends DialogWrapper { private static final String AAR_PACKAGING = "@" + SdkConstants.EXT_AAR; private static final String JAR_PACKAGING = "@" + SdkConstants.EXT_JAR; private static final int RESULT_LIMIT = 50; private static final String MAVEN_CENTRAL_SEARCH_URL = "https://search.maven.org/solrsearch/select?rows=%d&wt=xml&q=\"%s\""; private static final Logger LOG = Logger.getInstance(MavenDependencyLookupDialog.class); /** * Hardcoded list of common libraries that we will show in the dialog until the user actually does a search. */ private static final List<Artifact> COMMON_LIBRARIES = ImmutableList.of( new Artifact("com.google.code.gson", "gson", "2.2.4", "GSON"), new Artifact("joda-time", "joda-time", "2.3", "Joda-time"), new Artifact("com.squareup.picasso", "picasso", "2.3.2", "Picasso"), new Artifact("com.squareup", "otto", "1.3.5", "Otto"), new Artifact("org.slf4j", "slf4j-android", "1.7.7", "slf4j"), new Artifact("de.keyboardsurfer.android.widget", "crouton", "1.8.4", "Crouton"), new Artifact("com.nineoldandroids", "library", "2.4.0", "Nine Old Androids"), new Artifact("com.jakewharton", "butterknife", "5.1.1", "Butterknife"), new Artifact("com.google.guava", "guava", "16.0.1", "Guava"), new Artifact("com.squareup.okhttp", "okhttp", "2.0.0", "okhttp"), new Artifact("com.squareup.dagger", "dagger", "1.2.1", "Dagger")); /** * Hard-coded list of search rewrites to help users find common libraries. */ private static final Map<String, String> SEARCH_OVERRIDES = ImmutableMap.<String, String>builder() .put("jodatime", "joda-time").put("slf4j", "org.slf4j:slf4j-android") .put("slf4j-android", "org.slf4j:slf4j-android").put("animation", "com.nineoldandroids:library") .put("pulltorefresh", "com.github.chrisbanes.actionbarpulltorefresh:library") .put("wire", "wire-runtime").put("tape", "com.squareup:tape").put("annotations", "androidannotations") .put("svg", "svg-android").put("commons", "org.apache.commons").build(); private AsyncProcessIcon myProgressIcon; private TextFieldWithBrowseButton mySearchField; private JTextField mySearchTextField; private JPanel myPanel; private JBList myResultList; private final List<Artifact> myShownItems = Lists.newArrayList(); private final ExecutorService mySearchWorker = ConcurrencyUtil .newSingleThreadExecutor("Maven dependency lookup"); private final boolean myAndroidModule; private final List<String> myAndroidSdkLibraries = Lists.newArrayList(); private static class Artifact { @NotNull private final String myGroupId; @NotNull private final String myArtifactId; @NotNull private final String myVersion; @Nullable private final String myDescription; public Artifact(@NotNull String groupId, @NotNull String artifactId, @NotNull String version, @Nullable String description) { myGroupId = groupId; myArtifactId = artifactId; myVersion = version; myDescription = description; } @Nullable public static Artifact fromCoordinate(@NotNull String libraryCoordinate) { GradleCoordinate gradleCoordinate = GradleCoordinate.parseCoordinateString(libraryCoordinate); if (gradleCoordinate == null) { return null; } String groupId = gradleCoordinate.getGroupId(); String artifactId = gradleCoordinate.getArtifactId(); if (groupId == null || artifactId == null) { return null; } return new Artifact(groupId, artifactId, gradleCoordinate.getRevision(), groupId + ":" + artifactId); } @NotNull public String toString() { if (myDescription != null) { return myDescription + " (" + getCoordinates() + ")"; } else { return getCoordinates(); } } @NotNull public String getCoordinates() { String version = REVISION_ANY.equals(myVersion) ? "" : ':' + myVersion; return myGroupId + ":" + myArtifactId + version; } } /** * Comparator for Maven artifacts that does smart ordering for search results based on a given search string */ private static class ArtifactComparator implements Comparator<Artifact> { @NotNull private final String mySearchText; private ArtifactComparator(@NotNull String searchText) { mySearchText = searchText; } @Override public int compare(@NotNull Artifact artifact1, @NotNull Artifact artifact2) { int score = calculateScore(mySearchText, artifact2) - calculateScore(mySearchText, artifact1); if (score != 0) { return score; } else { return artifact2.myVersion.compareTo(artifact1.myVersion); } } private static int calculateScore(@NotNull String searchText, @NotNull Artifact artifact) { int score = 0; if (artifact.myArtifactId.equals(searchText)) { score++; } if (artifact.myArtifactId.contains(searchText)) { score++; } if (artifact.myGroupId.contains(searchText)) { score++; } return score; } } public MavenDependencyLookupDialog(@NotNull Project project, @Nullable Module module) { super(project, true); myAndroidModule = module != null && AndroidFacet.getInstance(module) != null; myProgressIcon.suspend(); mySearchField.setButtonIcon(AllIcons.Actions.Menu_find); mySearchField.getButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { startSearch(); } }); mySearchTextField = mySearchField.getTextField(); mySearchTextField.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { if (StringUtil.isEmpty(mySearchField.getText())) { return; } if (!isValidCoordinateSelected()) { startSearch(); } else { close(OK_EXIT_CODE); } } }); boolean preview = false; if (module != null) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null) { AndroidModuleModel androidModel = AndroidModuleModel.get(facet); if (androidModel != null) { ApiVersion minSdkVersion = androidModel.getSelectedVariant().getMergedFlavor() .getMinSdkVersion(); if (minSdkVersion != null) { preview = new AndroidVersion(minSdkVersion.getApiLevel(), minSdkVersion.getCodename()) .isPreview(); } } } } RepositoryUrlManager manager = RepositoryUrlManager.get(); for (SupportLibrary library : SupportLibrary.values()) { String libraryCoordinate = manager.getLibraryStringCoordinate(library, true); if (libraryCoordinate != null) { Artifact artifact = Artifact.fromCoordinate(libraryCoordinate); if (artifact != null) { myAndroidSdkLibraries.add(libraryCoordinate); myShownItems.add(artifact); } } } myShownItems.addAll(COMMON_LIBRARIES); myResultList.setModel(new CollectionComboBoxModel(myShownItems, null)); myResultList.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent listSelectionEvent) { Artifact value = (Artifact) myResultList.getSelectedValue(); if (value != null) { mySearchTextField.setText(value.getCoordinates()); } } }); myResultList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent mouseEvent) { if (mouseEvent.getClickCount() == 2 && isValidCoordinateSelected()) { close(OK_EXIT_CODE); } } }); myOKAction = new OkAction() { @Override protected void doAction(ActionEvent e) { String text = mySearchField.getText(); if (text != null && !hasVersion(text) && isKnownLocalLibrary(text)) { // If it's a known library that doesn't exist in the local repository, we don't display the version for it. Add it back so that // final string is a valid gradle coordinate. mySearchField.setText(text + ':' + REVISION_ANY); } super.doAction(e); } }; init(); } private static boolean isKnownLocalLibrary(@NotNull String text) { String group = getGroup(text); String artifact = getArtifact(text); if (group == null || artifact == null) { return false; } SupportLibrary library = SupportLibrary.find(group, artifact); return library != null; } @Nullable private static String getArtifact(@NotNull String coordinate) { int i = coordinate.indexOf(':'); if (i >= 0 && i + 1 < coordinate.length()) { // There's at least one char after the first ':' coordinate = coordinate.substring(i + 1); i = coordinate.indexOf(':'); if (i < 0) { i = coordinate.length(); } return coordinate.substring(0, i); } return null; } @Nullable private static String getGroup(@NotNull String coordinate) { int i = coordinate.indexOf(':'); if (i > 0) { return coordinate.substring(0, i); } return null; } @NotNull public String getSearchText() { return mySearchTextField.getText(); } /** * Prepares the search string and initiates the search in a worker thread. */ private void startSearch() { if (myProgressIcon.isRunning()) { return; } myProgressIcon.resume(); synchronized (myShownItems) { myResultList.clearSelection(); myShownItems.clear(); ((CollectionComboBoxModel) myResultList.getModel()).update(); } String text = mySearchTextField.getText(); if (StringUtil.isEmpty(text)) { return; } String override = SEARCH_OVERRIDES.get(text.toLowerCase(Locale.US)); if (override != null) { text = override; } final String finalText = text; mySearchWorker.submit(new Runnable() { @Override public void run() { searchAllRepositories(finalText); } }); } /** * Worker thread body that performs the search against the Maven index and interprets the result set */ private void searchAllRepositories(@NotNull final String text) { try { if (!myProgressIcon.isRunning()) { return; } List<String> results = Lists.newArrayList(); results.addAll(searchMavenCentral(text)); results.addAll(searchSdkRepositories(text)); if (!myProgressIcon.isRunning()) { return; } synchronized (myShownItems) { for (String s : results) { Artifact wrappedArtifact = Artifact.fromCoordinate(s); if (!myShownItems.contains(wrappedArtifact)) { myShownItems.add(wrappedArtifact); } } Collections.sort(myShownItems, new ArtifactComparator(text)); // In Android modules, if there are both @aar and @jar versions of the same artifact, hide the @jar one. if (myAndroidModule) { Set<String> itemsToRemove = Sets.newHashSet(); for (Artifact art : myShownItems) { String s = art.getCoordinates(); if (s.endsWith(AAR_PACKAGING)) { itemsToRemove.add(s.replace(AAR_PACKAGING, JAR_PACKAGING)); } } for (Iterator<Artifact> i = myShownItems.iterator(); i.hasNext();) { Artifact art = i.next(); if (itemsToRemove.contains(art.getCoordinates())) { i.remove(); } } } } /** * Update the UI in the Swing UI thread */ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { synchronized (myShownItems) { ((CollectionComboBoxModel) myResultList.getModel()).update(); if (myResultList.getSelectedIndex() == -1 && !myShownItems.isEmpty()) { myResultList.setSelectedIndex(0); } if (!myShownItems.isEmpty()) { myResultList.requestFocus(); } } } }); } catch (Exception e) { LOG.error(e); } finally { myProgressIcon.suspend(); } } @NotNull private List<String> searchSdkRepositories(@NotNull String text) { List<String> results = Lists.newArrayList(); for (String library : myAndroidSdkLibraries) { if (library.contains(text)) { results.add(library); } } return results; } @NotNull private static List<String> searchMavenCentral(@NotNull String text) { return HttpRequests.request(String.format(MAVEN_CENTRAL_SEARCH_URL, RESULT_LIMIT, text)) .accept("application/xml").connect(new HttpRequests.RequestProcessor<List<String>>() { @Override public List<String> process(@NotNull HttpRequests.Request request) throws IOException { try { XPath idPath = XPath.newInstance("str[@name='id']"); XPath versionPath = XPath.newInstance("str[@name='latestVersion']"); //noinspection unchecked List<Element> artifacts = (List<Element>) XPath.newInstance("/response/result/doc") .selectNodes(new SAXBuilder().build(request.getReader())); List<String> results = Lists.newArrayListWithExpectedSize(artifacts.size()); for (Element element : artifacts) { try { String id = ((Element) idPath.selectSingleNode(element)).getValue(); results.add(id + ":" + ((Element) versionPath.selectSingleNode(element)).getValue()); } catch (NullPointerException ignored) { // A result is missing an ID or version. Just skip it. } } return results; } catch (JDOMException e) { LOG.error(e); } return Collections.emptyList(); } }, Collections.<String>emptyList(), LOG); } @Override public JComponent getPreferredFocusedComponent() { return mySearchTextField; } @Override @Nullable protected ValidationInfo doValidate() { if (!isValidCoordinateSelected()) { return new ValidationInfo("Please enter a valid coordinate, discover it or select one from the list", getPreferredFocusedComponent()); } return super.doValidate(); } @Override @NotNull protected JComponent createCenterPanel() { return myPanel; } @Override protected void dispose() { Disposer.dispose(myProgressIcon); mySearchWorker.shutdown(); super.dispose(); } @Override @NotNull protected String getDimensionServiceKey() { return MavenDependencyLookupDialog.class.getName(); } private boolean isValidCoordinateSelected() { String text = mySearchTextField.getText(); return GradleCoordinate.parseCoordinateString(text) != null; } private void createUIComponents() { myProgressIcon = new AsyncProcessIcon("Progress"); } public static boolean hasVersion(String coordinateText) { return StringUtil.countChars(coordinateText, ':') > 1; } }