package org.eclipse.recommenders.models;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.io.FileUtils.listFiles;
import static org.eclipse.recommenders.utils.Fingerprints.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.GetTask;
import org.eclipse.aether.spi.connector.transport.Transporter;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
public class ModelRepositoryTest {
private static final ModelCoordinate COORDINATE = new ModelCoordinate("org.example", "example", "model", "zip",
"1.0.0");
private static final URI METADATA_XML = asUri("org/example/example/1.0.0-SNAPSHOT/maven-metadata.xml");
private static final URI METADATA_XML_SHA1 = asUri("org/example/example/1.0.0-SNAPSHOT/maven-metadata.xml.sha1");
private static final URI METADATA_XML_MD5 = asUri("org/example/example/1.0.0-SNAPSHOT/maven-metadata.xml.md5");
private static final URI EXAMPLE_MODEL_ZIP = asUri(
"org/example/example/1.0.0-SNAPSHOT/example-1.0.0-20140625.000000-1-model.zip");
private static final URI EXAMPLE_MODEL_FALLBACK_ZIP = asUri(
"org/example/example/1.0.0-SNAPSHOT/example-1.0.0-SNAPSHOT-model.zip");
private static final URI EXAMPLE_MODEL_ZIP_SHA1 = asUri(
"org/example/example/1.0.0-SNAPSHOT/example-1.0.0-20140625.000000-1-model.zip.sha1");
private static final URI EXAMPLE_MODEL_ZIP_MD5 = asUri(
"org/example/example/1.0.0-SNAPSHOT/example-1.0.0-20140625.000000-1-model.zip.md5");
private static final String REPO_URL = "http://www.example.org/repo";
private static final String[] CHECKSUM_EXTENSIONS = new String[] { "sha1", "md5" };
@Rule
public final TemporaryFolder tmp = new TemporaryFolder();
@Test
public void testGetLocationOnEmptyLocalRepository() throws Exception {
Transporter transporter = mockTransporter(Collections.<URI, Answer<Void>>emptyMap());
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.getLocation(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
verify(transporter, never()).get(Mockito.any(GetTask.class));
}
@Test
public void testResolveSucceeds() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(Files.toString(resolvedModel.get(), UTF_8), is(equalTo("model data")));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_SHA1))));
verify(transporter, times(4)).get(Mockito.any(GetTask.class));
assertThat(listFiles(tmp.getRoot(), CHECKSUM_EXTENSIONS, true).size(), is(0));
}
@Test
public void testResolveSucceedsWithoutSha1Checksums() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_MD5, new SucessfulDownload(md5(metadata)),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_MD5, new SucessfulDownload(md5(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(Files.toString(resolvedModel.get(), UTF_8), is(equalTo("model data")));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_MD5))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_MD5))));
verify(transporter, times(6)).get(Mockito.any(GetTask.class));
assertThat(listFiles(tmp.getRoot(), CHECKSUM_EXTENSIONS, true).size(), is(0));
}
@Test
public void testGetLocationSucceedsAfterSuccessfulResolve() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
Optional<File> cachedModel = sut.getLocation(COORDINATE, false);
assertThat(cachedModel, is(equalTo(resolvedModel)));
assertThat(Files.toString(cachedModel.get(), UTF_8), is(equalTo("model data")));
}
@Test
public void testGetLocationDoesNotBlockDuringResolve() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
final CountDownLatch startDownload = new CountDownLatch(1);
final CountDownLatch finishDownload = new CountDownLatch(1);
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new LongRunningDownload(model, startDownload, finishDownload),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
final IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
ExecutorService executors = Executors.newFixedThreadPool(2);
Future<Optional<File>> resolvedModel = executors.submit(new Callable<Optional<File>>() {
@Override
public Optional<File> call() {
return sut.resolve(COORDINATE, false);
};
});
Future<Optional<File>> cachedModel = executors.submit(new Callable<Optional<File>>() {
@Override
public Optional<File> call() throws InterruptedException {
startDownload.await();
Optional<File> preResolveLocation = sut.getLocation(COORDINATE, false);
finishDownload.countDown();
return preResolveLocation;
};
});
assertThat(resolvedModel.get().isPresent(), is(true));
assertThat(cachedModel.get().isPresent(), is(false));
}
@Test
public void testResolveFailsOnChecksumMismatchForMetadata() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload("0000000000000000000000000000000000000000"),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
// Aether retries download once on checksum mismatch
verify(transporter, times(2)).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter, times(2)).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
// Aether falls back on literal "SNAPSHOT" URI
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_FALLBACK_ZIP))));
verify(transporter, times(5)).get(Mockito.any(GetTask.class));
}
@Test
public void testResolveFailsOnMissingChecksumForMetadata() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_MD5))));
// Aether falls back on literal "SNAPSHOT" URI
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_FALLBACK_ZIP))));
verify(transporter, times(4)).get(Mockito.any(GetTask.class));
}
@Test
public void testResolveFailsOnChecksumMismatchForModel() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload("0000000000000000000000000000000000000000")));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
// Aether retries download once on checksum mismatch
verify(transporter, times(2)).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter, times(2)).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_SHA1))));
verify(transporter, times(6)).get(Mockito.any(GetTask.class));
}
@Test
public void testResolveFailsOnMissingChecksumForModel() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new SucessfulDownload(model)));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP_MD5))));
verify(transporter, times(5)).get(Mockito.any(GetTask.class));
}
@Test
public void testResolveFailsOnConnectionFailureOnModel() throws Exception {
final String metadata = mavenMetadata(COORDINATE);
final String model = "model data";
/* @formatter:off */
Transporter transporter = mockTransporter(ImmutableMap.of(
METADATA_XML, new SucessfulDownload(metadata),
METADATA_XML_SHA1, new SucessfulDownload(sha1(metadata)),
EXAMPLE_MODEL_ZIP, new ConnectionFailure(),
EXAMPLE_MODEL_ZIP_SHA1, new SucessfulDownload(sha1(model))));
/* @formatter:on */
RepositorySystem system = createRepositorySystem(transporter);
IModelRepository sut = new ModelRepository(system, tmp.getRoot(), REPO_URL);
Optional<File> resolvedModel = sut.resolve(COORDINATE, false);
assertThat(resolvedModel.isPresent(), is(false));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter, times(3)).get(Mockito.any(GetTask.class));
Optional<File> reresolvedModel = sut.resolve(COORDINATE, false);
assertThat(reresolvedModel.isPresent(), is(false));
// Aether makes no new request but caches negative results...
verify(transporter, times(3)).get(Mockito.any(GetTask.class));
Optional<File> forcedModel = sut.resolve(COORDINATE, true);
assertThat(forcedModel.isPresent(), is(false));
// ...unless forced
verify(transporter, times(1 + 1)).get(argThat(hasLocation(equalTo(METADATA_XML))));
verify(transporter, times(1 + 1)).get(argThat(hasLocation(equalTo(METADATA_XML_SHA1))));
verify(transporter, times(1 + 1)).get(argThat(hasLocation(equalTo(EXAMPLE_MODEL_ZIP))));
verify(transporter, times(3 + 3)).get(Mockito.any(GetTask.class));
}
private String mavenMetadata(ModelCoordinate coordinate) {
/* @formatter:off */
final String metadata = String.format("<?xml version='1.0' encoding='UTF-8'?>"
+ "<metadata modelVersion='1.1.0'>"
+ "<groupId>%1$s</groupId>"
+ "<artifactId>%2$s</artifactId>"
+ "<version>%3$s-SNAPSHOT</version>"
+ "<versioning>"
+ "<snapshot>"
+ "<timestamp>20140625.000000</timestamp>"
+ "<buildNumber>1</buildNumber>"
+ "</snapshot>"
+ "<lastUpdated>20140625000000</lastUpdated>"
+ "<snapshotVersions>"
+ "<snapshotVersion>"
+ "<classifier>%4$s</classifier>"
+ "<extension>%5$s</extension>"
+ "<value>%3$s-20140625.000000-1</value>"
+ "<updated>20140625000000</updated>"
+ "</snapshotVersion>"
+ "</snapshotVersions>"
+ "</versioning>"
+ "</metadata>",
coordinate.getGroupId(), coordinate.getArtifactId(), coordinate.getVersion(), coordinate.getClassifier(),
coordinate.getExtension());
/* @formatter:on */
return metadata;
}
private Transporter mockTransporter(Map<URI, ? extends Answer<Void>> resources) throws Exception {
Transporter transporter = mock(Transporter.class);
when(transporter.classify(Mockito.any(FileNotFoundException.class))).thenReturn(Transporter.ERROR_NOT_FOUND);
doAnswer(new MissingResource()).when(transporter).get(Mockito.any(GetTask.class));
for (Entry<URI, ? extends Answer<Void>> resource : resources.entrySet()) {
doAnswer(resource.getValue()).when(transporter).get(argThat(hasLocation(equalTo(resource.getKey()))));
}
return transporter;
}
private static RepositorySystem createRepositorySystem(Transporter transporter) throws Exception {
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
TransporterFactory transporterFactory = mock(TransporterFactory.class);
when(transporterFactory.newInstance(Mockito.any(RepositorySystemSession.class),
Mockito.any(RemoteRepository.class))).thenReturn(transporter);
locator.setServices(TransporterFactory.class, transporterFactory);
return locator.getService(RepositorySystem.class);
}
private static class SucessfulDownload implements Answer<Void> {
private final String contents;
public SucessfulDownload(String contents) {
this.contents = contents;
}
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
GetTask task = (GetTask) invocation.getArguments()[0];
OutputStream out = task.newOutputStream();
byte[] bytes = contents.getBytes(UTF_8);
task.getListener().transportStarted(0, bytes.length);
out.write(bytes);
task.getListener().transportProgressed(ByteBuffer.wrap(bytes));
out.close();
return null;
}
}
private static class LongRunningDownload extends SucessfulDownload {
private CountDownLatch startDownload;
private CountDownLatch finishDownload;
public LongRunningDownload(String contents, CountDownLatch startDownload, CountDownLatch finishDownload) {
super(contents);
this.startDownload = startDownload;
this.finishDownload = finishDownload;
}
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
startDownload.countDown();
super.answer(invocation);
finishDownload.await();
return null;
}
}
private static class MissingResource implements Answer<Void> {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
throw new FileNotFoundException();
}
}
private static class ConnectionFailure implements Answer<Void> {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
throw new IOException();
}
}
private static Matcher<GetTask> hasLocation(Matcher<URI> matcher) {
return hasProperty("location", matcher);
}
private static URI asUri(String uri) {
try {
return new URI(uri);
} catch (URISyntaxException e) {
throw Throwables.propagate(e);
}
}
}