blob: 627100aeb6b988ed81a84b314ed00244ea7a8455 [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.ide.common.repository;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class represents a maven coordinate and allows for comparison at any level.
* <p>
* This class does not directly implement {@link java.lang.Comparable}; instead,
* you should use one of the specific {@link java.util.Comparator} constants based
* on what type of ordering you need.
*/
public class GradleCoordinate {
private static final String NONE = "NONE";
/**
* Maven coordinates take the following form: groupId:artifactId:packaging:classifier:version
* where
* groupId is dot-notated alphanumeric
* artifactId is the name of the project
* packaging is optional and is jar/war/pom/aar/etc
* classifier is optional and provides filtering context
* version uniquely identifies a version.
*
* We only care about coordinates of the following form: groupId:artifactId:revision
* where revision is a series of '.' separated numbers optionally terminated by a '+' character.
*/
/**
* List taken from <a href="http://maven.apache.org/pom.html#Maven_Coordinates">http://maven.apache.org/pom.html#Maven_Coordinates</a>
*/
public enum ArtifactType {
POM("pom"),
JAR("jar"),
MAVEN_PLUGIN("maven-plugin"),
EJB("ejb"),
WAR("war"),
EAR("ear"),
RAR("rar"),
PAR("par"),
AAR("aar");
private final String mId;
ArtifactType(String id) {
mId = id;
}
@Nullable
public static ArtifactType getArtifactType(@Nullable String name) {
if (name != null) {
for (ArtifactType type : ArtifactType.values()) {
if (type.mId.equalsIgnoreCase(name)) {
return type;
}
}
}
return null;
}
@Override
public String toString() {
return mId;
}
}
public static final String PREVIEW_ID = "rc";
/**
* A single component of a revision number: either a number, a string or a list of
* components separated by dashes.
*/
public abstract static class RevisionComponent implements Comparable<RevisionComponent> {
public abstract int asInteger();
public abstract boolean isPreview();
}
public static class NumberComponent extends RevisionComponent {
private final int mNumber;
public NumberComponent(int number) {
mNumber = number;
}
@Override
public String toString() {
return Integer.toString(mNumber);
}
@Override
public int asInteger() {
return mNumber;
}
@Override
public boolean isPreview() {
return false;
}
@Override
public boolean equals(Object o) {
return o instanceof NumberComponent && ((NumberComponent) o).mNumber == mNumber;
}
@Override
public int hashCode() {
return mNumber;
}
@Override
public int compareTo(RevisionComponent o) {
if (o instanceof NumberComponent) {
return mNumber - ((NumberComponent) o).mNumber;
}
if (o instanceof StringComponent) {
return 1;
}
if (o instanceof ListComponent) {
return 1; // 1.0.x > 1-1
}
return 0;
}
}
/**
* Like NumberComponent, but used for numeric strings that have leading zeroes which
* we must preserve
*/
public static class PaddedNumberComponent extends NumberComponent {
private final String mString;
public PaddedNumberComponent(int number, String string) {
super(number);
mString = string;
}
@Override
public String toString() {
return mString;
}
@Override
public boolean equals(Object o) {
return o instanceof PaddedNumberComponent
&& ((PaddedNumberComponent) o).mString.equals(mString);
}
}
public static class StringComponent extends RevisionComponent {
private final String mString;
public StringComponent(String string) {
this.mString = string;
}
@Override
public String toString() {
return mString;
}
@Override
public int asInteger() {
return 0;
}
@Override
public boolean isPreview() {
return mString.startsWith(PREVIEW_ID);
}
@Override
public boolean equals(Object o) {
return o instanceof StringComponent && ((StringComponent) o).mString.equals(mString);
}
@Override
public int hashCode() {
return mString.hashCode();
}
@Override
public int compareTo(RevisionComponent o) {
if (o instanceof NumberComponent) {
return -1;
}
if (o instanceof StringComponent) {
return mString.compareTo(((StringComponent) o).mString);
}
if (o instanceof ListComponent) {
return -1; // 1-sp < 1-1
}
return 0;
}
}
private static class PlusComponent extends RevisionComponent {
@Override
public String toString() {
return "+";
}
@Override
public int asInteger() {
return PLUS_REV_VALUE;
}
@Override
public boolean isPreview() {
return false;
}
@Override
public int compareTo(RevisionComponent o) {
throw new UnsupportedOperationException(
"Please use a specific comparator that knows how to handle +");
}
}
/**
* A list of components separated by dashes.
*/
public static class ListComponent extends RevisionComponent {
private final List<RevisionComponent> mItems = new ArrayList<RevisionComponent>();
private boolean mClosed = false;
public static ListComponent of(RevisionComponent... components) {
ListComponent result = new ListComponent();
for (RevisionComponent component : components) {
result.add(component);
}
return result;
}
public void add(RevisionComponent component) {
mItems.add(component);
}
@Override
public int asInteger() {
return 0;
}
@Override
public boolean isPreview() {
return !mItems.isEmpty() && mItems.get(mItems.size() - 1).isPreview();
}
@Override
public int compareTo(RevisionComponent o) {
if (o instanceof NumberComponent) {
return -1; // 1-1 < 1.0.x
}
if (o instanceof StringComponent) {
return 1; // 1-1 > 1-sp
}
if (o instanceof ListComponent) {
ListComponent rhs = (ListComponent) o;
for (int i = 0; i < mItems.size() && i < rhs.mItems.size(); i++) {
int rc = mItems.get(i).compareTo(rhs.mItems.get(i));
if (rc != 0) return rc;
}
return mItems.size() - rhs.mItems.size();
}
return 0;
}
@Override
public boolean equals(Object o) {
return o instanceof ListComponent && ((ListComponent) o).mItems.equals(mItems);
}
@Override
public int hashCode() {
return mItems.hashCode();
}
@Override
public String toString() {
return Joiner.on("-").join(mItems);
}
}
public static final PlusComponent PLUS_REV = new PlusComponent();
public static final int PLUS_REV_VALUE = -1;
private final String mGroupId;
private final String mArtifactId;
private final ArtifactType mArtifactType;
private final List<RevisionComponent> mRevisions = new ArrayList<RevisionComponent>(3);
private static final Pattern MAVEN_PATTERN =
Pattern.compile("([\\w\\d\\.-]+):([\\w\\d\\.-]+):([^:@]+)(@[\\w-]+)?");
/**
* Constructor
*/
public GradleCoordinate(@NonNull String groupId, @NonNull String artifactId,
@NonNull RevisionComponent... revisions) {
this(groupId, artifactId, Arrays.asList(revisions), null);
}
/**
* Constructor
*/
public GradleCoordinate(@NonNull String groupId, @NonNull String artifactId,
@NonNull int... revisions) {
this(groupId, artifactId, createComponents(revisions), null);
}
private static List<RevisionComponent> createComponents(int[] revisions) {
List<RevisionComponent> result = new ArrayList<RevisionComponent>(revisions.length);
for (int revision : revisions) {
if (revision == PLUS_REV_VALUE) {
result.add(PLUS_REV);
} else {
result.add(new NumberComponent(revision));
}
}
return result;
}
/**
* Constructor
*/
public GradleCoordinate(@NonNull String groupId, @NonNull String artifactId,
@NonNull List<RevisionComponent> revisions, @Nullable ArtifactType type) {
mGroupId = groupId;
mArtifactId = artifactId;
mRevisions.addAll(revisions);
mArtifactType = type;
}
/**
* Create a GradleCoordinate from a string of the form groupId:artifactId:MajorRevision.MinorRevision.(MicroRevision|+)
*
* @param coordinateString the string to parse
* @return a coordinate object or null if the given string was malformed.
*/
@Nullable
public static GradleCoordinate parseCoordinateString(@NonNull String coordinateString) {
if (coordinateString == null) {
return null;
}
Matcher matcher = MAVEN_PATTERN.matcher(coordinateString);
if (!matcher.matches()) {
return null;
}
String groupId = matcher.group(1);
String artifactId = matcher.group(2);
String revision = matcher.group(3);
String typeString = matcher.group(4);
ArtifactType type = null;
if (typeString != null) {
// Strip off the '@' symbol and try to convert
type = ArtifactType.getArtifactType(typeString.substring(1));
}
List<RevisionComponent> revisions = parseRevisionNumber(revision);
return new GradleCoordinate(groupId, artifactId, revisions, type);
}
public static GradleCoordinate parseVersionOnly(@NonNull String revision) {
return new GradleCoordinate(NONE, NONE, parseRevisionNumber(revision), null);
}
@NonNull
public static List<RevisionComponent> parseRevisionNumber(@NonNull String revision) {
List<RevisionComponent> components = new ArrayList<RevisionComponent>();
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < revision.length(); i++) {
char c = revision.charAt(i);
if (c == '.') {
flushBuffer(components, buffer, true);
} else if (c == '+') {
if (buffer.length() > 0) {
flushBuffer(components, buffer, true);
}
components.add(PLUS_REV);
break;
} else if (c == '-') {
flushBuffer(components, buffer, false);
int last = components.size() - 1;
if (last == -1) {
components.add(ListComponent.of(new NumberComponent(0)));
} else if (!(components.get(last) instanceof ListComponent)) {
components.set(last, ListComponent.of(components.get(last)));
}
} else {
buffer.append(c);
}
}
if (buffer.length() > 0 || components.isEmpty()) {
flushBuffer(components, buffer, true);
}
return components;
}
private static void flushBuffer(List<RevisionComponent> components, StringBuilder buffer,
boolean closeList) {
RevisionComponent newComponent;
if (buffer.length() == 0) {
newComponent = new NumberComponent(0);
} else {
String string = buffer.toString();
try {
int number = Integer.parseInt(string);
if (string.length() > 1 && string.charAt(0) == '0') {
newComponent = new PaddedNumberComponent(number, string);
} else {
newComponent = new NumberComponent(number);
}
} catch (NumberFormatException e) {
newComponent = new StringComponent(string);
}
}
buffer.setLength(0);
if (!components.isEmpty() &&
components.get(components.size() - 1) instanceof ListComponent) {
ListComponent component = (ListComponent) components.get(components.size() - 1);
if (!component.mClosed) {
component.add(newComponent);
if (closeList) {
component.mClosed = true;
}
return;
}
}
components.add(newComponent);
}
@Override
public String toString() {
String s = String.format(Locale.US, "%s:%s:%s", mGroupId, mArtifactId, getFullRevision());
if (mArtifactType != null) {
s += "@" + mArtifactType.toString();
}
return s;
}
@Nullable
public String getGroupId() {
return mGroupId;
}
@Nullable
public String getArtifactId() {
return mArtifactId;
}
@Nullable
public String getId() {
if (mGroupId == null || mArtifactId == null) {
return null;
}
return String.format("%s:%s", mGroupId, mArtifactId);
}
@Nullable
public ArtifactType getType() {
return mArtifactType;
}
public boolean acceptsGreaterRevisions() {
return mRevisions.get(mRevisions.size() - 1) == PLUS_REV;
}
public String getFullRevision() {
StringBuilder revision = new StringBuilder();
for (RevisionComponent component : mRevisions) {
if (revision.length() > 0) {
revision.append('.');
}
revision.append(component.toString());
}
return revision.toString();
}
public boolean isPreview() {
return !mRevisions.isEmpty() && mRevisions.get(mRevisions.size() - 1).isPreview();
}
/**
* Returns the major version (X in X.2.3), which can be {@link #PLUS_REV}, or Integer.MIN_VALUE
* if it is not available
*/
public int getMajorVersion() {
return mRevisions.isEmpty() ? Integer.MIN_VALUE : mRevisions.get(0).asInteger();
}
/**
* Returns the minor version (X in 1.X.3), which can be {@link #PLUS_REV}, or Integer.MIN_VALUE
* if it is not available
*/
public int getMinorVersion() {
return mRevisions.size() < 2 ? Integer.MIN_VALUE : mRevisions.get(1).asInteger();
}
/**
* Returns the major version (X in 1.2.X), which can be {@link #PLUS_REV}, or Integer.MIN_VALUE
* if it is not available
*/
public int getMicroVersion() {
return mRevisions.size() < 3 ? Integer.MIN_VALUE : mRevisions.get(2).asInteger();
}
/**
* Returns true if and only if the given coordinate refers to the same group and artifact.
*
* @param o the coordinate to compare with
* @return true iff the other group and artifact match the group and artifact of this
* coordinate.
*/
public boolean isSameArtifact(@NonNull GradleCoordinate o) {
return o.mGroupId.equals(mGroupId) && o.mArtifactId.equals(mArtifactId);
}
@Override
public boolean equals(@NonNull Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GradleCoordinate that = (GradleCoordinate) o;
if (!mRevisions.equals(that.mRevisions)) {
return false;
}
if (!mArtifactId.equals(that.mArtifactId)) {
return false;
}
if (!mGroupId.equals(that.mGroupId)) {
return false;
}
if ((mArtifactType == null) != (that.mArtifactType == null)) {
return false;
}
if (mArtifactType != null && !mArtifactType.equals(that.mArtifactType)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = mGroupId.hashCode();
result = 31 * result + mArtifactId.hashCode();
for (RevisionComponent component : mRevisions) {
result = 31 * result + component.hashCode();
}
if (mArtifactType != null) {
result = 31 * result + mArtifactType.hashCode();
}
return result;
}
/**
* Comparator which compares Gradle versions - and treats a + version as lower
* than a specific number in the same place. This is typically useful when trying
* to for example order coordinates by "most specific".
*/
public static final Comparator<GradleCoordinate> COMPARE_PLUS_LOWER =
new GradleCoordinateComparator(-1);
/**
* Comparator which compares Gradle versions - and treats a + version as higher
* than a specific number. This is typically useful when seeing if a dependency
* is met, e.g. if you require version 0.7.3, comparing it with 0.7.+ would consider
* 0.7.+ higher and therefore satisfying the version requirement.
*/
public static final Comparator<GradleCoordinate> COMPARE_PLUS_HIGHER =
new GradleCoordinateComparator(1);
private static class GradleCoordinateComparator implements Comparator<GradleCoordinate> {
private final int mPlusResult;
private GradleCoordinateComparator(int plusResult) {
mPlusResult = plusResult;
}
@Override
public int compare(@NonNull GradleCoordinate a, @NonNull GradleCoordinate b) {
// Make sure we're comparing apples to apples. If not, compare artifactIds
if (!a.isSameArtifact(b)) {
return a.mArtifactId.compareTo(b.mArtifactId);
}
int sizeA = a.mRevisions.size();
int sizeB = b.mRevisions.size();
int common = Math.min(sizeA, sizeB);
for (int i = 0; i < common; ++i) {
RevisionComponent revision1 = a.mRevisions.get(i);
if (revision1 instanceof PlusComponent) return mPlusResult;
RevisionComponent revision2 = b.mRevisions.get(i);
if (revision2 instanceof PlusComponent) return -mPlusResult;
int delta = revision1.compareTo(revision2);
if (delta != 0) {
return delta;
}
}
if (sizeA == sizeB) {
return 0;
} else {
// Treat X.0 and X.0.0 as equal
List<RevisionComponent> revisionList;
int returnValueIfNonZero;
int from;
int to;
if (sizeA < sizeB) {
revisionList = b.mRevisions;
from = sizeA;
to = sizeB;
returnValueIfNonZero = -1;
} else {
revisionList = a.mRevisions;
from = sizeB;
to = sizeA;
returnValueIfNonZero = 1;
}
for (int i = from; i < to; ++i) {
RevisionComponent revision = revisionList.get(i);
if (revision instanceof NumberComponent) {
if (revision.asInteger() != 0) {
return returnValueIfNonZero;
}
} else {
return returnValueIfNonZero;
}
}
return 0;
}
}
}
}