From 92b2a6669392bd410e9a60749656a49f3e309cc0 Mon Sep 17 00:00:00 2001 From: Amadornes Date: Mon, 2 Jul 2018 07:12:27 +0200 Subject: Initial (untested) version of Artifactural --- .../artifactural/api/artifact/Artifact.java | 35 +++ .../api/artifact/ArtifactIdentifier.java | 61 +++++ .../api/artifact/ArtifactMetadata.java | 13 + .../artifactural/api/artifact/ArtifactType.java | 5 + .../artifactural/api/artifact/Internal.java | 84 ++++++ .../api/artifact/MissingArtifactException.java | 9 + .../artifactural/api/artifact/Streamable.java | 11 + .../artifactural/api/cache/ArtifactCache.java | 9 + .../api/repository/ArtifactProvider.java | 28 ++ .../artifactural/api/repository/Repository.java | 10 + .../api/transform/ArtifactPipeline.java | 11 + .../api/transform/ArtifactTransformer.java | 54 ++++ .../artifactural/gradle/DependencyResolver.java | 83 ++++++ .../artifactural/gradle/GradleArtifact.java | 26 ++ .../gradle/GradleRepositoryAdapter.java | 303 +++++++++++++++++++++ .../artifactural/gradle/ReflectionUtils.java | 52 ++++ .../artifactural/base/artifact/ArtifactBase.java | 48 ++++ .../base/artifact/ArtifactIdentifierImpl.java | 42 +++ .../base/artifact/SimpleArtifactMetadata.java | 57 ++++ .../base/artifact/StreamableArtifact.java | 56 ++++ .../artifactural/base/cache/ArtifactCacheBase.java | 33 +++ .../base/cache/LocatedArtifactCache.java | 31 +++ .../base/repository/ArtifactProviderBuilder.java | 81 ++++++ .../base/repository/SimpleRepository.java | 25 ++ .../base/transform/ExclusiveTransformer.java | 77 ++++++ .../base/transform/SimpleArtifactPipeline.java | 46 ++++ 26 files changed, 1290 insertions(+) create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/ArtifactIdentifier.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/ArtifactMetadata.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/ArtifactType.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/Internal.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java create mode 100644 src/api/java/com/amadornes/artifactural/api/artifact/Streamable.java create mode 100644 src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java create mode 100644 src/api/java/com/amadornes/artifactural/api/repository/ArtifactProvider.java create mode 100644 src/api/java/com/amadornes/artifactural/api/repository/Repository.java create mode 100644 src/api/java/com/amadornes/artifactural/api/transform/ArtifactPipeline.java create mode 100644 src/api/java/com/amadornes/artifactural/api/transform/ArtifactTransformer.java create mode 100644 src/gradlecomp/java/com/amadornes/artifactural/gradle/DependencyResolver.java create mode 100644 src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleArtifact.java create mode 100644 src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java create mode 100644 src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java create mode 100644 src/main/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java create mode 100644 src/main/java/com/amadornes/artifactural/base/artifact/ArtifactIdentifierImpl.java create mode 100644 src/main/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java create mode 100644 src/main/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java create mode 100644 src/main/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java create mode 100644 src/main/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java create mode 100644 src/main/java/com/amadornes/artifactural/base/repository/ArtifactProviderBuilder.java create mode 100644 src/main/java/com/amadornes/artifactural/base/repository/SimpleRepository.java create mode 100644 src/main/java/com/amadornes/artifactural/base/transform/ExclusiveTransformer.java create mode 100644 src/main/java/com/amadornes/artifactural/base/transform/SimpleArtifactPipeline.java diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java b/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java new file mode 100644 index 0000000..c82bbd9 --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java @@ -0,0 +1,35 @@ +package com.amadornes.artifactural.api.artifact; + +import com.amadornes.artifactural.api.cache.ArtifactCache; +import com.amadornes.artifactural.api.transform.ArtifactTransformer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.function.UnaryOperator; + +public interface Artifact { + + static Artifact none() { + return Internal.NO_ARTIFACT; + } + + ArtifactIdentifier getIdentifier(); + + ArtifactMetadata getMetadata(); + + ArtifactType getType(); + + Artifact withMetadata(ArtifactMetadata metadata); + + Artifact apply(ArtifactTransformer transformer); + + Artifact cache(ArtifactCache cache, String specifier); + + boolean isPresent(); + + InputStream openStream() throws IOException, MissingArtifactException; + +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactIdentifier.java b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactIdentifier.java new file mode 100644 index 0000000..57c263a --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactIdentifier.java @@ -0,0 +1,61 @@ +package com.amadornes.artifactural.api.artifact; + +import java.util.function.Predicate; + +public interface ArtifactIdentifier { + + static ArtifactIdentifier none() { + return Internal.NO_IDENTIFIER; + } + + String getGroup(); + + String getName(); + + String getVersion(); + + String getClassifier(); + + String getExtension(); + + static Predicate groupMatches(String group) { + return identifier -> identifier.getGroup().matches(group); + } + + static Predicate nameMatches(String name) { + return identifier -> identifier.getName().matches(name); + } + + static Predicate versionMatches(String version) { + return identifier -> identifier.getVersion().matches(version); + } + + static Predicate classifierMatches(String classifier) { + return identifier -> identifier.getClassifier().matches(classifier); + } + + static Predicate extensionMatches(String extension) { + return identifier -> identifier.getExtension().matches(extension); + } + + static Predicate groupEquals(String group) { + return identifier -> identifier.getGroup().equals(group); + } + + static Predicate nameEquals(String name) { + return identifier -> identifier.getName().equals(name); + } + + static Predicate versionEquals(String version) { + return identifier -> identifier.getVersion().equals(version); + } + + static Predicate classifierEquals(String classifier) { + return identifier -> identifier.getClassifier().equals(classifier); + } + + static Predicate extensionEquals(String extension) { + return identifier -> identifier.getExtension().equals(extension); + } + +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactMetadata.java b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactMetadata.java new file mode 100644 index 0000000..1dcd838 --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactMetadata.java @@ -0,0 +1,13 @@ +package com.amadornes.artifactural.api.artifact; + +public interface ArtifactMetadata { + + static ArtifactMetadata empty() { + return (ArtifactMetadata) null; + } + + ArtifactMetadata with(String key, String value); + + String getHash(); + +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactType.java b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactType.java new file mode 100644 index 0000000..1bd0f97 --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/ArtifactType.java @@ -0,0 +1,5 @@ +package com.amadornes.artifactural.api.artifact; + +public enum ArtifactType { + BINARY, SOURCE, OTHER +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java b/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java new file mode 100644 index 0000000..41201df --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java @@ -0,0 +1,84 @@ +package com.amadornes.artifactural.api.artifact; + +import com.amadornes.artifactural.api.cache.ArtifactCache; +import com.amadornes.artifactural.api.transform.ArtifactTransformer; + +import java.io.IOException; +import java.io.InputStream; + +class Internal { + + static final ArtifactIdentifier NO_IDENTIFIER = new ArtifactIdentifier() { + + @Override + public String getGroup() { + return "missing"; + } + + @Override + public String getName() { + return "missing"; + } + + @Override + public String getVersion() { + return "0.0.0"; + } + + @Override + public String getClassifier() { + return ""; + } + + @Override + public String getExtension() { + return "missing"; + } + + }; + + static final Artifact NO_ARTIFACT = new Artifact() { + + @Override + public ArtifactIdentifier getIdentifier() { + return ArtifactIdentifier.none(); + } + + @Override + public ArtifactMetadata getMetadata() { + return ArtifactMetadata.empty(); + } + + @Override + public ArtifactType getType() { + return ArtifactType.OTHER; + } + + @Override + public Artifact withMetadata(ArtifactMetadata metadata) { + return this; + } + + @Override + public Artifact apply(ArtifactTransformer transformer) { + return this; + } + + @Override + public Artifact cache(ArtifactCache cache, String specifier) { + return this; + } + + @Override + public boolean isPresent() { + return false; + } + + @Override + public InputStream openStream() throws MissingArtifactException { + throw new MissingArtifactException(getIdentifier()); + } + + }; + +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java b/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java new file mode 100644 index 0000000..479909c --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java @@ -0,0 +1,9 @@ +package com.amadornes.artifactural.api.artifact; + +public class MissingArtifactException extends RuntimeException { + + public MissingArtifactException(ArtifactIdentifier identifier) { + super("Could not find artifact: " + identifier); + } + +} diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/Streamable.java b/src/api/java/com/amadornes/artifactural/api/artifact/Streamable.java new file mode 100644 index 0000000..1b7de8f --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/artifact/Streamable.java @@ -0,0 +1,11 @@ +package com.amadornes.artifactural.api.artifact; + +import java.io.IOException; +import java.io.InputStream; + +@FunctionalInterface +public interface Streamable { + + InputStream openStream() throws IOException; + +} diff --git a/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java b/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java new file mode 100644 index 0000000..4f86798 --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java @@ -0,0 +1,9 @@ +package com.amadornes.artifactural.api.cache; + +import com.amadornes.artifactural.api.artifact.Artifact; + +public interface ArtifactCache { + + Artifact store(Artifact artifact, String specifier); + +} diff --git a/src/api/java/com/amadornes/artifactural/api/repository/ArtifactProvider.java b/src/api/java/com/amadornes/artifactural/api/repository/ArtifactProvider.java new file mode 100644 index 0000000..c9f21ee --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/repository/ArtifactProvider.java @@ -0,0 +1,28 @@ +package com.amadornes.artifactural.api.repository; + +import com.amadornes.artifactural.api.artifact.Artifact; + +import java.util.function.Function; +import java.util.function.Predicate; + +public interface ArtifactProvider { + + Artifact getArtifact(I info); + + interface Builder { + + Builder filter(Predicate filter); + + Builder mapInfo(Function mapper); + + Complete provide(ArtifactProvider provider); + + interface Complete extends ArtifactProvider { + + Complete provide(ArtifactProvider provider); + + } + + } + +} diff --git a/src/api/java/com/amadornes/artifactural/api/repository/Repository.java b/src/api/java/com/amadornes/artifactural/api/repository/Repository.java new file mode 100644 index 0000000..3cf456d --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/repository/Repository.java @@ -0,0 +1,10 @@ +package com.amadornes.artifactural.api.repository; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; + +public interface Repository { + + Artifact getArtifact(ArtifactIdentifier identifier); + +} diff --git a/src/api/java/com/amadornes/artifactural/api/transform/ArtifactPipeline.java b/src/api/java/com/amadornes/artifactural/api/transform/ArtifactPipeline.java new file mode 100644 index 0000000..014046f --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/transform/ArtifactPipeline.java @@ -0,0 +1,11 @@ +package com.amadornes.artifactural.api.transform; + +import com.amadornes.artifactural.api.cache.ArtifactCache; + +public interface ArtifactPipeline extends ArtifactTransformer { + + ArtifactPipeline apply(ArtifactTransformer transformer); + + ArtifactPipeline cache(ArtifactCache cache, String specifier); + +} diff --git a/src/api/java/com/amadornes/artifactural/api/transform/ArtifactTransformer.java b/src/api/java/com/amadornes/artifactural/api/transform/ArtifactTransformer.java new file mode 100644 index 0000000..e24e790 --- /dev/null +++ b/src/api/java/com/amadornes/artifactural/api/transform/ArtifactTransformer.java @@ -0,0 +1,54 @@ +package com.amadornes.artifactural.api.transform; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactMetadata; + +import java.util.Set; +import java.util.function.UnaryOperator; + +public interface ArtifactTransformer { + + static ArtifactTransformer of(UnaryOperator operator) { + return new ArtifactTransformer() { + @Override + public Artifact transform(Artifact artifact) { + return operator.apply(artifact); + } + + @Override + public ArtifactMetadata withInfo(ArtifactMetadata metadata) { + return metadata; + } + }; + } + + static ArtifactTransformer exclude(Set filters) { + return (ArtifactTransformer) new Object(); + } + + default boolean appliesTo(Artifact artifact) { + return true; + } + + Artifact transform(Artifact artifact); + + ArtifactMetadata withInfo(ArtifactMetadata metadata); + + default ArtifactTransformer andThen(ArtifactTransformer other) { + ArtifactTransformer current = this; + return new ArtifactTransformer() { + + @Override + public Artifact transform(Artifact artifact) { + return other.transform(current.transform(artifact)); + } + + @Override + public ArtifactMetadata withInfo(ArtifactMetadata metadata) { + return other.withInfo(current.withInfo(metadata)); + } + + }; + } + +} diff --git a/src/gradlecomp/java/com/amadornes/artifactural/gradle/DependencyResolver.java b/src/gradlecomp/java/com/amadornes/artifactural/gradle/DependencyResolver.java new file mode 100644 index 0000000..12364d7 --- /dev/null +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/DependencyResolver.java @@ -0,0 +1,83 @@ +package com.amadornes.artifactural.gradle; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.*; +import org.gradle.internal.impldep.com.google.common.cache.Cache; +import org.gradle.internal.impldep.com.google.common.cache.CacheBuilder; + +import java.io.File; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class DependencyResolver { + + private final Project project; + private final AtomicInteger counter = new AtomicInteger(0); + private final Cache>> resolved = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build(); + + public DependencyResolver(Project project) { + this.project = project; + } + + /** + * Resolves a dependency, downloading the file and its transitives + * if not cached and returns the set of files. + */ + public Set resolveDependency(Dependency dependency) { + if (dependency instanceof FileCollectionDependency) { + return ((FileCollectionDependency) dependency).getFiles().getFiles(); + } + String name = dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion(); + if (dependency instanceof ModuleDependency) { + Set artifacts = ((ModuleDependency) dependency).getArtifacts(); + if (!artifacts.isEmpty()) { + DependencyArtifact artifact = artifacts.iterator().next(); + name += ":" + artifact.getClassifier() + "@" + artifact.getExtension(); + } + } + + // If this dep is being resolved on another thread, let it do it + CompletableFuture> future; + boolean found = true; + synchronized (resolved) { + future = resolved.getIfPresent(name); + if (future == null) { + resolved.put(name, future = new CompletableFuture<>()); + found = false; + } + } + + if (found) { + try { + return future.get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + + // No other thread is resolving this dep and we've claimed it, so let's go! + int currentID = counter.getAndIncrement(); + Configuration cfg = project.getConfigurations().maybeCreate("resolve_dep_" + currentID); + cfg.getDependencies().add(dependency); + Set files = cfg.resolve(); + project.getConfigurations().remove(cfg); + future.complete(files); + return files; + } + + /** + * Resolves a dependency, downloading the file and its transitives + * if not cached and returns the set of files. + */ + public Set resolveDependency(Object dependency, boolean transitive) { + Dependency dep = project.getDependencies().create(dependency); + if (dep instanceof ClientModule) { + dep = ((ClientModule) dep).copy().setTransitive(transitive); + } + return resolveDependency(dep); + } + +} diff --git a/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleArtifact.java b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleArtifact.java new file mode 100644 index 0000000..2e742ed --- /dev/null +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleArtifact.java @@ -0,0 +1,26 @@ +package com.amadornes.artifactural.gradle; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; +import com.amadornes.artifactural.api.artifact.ArtifactType; +import com.amadornes.artifactural.base.artifact.StreamableArtifact; + +import java.io.File; +import java.util.Set; + +public class GradleArtifact { + + public static Artifact maven(DependencyResolver resolver, ArtifactIdentifier identifier, ArtifactType type) { + Set files = resolver.resolveDependency( + identifier.getGroup() + + ":" + identifier.getName() + + ":" + identifier.getVersion() + + (identifier.getClassifier().isEmpty() ? "" : ":" + identifier.getClassifier()) + + (identifier.getExtension().isEmpty() ? "" : "@" + identifier.getExtension()), + false + ); + if (files.isEmpty()) return Artifact.none(); + return StreamableArtifact.ofJar(identifier, type, files.iterator().next()); + } + +} diff --git a/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java new file mode 100644 index 0000000..a601a1e --- /dev/null +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java @@ -0,0 +1,303 @@ +package com.amadornes.artifactural.gradle; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; +import com.amadornes.artifactural.api.repository.Repository; +import com.amadornes.artifactural.base.artifact.ArtifactIdentifierImpl; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectCollection; +import org.gradle.api.Transformer; +import org.gradle.api.artifacts.dsl.RepositoryHandler; +import org.gradle.api.artifacts.repositories.ArtifactRepository; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.ConfiguredModuleComponentRepository; +import org.gradle.api.internal.artifacts.repositories.AbstractArtifactRepository; +import org.gradle.api.internal.artifacts.repositories.DefaultMavenArtifactRepository; +import org.gradle.api.internal.artifacts.repositories.ResolutionAwareRepository; +import org.gradle.api.internal.artifacts.repositories.resolver.ExternalResourceArtifactResolver; +import org.gradle.api.internal.artifacts.repositories.resolver.ExternalResourceResolver; +import org.gradle.api.internal.artifacts.repositories.resolver.MavenResolver; +import org.gradle.api.resources.ResourceException; +import org.gradle.internal.impldep.com.google.common.io.CountingInputStream; +import org.gradle.internal.impldep.org.apache.commons.io.IOUtils; +import org.gradle.internal.resource.*; +import org.gradle.internal.resource.metadata.DefaultExternalResourceMetaData; +import org.gradle.internal.resource.metadata.ExternalResourceMetaData; + +import javax.annotation.Nullable; +import java.io.*; +import java.net.URI; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GradleRepositoryAdapter extends AbstractArtifactRepository implements ResolutionAwareRepository { + + private static final Pattern URL_PATTERN = Pattern.compile( + "^/(?\\S+(?:/\\S+)*)/(?\\S+)/(?\\S+)/" + + "\\2-\\3(?:-(?[^.\\s]+))?\\.(?\\S+)$"); + + public static GradleRepositoryAdapter add(RepositoryHandler handler, String name, Object url, Repository repository) { + // Create the real maven test we'll be using and remove it + MavenArtifactRepository maven = handler.maven($ -> { + $.setName(name); + $.setUrl(url); + }); + handler.remove(maven); + + // Add our own custom test instead, using the real one in the background + GradleRepositoryAdapter repo = new GradleRepositoryAdapter((DefaultMavenArtifactRepository) maven, repository); + handler.add(repo); + return repo; + } + + private final DefaultMavenArtifactRepository maven; + private final Repository repository; + + private GradleRepositoryAdapter(DefaultMavenArtifactRepository maven, Repository repository) { + this.maven = maven; + this.repository = repository; + } + + @Override + public String getName() { + return maven.getName(); // Proxy to the real repo + } + + @Override + public void setName(String name) { + maven.setName(name); // Proxy to the real repo + } + + @Override + public String getDisplayName() { + return maven.getDisplayName(); // Proxy to the real repo + } + + @Override + public void onAddToContainer(NamedDomainObjectCollection container) { + // No-op. The real repo will get this already + } + + @Override + public ConfiguredModuleComponentRepository createResolver() { + MavenResolver resolver = (MavenResolver) maven.createResolver(); + ExternalResourceRepository repo = new StreamingRepo(); + + ExternalResourceArtifactResolver artifactResolver = ReflectionUtils.invoke(resolver, ExternalResourceResolver.class, "createArtifactResolver"); + ReflectionUtils.alter(resolver, "repository", prev -> repo); + ReflectionUtils.alter(resolver, "mavenMetaDataLoader.cacheAwareExternalResourceAccessor.delegate", prev -> repo); + ReflectionUtils.alter(artifactResolver, "repository", prev -> repo); + + return resolver; + } + + private class StreamingRepo implements ExternalResourceRepository { + + @Override + public ExternalResourceRepository withProgressLogging() { + return this; + } + + @Override + public ExternalResource resource(ExternalResourceName name, boolean revalidate) { + URI uri = name.getUri(); + Matcher matcher = URL_PATTERN.matcher(uri.getPath()); + if (!matcher.matches()) return new NullExternalResource(uri); + ArtifactIdentifier identifier = new ArtifactIdentifierImpl( + matcher.group("group").replace('/', '.'), + matcher.group("name"), + matcher.group("version"), + matcher.group("classifier"), + matcher.group("extension")); + Artifact artifact = repository.getArtifact(identifier); + if (!artifact.isPresent()) return new NullExternalResource(uri); + return new CustomArtifactExternalResource(uri, artifact); + } + + @Override + public ExternalResource resource(ExternalResourceName name) { + return resource(name, false); + } + + } + + private class CustomArtifactExternalResource extends AbstractExternalResource { + + private final URI uri; + private final Artifact artifact; + + private CustomArtifactExternalResource(URI uri, Artifact artifact) { + this.uri = uri; + this.artifact = artifact; + } + + @Override + public String getDisplayName() { + return uri.toString(); + } + + @Override + public URI getURI() { + return uri; + } + + @Nullable + @Override + public ExternalResourceReadResult writeToIfPresent(File file) { + try { + if (!artifact.isPresent()) return null; + FileOutputStream out = new FileOutputStream(file); + ExternalResourceReadResult result = writeTo(out); + out.close(); + return result; + } catch (IOException ex) { + return null; + } + } + + @Override + public ExternalResourceReadResult writeTo(OutputStream out) throws ResourceException { + return withContent(in -> { + try { + IOUtils.copy(in, out); + } catch (IOException ex) { + throw ResourceExceptions.failure(uri, "Failed to write resource!", ex); + } + }); + } + + @Override + public ExternalResourceReadResult withContent(Action action) throws ResourceException { + try { + if (!artifact.isPresent()) throw ResourceExceptions.getMissing(uri); + CountingInputStream in = new CountingInputStream(artifact.openStream()); + action.execute(in); + in.close(); + return ExternalResourceReadResult.of(in.getCount()); + } catch (IOException ex) { + throw ResourceExceptions.failure(uri, "Failed to write resource!", ex); + } + } + + @Nullable + @Override + public ExternalResourceReadResult withContentIfPresent(Transformer transformer) { + try { + if (!artifact.isPresent()) return null; + CountingInputStream in = new CountingInputStream(artifact.openStream()); + T result = transformer.transform(in); + in.close(); + return ExternalResourceReadResult.of(in.getCount(), result); + } catch (IOException ex) { + return null; + } + } + + @Nullable + @Override + public ExternalResourceReadResult withContentIfPresent(ContentAction contentAction) { + try { + if (!artifact.isPresent()) return null; + CountingInputStream in = new CountingInputStream(artifact.openStream()); + T result = contentAction.execute(in, getMetaData()); + in.close(); + return ExternalResourceReadResult.of(in.getCount(), result); + } catch (IOException ex) { + return null; + } + } + + @Override + public ExternalResourceWriteResult put(ReadableContent readableContent) throws ResourceException { + throw ResourceExceptions.putFailed(uri, null); + } + + @Nullable + @Override + public List list() throws ResourceException { + return null; + } + + @Nullable + @Override + public ExternalResourceMetaData getMetaData() { + try { + if (!artifact.isPresent()) return null; + InputStream stream = artifact.openStream(); + int length = stream.available(); + stream.close(); + return new DefaultExternalResourceMetaData(uri, 0, length); + } catch (IOException ex) { + return null; + } + } + + } + + private class NullExternalResource extends AbstractExternalResource { + + private final URI uri; + + private NullExternalResource(URI uri) { + this.uri = uri; + } + + @Override + public String getDisplayName() { + return uri.toString(); + } + + @Override + public URI getURI() { + return uri; + } + + @Nullable + @Override + public ExternalResourceReadResult writeToIfPresent(File destination) throws ResourceException { + return null; + } + + @Override + public ExternalResourceReadResult writeTo(OutputStream destination) throws ResourceException { + throw ResourceExceptions.getMissing(uri); + } + + @Override + public ExternalResourceReadResult withContent(Action readAction) throws ResourceException { + throw ResourceExceptions.getMissing(uri); + } + + @Nullable + @Override + public ExternalResourceReadResult withContentIfPresent(Transformer readAction) { + return null; + } + + @Nullable + @Override + public ExternalResourceReadResult withContentIfPresent(ContentAction readAction) { + return null; + } + + @Override + public ExternalResourceWriteResult put(ReadableContent source) throws ResourceException { + throw ResourceExceptions.getMissing(uri); + } + + @Nullable + @Override + public List list() { + return null; + } + + @Nullable + @Override + public ExternalResourceMetaData getMetaData() { + return null; + } + + } + +} diff --git a/src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java b/src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java new file mode 100644 index 0000000..4fbe925 --- /dev/null +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java @@ -0,0 +1,52 @@ +package com.amadornes.artifactural.gradle; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.UnaryOperator; + +public class ReflectionUtils { + + public static void alter(Object target, String name, UnaryOperator operator) { + try { + Object prev = target; + Field f = null; + for (String n : name.split("\\.")) { + f = findField(target.getClass(), n); + if(f == null) throw new IllegalStateException("Could not find '" + name + "'"); + f.setAccessible(true); + prev = target; + target = f.get(target); + } + if(f == null) throw new IllegalStateException("Could not find '" + name + "'"); + f.set(prev, operator.apply((T) target)); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + private static Field findField(Class clazz, String name) { + while (clazz != Object.class) { + for (Field f : clazz.getDeclaredFields()) { + if (f.getName().equals(name)) { + return f; + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + + /** + * Invokes a method (can be private). + */ + public static T invoke(Object target, Class type, String name, Object... args) { + try { + Method method = type.getDeclaredMethod(name); + method.setAccessible(true); + return (T) method.invoke(target, args); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java b/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java new file mode 100644 index 0000000..4656285 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java @@ -0,0 +1,48 @@ +package com.amadornes.artifactural.base.artifact; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactMetadata; +import com.amadornes.artifactural.api.artifact.ArtifactType; +import com.amadornes.artifactural.api.cache.ArtifactCache; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; +import com.amadornes.artifactural.api.transform.ArtifactTransformer; + +public abstract class ArtifactBase implements Artifact { + + private final ArtifactIdentifier identifier; + private final ArtifactType type; + private final ArtifactMetadata metadata; + + ArtifactBase(ArtifactIdentifier identifier, ArtifactType type, ArtifactMetadata metadata) { + this.identifier = identifier; + this.type = type; + this.metadata = metadata; + } + + @Override + public ArtifactIdentifier getIdentifier() { + return identifier; + } + + @Override + public ArtifactType getType() { + return type; + } + + @Override + public ArtifactMetadata getMetadata() { + return metadata; + } + + @Override + public Artifact apply(ArtifactTransformer transformer) { + if (!transformer.appliesTo(this)) return this; + return transformer.transform(this); + } + + @Override + public Artifact cache(ArtifactCache cache, String specifier) { + return cache.store(this, specifier); + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactIdentifierImpl.java b/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactIdentifierImpl.java new file mode 100644 index 0000000..26acd29 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/artifact/ArtifactIdentifierImpl.java @@ -0,0 +1,42 @@ +package com.amadornes.artifactural.base.artifact; + +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; + +public class ArtifactIdentifierImpl implements ArtifactIdentifier { + + private final String group, name, version, classifier, extension; + + public ArtifactIdentifierImpl(String group, String name, String version, String classifier, String extension) { + this.group = group; + this.name = name; + this.version = version; + this.classifier = classifier; + this.extension = extension; + } + + @Override + public String getGroup() { + return group; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public String getClassifier() { + return classifier; + } + + @Override + public String getExtension() { + return extension; + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java b/src/main/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java new file mode 100644 index 0000000..e08cd7b --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java @@ -0,0 +1,57 @@ +package com.amadornes.artifactural.base.artifact; + +import com.amadornes.artifactural.api.artifact.ArtifactMetadata; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.LinkedList; +import java.util.stream.Collectors; + +public class SimpleArtifactMetadata implements ArtifactMetadata { + + private final LinkedList entries = new LinkedList<>(); + + public SimpleArtifactMetadata() { + } + + private SimpleArtifactMetadata(SimpleArtifactMetadata parent, Entry entry) { + this.entries.addAll(parent.entries); + this.entries.add(entry); + } + + @Override + public ArtifactMetadata with(String key, String value) { + return new SimpleArtifactMetadata(this, new Entry(key, value)); + } + + @Override + public String getHash() { + try { + String str = entries.stream().map(Entry::toString).collect(Collectors.joining(";;")); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(str.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + + private static class Entry { + + private final String key, value; + + private Entry(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String toString() { + return '[' + key + ',' + value + ']'; + } + + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java b/src/main/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java new file mode 100644 index 0000000..4f3df7a --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java @@ -0,0 +1,56 @@ +package com.amadornes.artifactural.base.artifact; + +import com.amadornes.artifactural.api.artifact.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public class StreamableArtifact extends ArtifactBase { + + public static Artifact ofJar(ArtifactIdentifier identifier, ArtifactType type, File file) { + return ofStreamable(identifier, type, () -> new FileInputStream(file)); + } + + public static Artifact ofURL(ArtifactIdentifier identifier, ArtifactType type, URL url) { + return ofStreamable(identifier, type, url::openStream); + } + + public static Artifact ofStreamable(ArtifactIdentifier identifier, ArtifactType type, Streamable streamable) { + return new StreamableArtifact(identifier, type, streamable); + } + + private final Streamable streamable; + + private StreamableArtifact(ArtifactIdentifier identifier, ArtifactType type, Streamable streamable) { + this(identifier, type, ArtifactMetadata.empty(), streamable); + } + + private StreamableArtifact(ArtifactIdentifier identifier, ArtifactType type, ArtifactMetadata metadata, Streamable streamable) { + super(identifier, type, metadata); + this.streamable = streamable; + } + + @Override + public Artifact withMetadata(ArtifactMetadata metadata) { + return new StreamableArtifact(getIdentifier(), getType(), metadata, streamable); + } + + @Override + public boolean isPresent() { + try (InputStream is = openStream()) { + is.close(); + return true; + } catch (IOException ex) { + return false; + } + } + + @Override + public InputStream openStream() throws IOException { + return streamable.openStream(); + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java b/src/main/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java new file mode 100644 index 0000000..5f1c240 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java @@ -0,0 +1,33 @@ +package com.amadornes.artifactural.base.cache; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.cache.ArtifactCache; +import com.amadornes.artifactural.base.artifact.StreamableArtifact; + +import java.io.*; + +abstract class ArtifactCacheBase implements ArtifactCache { + + Artifact doStore(File path, Artifact artifact) { + return StreamableArtifact.ofStreamable(artifact.getIdentifier(), artifact.getType(), () -> stream(path, artifact)) + .withMetadata(artifact.getMetadata()); + } + + private InputStream stream(File path, Artifact artifact) throws IOException { + if (!path.exists()) { + path.getParentFile().mkdirs(); + path.createNewFile(); + FileOutputStream fos = new FileOutputStream(path); + InputStream is = artifact.openStream(); + int read; + byte[] bytes = new byte[256]; + while ((read = is.read(bytes)) > 0) { + fos.write(bytes, 0, read); + } + fos.close(); + is.close(); + } + return new FileInputStream(path); + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java b/src/main/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java new file mode 100644 index 0000000..caa5de7 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java @@ -0,0 +1,31 @@ +package com.amadornes.artifactural.base.cache; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; + +import java.io.File; + +public class LocatedArtifactCache extends ArtifactCacheBase { + + private final File path; + + public LocatedArtifactCache(File path) { + this.path = path; + } + + @Override + public Artifact store(Artifact artifact, String specifier) { + ArtifactIdentifier identifier = artifact.getIdentifier(); + File cachePath = new File(path.getAbsolutePath() + .replace("${GROUP}", identifier.getGroup()) + .replace("${NAME}", identifier.getName()) + .replace("${VERSION}", identifier.getVersion()) + .replace("${CLASSIFIER}", identifier.getClassifier()) + .replace("${EXTENSION}", identifier.getExtension()) + .replace("${SPECIFIER}", specifier) + .replace("${META_HASH}", artifact.getMetadata().getHash()) + ); + return doStore(cachePath, artifact); + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/repository/ArtifactProviderBuilder.java b/src/main/java/com/amadornes/artifactural/base/repository/ArtifactProviderBuilder.java new file mode 100644 index 0000000..1f756ec --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/repository/ArtifactProviderBuilder.java @@ -0,0 +1,81 @@ +package com.amadornes.artifactural.base.repository; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.repository.ArtifactProvider; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +public class ArtifactProviderBuilder implements ArtifactProvider.Builder { + + public static ArtifactProviderBuilder begin(Class type) { + return new ArtifactProviderBuilder<>(Function.identity()); + } + + private final Function mapper; + private final Set> filters = new HashSet<>(); + + private ArtifactProviderBuilder(Function mapper) { + this.mapper = mapper; + } + + @Override + public ArtifactProvider.Builder filter(Predicate filter) { + filters.add(filter); + return this; + } + + @Override + public ArtifactProvider.Builder mapInfo(Function mapper) { + if (filters.isEmpty()) { + return new ArtifactProviderBuilder<>(this.mapper.andThen(mapper)); + } + return new ArtifactProviderBuilder<>((S info) -> { + I localInfo = this.mapper.apply(info); + if (localInfo == null) return null; + for (Predicate filter : filters) { + if (!filter.test(localInfo)) { + return null; + } + } + return mapper.apply(localInfo); + }); + } + + @Override + public ArtifactProvider.Builder.Complete provide(ArtifactProvider provider) { + return new Complete<>(mapper).provide(provider); + } + + private static class Complete implements ArtifactProvider.Builder.Complete { + + private final Set> providers = new HashSet<>(); + private final Function mapper; + + private Complete(Function mapper) { + this.mapper = mapper; + } + + @Override + public Builder.Complete provide(ArtifactProvider provider) { + providers.add(provider); + return this; + } + + @Override + public Artifact getArtifact(S info) { + I localInfo = mapper.apply(info); + if (localInfo == null) return Artifact.none(); + + for (ArtifactProvider provider : providers) { + Artifact artifact = provider.getArtifact(localInfo); + if (artifact.isPresent()) return artifact; + } + return Artifact.none(); + } + + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/repository/SimpleRepository.java b/src/main/java/com/amadornes/artifactural/base/repository/SimpleRepository.java new file mode 100644 index 0000000..7f437f8 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/repository/SimpleRepository.java @@ -0,0 +1,25 @@ +package com.amadornes.artifactural.base.repository; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; +import com.amadornes.artifactural.api.repository.ArtifactProvider; +import com.amadornes.artifactural.api.repository.Repository; + +public class SimpleRepository implements Repository { + + public static Repository of(ArtifactProvider provider) { + return new SimpleRepository(provider); + } + + private final ArtifactProvider provider; + + private SimpleRepository(ArtifactProvider provider) { + this.provider = provider; + } + + @Override + public Artifact getArtifact(ArtifactIdentifier identifier) { + return provider.getArtifact(identifier); + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/transform/ExclusiveTransformer.java b/src/main/java/com/amadornes/artifactural/base/transform/ExclusiveTransformer.java new file mode 100644 index 0000000..e16be1f --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/transform/ExclusiveTransformer.java @@ -0,0 +1,77 @@ +package com.amadornes.artifactural.base.transform; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactMetadata; +import com.amadornes.artifactural.api.artifact.ArtifactType; +import com.amadornes.artifactural.api.transform.ArtifactTransformer; +import com.amadornes.artifactural.base.artifact.StreamableArtifact; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ExclusiveTransformer implements ArtifactTransformer { + + public static ExclusiveTransformer of(boolean whitelist, String... filters) { + return new ExclusiveTransformer(whitelist, filters); + } + + private final Set filters = new HashSet<>(); + private final boolean whitelist; + + private ExclusiveTransformer(boolean whitelist, String... filters) { + this.whitelist = whitelist; + for (String s : filters) { + String regex = s + .replaceAll("(?:(^|[^\\w\\*])\\*\\*([^\\w\\*]|$))", "$1.*$2") // ** matches anything + .replaceAll("(?:(^|[^\\w\\*])\\*([^\\w\\*]|$))", "$1[^\\/]*$2"); // * matches anything but / + this.filters.add(Pattern.compile(regex)); + } + } + + @Override + public Artifact transform(Artifact artifact) { + if (!artifact.isPresent()) return Artifact.none(); + + if (artifact.getType() == ArtifactType.BINARY || artifact.getType() == ArtifactType.SOURCE) { + return exclude(artifact); + } else { + return Artifact.none(); + } + } + + @Override + public ArtifactMetadata withInfo(ArtifactMetadata metadata) { + return metadata.with("EXCLUDE", filters.stream().map(Pattern::pattern).collect(Collectors.joining(";"))); + } + + private Artifact exclude(Artifact artifact) { + return StreamableArtifact.ofStreamable(artifact.getIdentifier(), artifact.getType(), + () -> new ZipInputStream(artifact.openStream()) { + @Override + public ZipEntry getNextEntry() throws IOException { + ZipEntry next; + while ((next = super.getNextEntry()) != null) { + if (isAllowed(next.getName())) { + return next; + } + } + return null; + } + }); + } + + private boolean isAllowed(String name) { + if (whitelist) { + return filters.stream().anyMatch(p -> p.asPredicate().test(name)); + } else { + return filters.stream().noneMatch(p -> p.asPredicate().test(name)); + } + } + +} diff --git a/src/main/java/com/amadornes/artifactural/base/transform/SimpleArtifactPipeline.java b/src/main/java/com/amadornes/artifactural/base/transform/SimpleArtifactPipeline.java new file mode 100644 index 0000000..625b2e9 --- /dev/null +++ b/src/main/java/com/amadornes/artifactural/base/transform/SimpleArtifactPipeline.java @@ -0,0 +1,46 @@ +package com.amadornes.artifactural.base.transform; + +import com.amadornes.artifactural.api.artifact.Artifact; +import com.amadornes.artifactural.api.artifact.ArtifactMetadata; +import com.amadornes.artifactural.api.cache.ArtifactCache; +import com.amadornes.artifactural.api.transform.ArtifactPipeline; +import com.amadornes.artifactural.api.transform.ArtifactTransformer; + +import java.util.function.UnaryOperator; + +public class SimpleArtifactPipeline implements ArtifactPipeline { + + public static ArtifactPipeline create() { + return new SimpleArtifactPipeline(); + } + + private static final ArtifactTransformer IDENTITY = ArtifactTransformer.of(UnaryOperator.identity()); + + private ArtifactTransformer transformer = IDENTITY; + + private SimpleArtifactPipeline() { + } + + @Override + public ArtifactPipeline apply(ArtifactTransformer transformer) { + this.transformer = this.transformer.andThen(transformer); + return this; + } + + @Override + public ArtifactPipeline cache(ArtifactCache cache, String specifier) { + transformer = transformer.andThen(ArtifactTransformer.of(artifact -> cache.store(artifact, specifier))); + return this; + } + + @Override + public Artifact transform(Artifact artifact) { + return transformer.transform(artifact); + } + + @Override + public ArtifactMetadata withInfo(ArtifactMetadata metadata) { + return transformer.withInfo(metadata); + } + +} -- cgit