| /* |
| * 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; |
| } |
| } |
| } |
| } |