// Copyright 2017 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.workspace.maven; import static com.google.devtools.build.workspace.maven.ArtifactBuilder.InvalidArtifactCoordinateException; import com.google.common.annotations.VisibleForTesting; import java.util.List; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.resolution.VersionRangeResolutionException; /** * Given a Maven coordinate with a version specification resolves the version of the coordinate in a * similar fashion as Maven. Version specifications can include hard and soft pins as well as various * forms of version ranges. When given a version range, Maven selects the highest available version. * For a soft pin, it selects the pinned version or the nearest valid version. * * Documentation on Maven's versioning scheme can be found here: * http://maven.apache.org/enforcer/enforcer-rules/versionRanges.html */ class VersionResolver { private final Aether aether; VersionResolver(Aether aether) { this.aether = aether; } /** * Given a maven coordinate and its version specifications, selects the highest version * if it is a version range or returns the pinned version if is a hard or soft pin. * For soft pins, if that version does not exist it selects the nearest version. */ String resolveVersion(String groupId, String artifactId, String versionSpec) throws InvalidArtifactCoordinateException { List<String> versions; try { versions = requestVersionList(groupId, artifactId, versionSpec); } catch (VersionRangeResolutionException e) { String errorMessage = messageForInvalidArtifact(groupId, artifactId, versionSpec, e.getMessage()); throw new InvalidArtifactCoordinateException(errorMessage); } if (isInvalidRangeResult(versions)) { String errorMessage = messageForInvalidArtifact(groupId, artifactId, versionSpec, "Invalid Range Result"); throw new InvalidArtifactCoordinateException(errorMessage); } return selectVersion(versionSpec, versions); } /** * Given a set of maven coordinates, obtains a list of valid versions in ascending order. * Note, for soft pinned versions, it interprets it as the following version range: "[version,)" */ private List<String> requestVersionList(String groupId, String artifactId, String versionSpec) throws VersionRangeResolutionException, InvalidArtifactCoordinateException { String transformedSpec = makeVersionRange(versionSpec); Artifact artifact = ArtifactBuilder.fromCoords(groupId, artifactId, transformedSpec); return aether.requestVersionRange(artifact); } /** * Given a list of potential valid versions (in ascending order), selects the appropriate version * based on the following heuristic: (1) If it is a version range, it selects the highest version. * (2) If it is a soft pinned version, it selects the earliest valid version. */ private String selectVersion(String versionSpec, List<String> versions) { int index = (isVersionRange(versionSpec)) ? versions.size() - 1 : 0; return versions.get(index); } private boolean isInvalidRangeResult(List<String> result) { return result == null || result.isEmpty(); } /** default error message */ private static String messageForInvalidArtifact( String groupId, String artifactId, String versionSpec, String errorMessage) { return String.format("Unable to find a version for %s:%s:%s due to %s", groupId, artifactId, versionSpec, errorMessage); } /** * Crudely checks whether a version specification is a version range. * Leaves checks for semantic correctness to maven. * By definition, any range must start with an open bracket or parenthesis. */ @VisibleForTesting static boolean isVersionRange(String versionSpec) { return versionSpec.charAt(0) == '(' || versionSpec.charAt(0) == '['; } /** * Transforms all version specifications into a version range. For soft pinned versions like * "3.0", it transforms it into "[3.0,)" */ private static String makeVersionRange(String versionSpec) { if (isVersionRange(versionSpec)) { return versionSpec; } // is a soft pinned version. return String.format("[%s,)", versionSpec); } /** * Creates a VersionResolver with the default Aether settings. */ public static VersionResolver defaultResolver() { return new VersionResolver(Aether.defaultOption()); } }