package io.airlift.airship.coordinator; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.io.ByteProcessor; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import com.google.common.io.Resources; import com.google.inject.Inject; import io.airlift.airship.coordinator.MavenMetadata.SnapshotVersion; import io.airlift.airship.shared.HttpUriBuilder; import io.airlift.airship.shared.MavenCoordinates; import io.airlift.airship.shared.Repository; import io.airlift.log.Logger; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.google.common.base.Charsets.UTF_8; import static com.google.common.collect.Lists.newArrayList; import static io.airlift.airship.shared.HttpUriBuilder.uriBuilderFrom; import static io.airlift.airship.shared.MavenCoordinates.toBinaryGAV; import static io.airlift.airship.shared.MavenCoordinates.toConfigGAV; public class MavenRepository implements Repository { private static final Logger log = Logger.get(MavenRepository.class); private static final Pattern TIMESTAMP_VERSION = Pattern.compile("^(.+)-[0-9]{8}\\.[0-9]{6}\\-[0-9]+$"); private final List<String> defaultGroupIds; private final List<URI> repositoryBases; public MavenRepository(Iterable<String> defaultGroupIds, URI repositoryBase, URI... repositoryBases) { this(defaultGroupIds, ImmutableList.<URI>builder().add(repositoryBase).add(repositoryBases).build()); } public MavenRepository(Iterable<String> defaultGroupIds, Iterable<URI> repositoryBases) { this.defaultGroupIds = ImmutableList.copyOf(defaultGroupIds); for (URI uri : repositoryBases) { Preconditions.checkArgument(uri.toASCIIString().endsWith("/"), "Uri must end with a '/' " + uri); } this.repositoryBases = ImmutableList.copyOf(repositoryBases); } @Inject public MavenRepository(CoordinatorConfig config) { if (config.getDefaultRepositoryGroupId() != null) { this.defaultGroupIds = ImmutableList.copyOf(config.getDefaultRepositoryGroupId()); } else { this.defaultGroupIds = ImmutableList.of(); } Builder<URI> builder = ImmutableList.builder(); for (String binaryRepoBase : config.getRepositories()) { if (!binaryRepoBase.endsWith("/")) { binaryRepoBase = binaryRepoBase + "/"; } builder.add(URI.create(binaryRepoBase)); } repositoryBases = builder.build(); } @Override public String configRelativize(String config) { if (!config.startsWith("@")) { return null; } MavenCoordinates coordinates = MavenCoordinates.fromConfigGAV(config); if (coordinates == null) { return null; } for (String defaultGroupId : defaultGroupIds) { if (defaultGroupId.equals(coordinates.getGroupId())) { return MavenCoordinates.toConfigGAV(new MavenCoordinates( null, coordinates.getArtifactId(), coordinates.getVersion(), coordinates.getPackaging(), coordinates.getClassifier(), coordinates.getFileVersion())); } } return null; } @Override public String configResolve(String config) { MavenCoordinates coordinates = MavenCoordinates.fromConfigGAV(config); if (coordinates == null) { return null; } coordinates = resolve(coordinates); if (coordinates != null) { return toConfigGAV(coordinates); } return null; } @Override public String configShortName(String config) { if (!config.startsWith("@")) { return null; } MavenCoordinates coordinates = MavenCoordinates.fromConfigGAV(config); if (coordinates == null) { return config.substring(0).replaceAll(":", "_"); } return coordinates.getArtifactId(); } @Override public String configUpgrade(String config, String version) { if (!version.startsWith("@")) { return null; } MavenCoordinates coordinates = MavenCoordinates.fromConfigGAV(config); if (coordinates == null) { return null; } coordinates = new MavenCoordinates(coordinates.getGroupId(), coordinates.getArtifactId(), version.substring(1), coordinates.getPackaging(), coordinates.getClassifier(), null); coordinates = resolve(coordinates); if (coordinates != null) { return MavenCoordinates.toConfigGAV(coordinates); } return configResolve(version); } @Override public boolean configEqualsIgnoreVersion(String config1, String config2) { MavenCoordinates coordinates1 = MavenCoordinates.fromConfigGAV(config1); MavenCoordinates coordinates2 = MavenCoordinates.fromConfigGAV(config2); return coordinates1 != null && coordinates2 != null && coordinates1.equalsIgnoreVersion(coordinates2); } @Override public URI configToHttpUri(String config) { MavenCoordinates coordinates = MavenCoordinates.fromConfigGAV(config); if (coordinates == null) { return null; } return toHttpUri(coordinates, true); } @Override public String binaryRelativize(String binary) { MavenCoordinates coordinates = MavenCoordinates.fromBinaryGAV(binary); if (coordinates == null) { return null; } for (String defaultGroupId : defaultGroupIds) { if (defaultGroupId.equals(coordinates.getGroupId())) { return MavenCoordinates.toBinaryGAV(new MavenCoordinates( null, coordinates.getArtifactId(), coordinates.getVersion(), coordinates.getPackaging(), coordinates.getClassifier(), coordinates.getFileVersion())); } } return null; } @Override public String binaryResolve(String binary) { MavenCoordinates coordinates = MavenCoordinates.fromBinaryGAV(binary); if (coordinates == null) { return null; } coordinates = resolve(coordinates); if (coordinates != null) { return toBinaryGAV(coordinates); } return null; } @Override public String binaryUpgrade(String binary, String version) { MavenCoordinates coordinates = MavenCoordinates.fromBinaryGAV(binary); if (coordinates == null) { return null; } coordinates = new MavenCoordinates(coordinates.getGroupId(), coordinates.getArtifactId(), version, coordinates.getPackaging(), coordinates.getClassifier(), null); coordinates = resolve(coordinates); if (coordinates != null) { return MavenCoordinates.toBinaryGAV(coordinates); } return binaryResolve(version); } @Override public boolean binaryEqualsIgnoreVersion(String binary1, String binary2) { MavenCoordinates coordinates1 = MavenCoordinates.fromBinaryGAV(binary1); MavenCoordinates coordinates2 = MavenCoordinates.fromBinaryGAV(binary2); return coordinates1 != null && coordinates2 != null && coordinates1.equalsIgnoreVersion(coordinates2); } @Override public URI binaryToHttpUri(String binary) { MavenCoordinates coordinates = MavenCoordinates.fromBinaryGAV(binary); if (coordinates == null) { return null; } return toHttpUri(coordinates, true); } public URI toHttpUri(MavenCoordinates coordinates, boolean required) { // resolve binary spec groupId or snapshot version coordinates = resolve(coordinates); if (coordinates == null) { return null; } List<URI> checkedUris = newArrayList(); for (URI repositoryBase : repositoryBases) { // build the uri HttpUriBuilder uriBuilder = uriBuilderFrom(repositoryBase); uriBuilder.appendPath(coordinates.getGroupId().replace('.', '/')); uriBuilder.appendPath(coordinates.getArtifactId()); uriBuilder.appendPath(coordinates.getVersion()); StringBuilder fileNameBuilder = new StringBuilder().append(coordinates.getArtifactId()).append('-').append(coordinates.getFileVersion()); if (coordinates.getClassifier() != null) { fileNameBuilder.append('-').append(coordinates.getClassifier()); } fileNameBuilder.append('.').append(coordinates.getPackaging()); uriBuilder.appendPath(fileNameBuilder.toString()); URI uri = uriBuilder.build(); // try to download some of the file if (isValidBinary(uri)) { return uri; } checkedUris.add(uri); } if (required) { throw new RuntimeException("Unable to find binary " + coordinates + " at " + checkedUris); } else { return null; } } public MavenCoordinates resolve(MavenCoordinates coordinates) { if (coordinates.isResolved()) { return coordinates; } List<String> groupIds; if (coordinates.getGroupId() != null) { groupIds = ImmutableList.of(coordinates.getGroupId()); } else { groupIds = defaultGroupIds; } List<MavenCoordinates> matchedCoordinates = newArrayList(); for (String groupId : groupIds) { // check for a file with the exact name MavenCoordinates resolvedSpec = new MavenCoordinates(groupId, coordinates.getArtifactId(), coordinates.getVersion(), coordinates.getPackaging(), coordinates.getClassifier(), coordinates.getFileVersion()); if (toHttpUri(resolvedSpec, false) != null) { matchedCoordinates.add(resolvedSpec); continue; } // check of a timestamped snapshot file if (coordinates.getVersion().contains("SNAPSHOT")) { MavenCoordinates timestampSpec = resolveSnapshotTimestamp(coordinates, groupId); if (timestampSpec != null) { matchedCoordinates.add(timestampSpec); continue; } } // Snapshot revisions are resolved to timestamp version which may need to be converted back to SNAPSHOT for resolution Matcher timestampMatcher = TIMESTAMP_VERSION.matcher(coordinates.getVersion()); if (timestampMatcher.matches()) { MavenCoordinates snapshotSpec = new MavenCoordinates(groupId, coordinates.getArtifactId(), timestampMatcher.group(1) + "-SNAPSHOT", coordinates.getPackaging(), coordinates.getClassifier(), coordinates.getVersion()); if (toHttpUri(snapshotSpec, false) != null) { matchedCoordinates.add(snapshotSpec); } } } if (matchedCoordinates.size() > 1) { throw new RuntimeException("Ambiguous spec " + coordinates + " matched " + matchedCoordinates); } if (matchedCoordinates.isEmpty()) { return null; } return matchedCoordinates.get(0); } private MavenCoordinates resolveSnapshotTimestamp(MavenCoordinates coordinates, String groupId) { for (URI repositoryBase : repositoryBases) { try { // load maven metadata file HttpUriBuilder uriBuilder = uriBuilderFrom(repositoryBase); uriBuilder.appendPath(groupId.replace('.', '/')); uriBuilder.appendPath(coordinates.getArtifactId()); uriBuilder.appendPath(coordinates.getVersion()); uriBuilder.appendPath("maven-metadata.xml"); URI uri = uriBuilder.build(); MavenMetadata metadata = MavenMetadata.unmarshalMavenMetadata(toString(uri)); for (SnapshotVersion snapshotVersion : metadata.versioning.snapshotVersions) { if (coordinates.getPackaging().equals(snapshotVersion.extension) && Objects.equal(coordinates.getClassifier(), snapshotVersion.classifier)) { MavenCoordinates timestampSpec = new MavenCoordinates(groupId, coordinates.getArtifactId(), coordinates.getVersion(), coordinates.getPackaging(), coordinates.getClassifier(), snapshotVersion.value); return timestampSpec; } } } catch (Exception ignored) { // no maven-metadata.xml file... hope this is laid out normally } } return null; } private String toString(URI uri) throws IOException { final URL url = uri.toURL(); return new CharSource() { @Override public Reader openStream() throws IOException { URLConnection connection = url.openConnection(); if (connection instanceof HttpURLConnection) { HttpURLConnection httpConnection = (HttpURLConnection) connection; httpConnection.addRequestProperty("User-Agent", "User-Agent: Apache-Maven/3.0.3 (Java 1.6.0_29; Mac OS X 10.7.2)"); } InputStream in = connection.getInputStream(); return new InputStreamReader(in, UTF_8); } }.read(); } private boolean isValidBinary(URI uri) { log.debug("validating URI: %s", uri); try { ByteSource byteSource = Resources.asByteSource(uri.toURL()); byteSource.read(new ByteProcessor<Void>() { private int count; public boolean processBytes(byte[] buffer, int offset, int length) { count += length; // make sure we got at least 10 bytes return count < 10; } public Void getResult() { return null; } }); return true; } catch (FileNotFoundException e) { log.debug("URI does not exist: %s", uri); } catch (Exception e) { log.debug(e, "error validating URI: %s", uri); } return false; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("MavenRepository"); sb.append("{repositoryBases=").append(repositoryBases); sb.append(", defaultGroupIds=").append(defaultGroupIds); sb.append('}'); return sb.toString(); } }