blob: 0a94550264fee00a9c79f97002321100fea0870f [file] [log] [blame]
/*
* 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.templates;
import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.repository.SdkMavenRepository;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.*;
/**
* Helper class to aid in generating Maven URLs for various internal repository files (Support Library, AppCompat, etc).
*/
public class RepositoryUrlManager {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.templates.RepositoryUrlManager");
/** The tag used by the maven metadata file to describe versions */
public static final String TAG_VERSION = "version";
/** The path ID of the support library. */
public static final String SUPPORT_ID_V4 = "support-v4";
/** The path ID of the support library. */
public static final String SUPPORT_ID_V13 = "support-v13";
/** The path ID of the appcompat library. */
public static final String APP_COMPAT_ID_V7 = "appcompat-v7";
/** The path ID of the design support library. */
public static final String DESIGN = "design";
/** The path ID of the gridlayout library. */
public static final String GRID_LAYOUT_ID_V7 = "gridlayout-v7";
/** The path ID of the mediarouter library*/
public static final String MEDIA_ROUTER_ID_V7 = "mediarouter-v7";
/** The path ID of the Play Services library */
public static final String PLAY_SERVICES_ID = "play-services";
/** The path ID of the Wearable Play Services library */
public static final String PLAY_SERVICES_WEARABLE_ID = "play-services-wearable";
/** The path ID of the wearable support library */
public static final String SUPPORT_WEARABLE_ID = "wearable";
/** The path ID of the cardview library*/
public static final String CARDVIEW_ID_V7 = "cardview-v7";
/** The path ID of the leanback library*/
public static final String LEANBACK_ID_V17 = "leanback-v17";
/** The path ID of the palette library*/
public static final String PALETTE_ID_V7 = "palette-v7";
/** The path ID of the recyclerview library*/
public static final String RECYCLER_VIEW_ID_V7 = "recyclerview-v7";
/** The path ID of the support-annotations library*/
public static final String SUPPORT_ANNOTATIONS = "support-annotations";
/** The path ID of the compatibility library (which was its id for releases 1-3). */
public static final String COMPATIBILITY_ID = "compatibility";
/** Internal Maven Repository settings */
/** Constant full revision for "anything available" */
public static final String REVISION_ANY = "0.0.+";
private static final String SUPPORT_BASE_COORDINATE = "com.android.support:%s:%s";
private static final String GOOGLE_BASE_COORDINATE = "com.google.android.gms:%s:%s";
private static final String GOOGLE_SUPPORT_BASE_COORDINATE = "com.google.android.support:%s:%s";
private static final String SUPPORT_REPOSITORY_BASE_PATH = "%s/extras/android/m2repository/com/android/support/%s/";
private static final String GOOGLE_REPOSITORY_BASE_PATH = "%s/extras/google/m2repository/com/google/android/gms/%s/";
private static final String GOOGLE_SUPPORT_REPOSITORY_BASE_PATH = "%s/extras/google/m2repository/com/google/android/support/%s/";
// e.g. 18.0.0/appcompat-v7-18.0.0
private static final String MAVEN_REVISION_PATH = "%2$s/%1$s-%2$s";
public static final String MAVEN_METADATA_FILE_NAME = "maven-metadata.xml";
public static final String HELP_COMMENT =
"// You must install or update the %1$s Repository through the SDK manager to use this dependency.";
/** Model of our internal extras repository */
private static final RangeMap<Integer, String> SUPPORT_LIBRARY_EXTENSIONS = ImmutableRangeMap.<Integer, String>builder()
.put(Range.closed(1, 19), SdkConstants.DOT_JAR)
.put(Range.atLeast(20), SdkConstants.DOT_AAR)
.build();
public static final Map<String, RepositoryLibrary> EXTRAS_REPOSITORY = new ImmutableMap.Builder<String, RepositoryLibrary>()
.put(SUPPORT_ANNOTATIONS, new RepositoryLibrary(SUPPORT_ANNOTATIONS, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_JAR))
.put(SUPPORT_ID_V4,
new RepositoryLibrary(SUPPORT_ID_V4, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SUPPORT_LIBRARY_EXTENSIONS))
.put(SUPPORT_ID_V13, new RepositoryLibrary(SUPPORT_ID_V13, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE,
SUPPORT_LIBRARY_EXTENSIONS))
.put(APP_COMPAT_ID_V7, new RepositoryLibrary(APP_COMPAT_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(DESIGN, new RepositoryLibrary(DESIGN, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(GRID_LAYOUT_ID_V7, new RepositoryLibrary(GRID_LAYOUT_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(MEDIA_ROUTER_ID_V7, new RepositoryLibrary(MEDIA_ROUTER_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(PLAY_SERVICES_ID, new RepositoryLibrary(PLAY_SERVICES_ID, GOOGLE_REPOSITORY_BASE_PATH, GOOGLE_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(PLAY_SERVICES_WEARABLE_ID, new RepositoryLibrary(PLAY_SERVICES_WEARABLE_ID, GOOGLE_REPOSITORY_BASE_PATH, GOOGLE_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(CARDVIEW_ID_V7, new RepositoryLibrary(CARDVIEW_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(PALETTE_ID_V7, new RepositoryLibrary(PALETTE_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(RECYCLER_VIEW_ID_V7, new RepositoryLibrary(RECYCLER_VIEW_ID_V7, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(LEANBACK_ID_V17, new RepositoryLibrary(LEANBACK_ID_V17, SUPPORT_REPOSITORY_BASE_PATH, SUPPORT_BASE_COORDINATE, SdkConstants.DOT_AAR))
.put(SUPPORT_WEARABLE_ID, new RepositoryLibrary(SUPPORT_WEARABLE_ID, GOOGLE_SUPPORT_REPOSITORY_BASE_PATH, GOOGLE_SUPPORT_BASE_COORDINATE,
SdkConstants.DOT_AAR))
.build();
public static RepositoryUrlManager get() {
return new RepositoryUrlManager();
}
@VisibleForTesting
RepositoryUrlManager() {}
/**
* Calculate the coordinate pointing to the highest valued version of the given library we
* have available in our repository.
* @param libraryId the id of the library to find
* @return a maven coordinate for the requested library or null if we don't support that library
*/
@Nullable
public String getLibraryCoordinate(String libraryId) {
return getLibraryCoordinate(libraryId, null, true);
}
/**
* Returns the string for the specific version number of the most recent version of the given library
* (matching the given prefix filter, if any) in one of the Sdk repositories.
*
* @param groupId the group id
* @param artifactId the artifact id
* @param filterPrefix a prefix, if any
* @param includePreviews whether to include preview versions of libraries
* @return
*/
@Nullable
public String getLibraryCoordinate(String groupId, String artifactId, @Nullable String filterPrefix, boolean includePreviews) {
SdkMavenRepository repository = SdkMavenRepository.getByGroupId(groupId);
if (repository == null) {
return null;
}
AndroidSdkData sdk = tryToChooseAndroidSdk();
if (sdk == null) {
return null;
}
File sdkLocation = sdk.getLocation();
File repo = repository.getRepositoryLocation(sdkLocation, false);
if (repo == null) {
return null;
}
GradleCoordinate max = repository.getHighestInstalledVersion(sdk.getLocation(), groupId, artifactId, filterPrefix, includePreviews);
if (max == null) {
return null;
}
return max.getFullRevision();
}
/**
* Calculate the coordinate pointing to the highest valued version of the given library we
* have available in our repository.
* @param libraryId the id of the library to find
* @param filterPrefix an optional prefix libraries must match; e.g. if the prefix is "18." then only coordinates
* in version 18.x will be considered
* @return a maven coordinate for the requested library or null if we don't support that library
* @deprecated Use {@link #getLibraryCoordinate(String, String, String, boolean)} instead. This method only takes
* an artifact id, which may <b>not</b> be unique across group id's, and besides, the below method relies on a hardcoded
* list of libraries in each repository, which gets obsolete all the time as new repositories are added. The method
* above however, does not rely on a table like that and should continue to work as new libraries are added.
*/
@Nullable
@Deprecated
public String getLibraryCoordinate(String libraryId, @Nullable String filterPrefix, boolean includePreviews) {
// Check to see if this is a URL we support:
if (!EXTRAS_REPOSITORY.containsKey(libraryId)) {
return null;
}
AndroidSdkData sdk = tryToChooseAndroidSdk();
if (sdk == null) {
return null;
}
// Read the support repository and find the latest version available
String sdkLocation = sdk.getLocation().getPath();
RepositoryLibrary library = EXTRAS_REPOSITORY.get(libraryId);
File supportMetadataFile = new File(String.format(library.basePath, sdkLocation, library.id), MAVEN_METADATA_FILE_NAME);
if (!fileExists(supportMetadataFile)) {
return String.format(library.baseCoordinate, library.id, REVISION_ANY);
}
String version = getLatestVersionFromMavenMetadata(supportMetadataFile, filterPrefix, includePreviews);
return version != null ? String.format(library.baseCoordinate, library.id, version) : null;
}
/**
* Get the file on the local filesystem that corresponds to the given maven coordinate.
* @param gradleCoordinate the coordinate to retrieve an archive file for
* @return a file pointing at the archive for the given coordinate or null if no SDK is configured
*/
@Nullable
public File getArchiveForCoordinate(GradleCoordinate gradleCoordinate) {
AndroidSdkData sdk = tryToChooseAndroidSdk();
if (sdk == null) {
return null;
}
// Get the parameters to include in the path
String sdkLocation = sdk.getLocation().getPath();
String artifactId = gradleCoordinate.getArtifactId();
String revision = gradleCoordinate.getFullRevision();
RepositoryLibrary library = EXTRAS_REPOSITORY.get(artifactId);
File path = new File(String.format(library.basePath, sdkLocation, library.id));
String revisionPath = String.format(MAVEN_REVISION_PATH, library.id, revision) +
library.getArchiveExtension(gradleCoordinate.getMajorVersion());
return new File(path, revisionPath);
}
/**
* Returns true iff this class knows how to handle the given library id (Maven Artifact Id)
* @param libraryId the library id to test
* @return true iff this class supports the given library
*/
public static boolean supports(String libraryId) {
return EXTRAS_REPOSITORY.containsKey(libraryId);
}
/**
* Parses a Maven metadata file and returns a string of the highest found version
* @param metadataFile the files to parse
* @param includePreviews if false, preview versions of the library will not be returned
* @return the string representing the highest version found in the file or "0.0.0" if no versions exist in the file
*/
@Nullable
private String getLatestVersionFromMavenMetadata(final File metadataFile, @Nullable final String filterPrefix,
final boolean includePreviews) {
String xml = readTextFile(metadataFile);
final List<GradleCoordinate> versions = Lists.newLinkedList();
try {
SAXParserFactory.newInstance().newSAXParser().parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
boolean inVersionTag = false;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName.equals(TAG_VERSION)) {
inVersionTag = true;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// Get the version and compare it to the current known max version
if (inVersionTag) {
inVersionTag = false;
String revision = new String(ch, start, length);
//noinspection StatementWithEmptyBody
if (!includePreviews && "5.2.08".equals(revision) && metadataFile.getPath().contains(PLAY_SERVICES_ID)) {
// This version (despite not having -rcN in its version name is actually a preview
// (See https://code.google.com/p/android/issues/detail?id=75292)
// Ignore it
} else if (filterPrefix == null || revision.startsWith(filterPrefix)) {
versions.add(GradleCoordinate.parseVersionOnly(revision));
}
}
}
});
} catch (Exception e) {
LOG.warn(e);
}
if (versions.isEmpty()) {
return REVISION_ANY;
} else if (includePreviews) {
return GRADLE_COORDINATE_ORDERING.max(versions).getFullRevision();
} else {
try {
return GRADLE_COORDINATE_ORDERING.max(Iterables.filter(versions, IS_NOT_PREVIEW)).getFullRevision();
} catch (NoSuchElementException e) {
return null;
}
}
}
/**
* Evaluates to true iff the given FullRevision is not a preview version
*/
private static final Predicate<GradleCoordinate> IS_NOT_PREVIEW = new Predicate<GradleCoordinate>() {
@Override
public boolean apply(GradleCoordinate input) {
return !input.isPreview();
}
};
private static final Ordering<GradleCoordinate> GRADLE_COORDINATE_ORDERING = new Ordering<GradleCoordinate>() {
@Override
public int compare(GradleCoordinate left, GradleCoordinate right) {
return GradleCoordinate.COMPARE_PLUS_LOWER.compare(left, right);
}
};
@Nullable
protected AndroidSdkData tryToChooseAndroidSdk() {
return AndroidSdkUtils.tryToChooseAndroidSdk();
}
@Nullable
protected String readTextFile(File file) {
return TemplateUtils.readTextFile(file);
}
protected boolean fileExists(@NotNull File file) {
return file.exists();
}
@VisibleForTesting
static class RepositoryLibrary {
public final String id;
public final String basePath;
public final String baseCoordinate;
private final RangeMap<Integer, String> myArchiveExtensions;
private RepositoryLibrary(String id, String basePath, String baseCoordinate, String archiveExtension) {
this.id = id;
this.basePath = basePath;
this.baseCoordinate = baseCoordinate;
myArchiveExtensions = TreeRangeMap.create();
myArchiveExtensions.put(Range.<Integer>all(), archiveExtension);
}
private RepositoryLibrary(String id, String basePath, String baseCoordinate, RangeMap<Integer, String> archiveExtensions) {
this.id = id;
this.basePath = basePath;
this.baseCoordinate = baseCoordinate;
myArchiveExtensions = archiveExtensions;
}
@NotNull
public String getArchiveExtension(int revision) {
String extension = myArchiveExtensions.get(revision);
return extension == null ? "" : extension;
}
}
}