aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAmadornes <amadornes@gmail.com>2018-07-02 07:12:27 +0200
committerAmadornes <amadornes@gmail.com>2018-07-02 07:12:27 +0200
commit92b2a6669392bd410e9a60749656a49f3e309cc0 (patch)
treecae858e8f371018771f84afeef6ae059d8910e0d /src
parent4ffefc5676def5855d91dffa0d089fe5402364fa (diff)
downloadArtifactural-92b2a6669392bd410e9a60749656a49f3e309cc0.tar.gz
Artifactural-92b2a6669392bd410e9a60749656a49f3e309cc0.tar.bz2
Artifactural-92b2a6669392bd410e9a60749656a49f3e309cc0.zip
Initial (untested) version of Artifactural
Diffstat (limited to 'src')
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java35
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/ArtifactIdentifier.java61
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/ArtifactMetadata.java13
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/ArtifactType.java5
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/Internal.java84
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java9
-rw-r--r--src/api/java/com/amadornes/artifactural/api/artifact/Streamable.java11
-rw-r--r--src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java9
-rw-r--r--src/api/java/com/amadornes/artifactural/api/repository/ArtifactProvider.java28
-rw-r--r--src/api/java/com/amadornes/artifactural/api/repository/Repository.java10
-rw-r--r--src/api/java/com/amadornes/artifactural/api/transform/ArtifactPipeline.java11
-rw-r--r--src/api/java/com/amadornes/artifactural/api/transform/ArtifactTransformer.java54
-rw-r--r--src/gradlecomp/java/com/amadornes/artifactural/gradle/DependencyResolver.java83
-rw-r--r--src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleArtifact.java26
-rw-r--r--src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java303
-rw-r--r--src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java52
-rw-r--r--src/main/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java48
-rw-r--r--src/main/java/com/amadornes/artifactural/base/artifact/ArtifactIdentifierImpl.java42
-rw-r--r--src/main/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java57
-rw-r--r--src/main/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java56
-rw-r--r--src/main/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java33
-rw-r--r--src/main/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java31
-rw-r--r--src/main/java/com/amadornes/artifactural/base/repository/ArtifactProviderBuilder.java81
-rw-r--r--src/main/java/com/amadornes/artifactural/base/repository/SimpleRepository.java25
-rw-r--r--src/main/java/com/amadornes/artifactural/base/transform/ExclusiveTransformer.java77
-rw-r--r--src/main/java/com/amadornes/artifactural/base/transform/SimpleArtifactPipeline.java46
26 files changed, 1290 insertions, 0 deletions
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<ArtifactIdentifier> groupMatches(String group) {
+ return identifier -> identifier.getGroup().matches(group);
+ }
+
+ static Predicate<ArtifactIdentifier> nameMatches(String name) {
+ return identifier -> identifier.getName().matches(name);
+ }
+
+ static Predicate<ArtifactIdentifier> versionMatches(String version) {
+ return identifier -> identifier.getVersion().matches(version);
+ }
+
+ static Predicate<ArtifactIdentifier> classifierMatches(String classifier) {
+ return identifier -> identifier.getClassifier().matches(classifier);
+ }
+
+ static Predicate<ArtifactIdentifier> extensionMatches(String extension) {
+ return identifier -> identifier.getExtension().matches(extension);
+ }
+
+ static Predicate<ArtifactIdentifier> groupEquals(String group) {
+ return identifier -> identifier.getGroup().equals(group);
+ }
+
+ static Predicate<ArtifactIdentifier> nameEquals(String name) {
+ return identifier -> identifier.getName().equals(name);
+ }
+
+ static Predicate<ArtifactIdentifier> versionEquals(String version) {
+ return identifier -> identifier.getVersion().equals(version);
+ }
+
+ static Predicate<ArtifactIdentifier> classifierEquals(String classifier) {
+ return identifier -> identifier.getClassifier().equals(classifier);
+ }
+
+ static Predicate<ArtifactIdentifier> 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<I> {
+
+ Artifact getArtifact(I info);
+
+ interface Builder<S, I> {
+
+ Builder<S, I> filter(Predicate<I> filter);
+
+ <D> Builder<S, D> mapInfo(Function<I, D> mapper);
+
+ Complete<S, I> provide(ArtifactProvider<I> provider);
+
+ interface Complete<S, I> extends ArtifactProvider<S> {
+
+ Complete<S, I> provide(ArtifactProvider<I> 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<Artifact> 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<String> 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<String, CompletableFuture<Set<File>>> 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<File> 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<DependencyArtifact> 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<Set<File>> 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<File> 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<File> 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<File> 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(
+ "^/(?<group>\\S+(?:/\\S+)*)/(?<name>\\S+)/(?<version>\\S+)/" +
+ "\\2-\\3(?:-(?<classifier>[^.\\s]+))?\\.(?<extension>\\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<ArtifactRepository> 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<Void> writeToIfPresent(File file) {
+ try {
+ if (!artifact.isPresent()) return null;
+ FileOutputStream out = new FileOutputStream(file);
+ ExternalResourceReadResult<Void> result = writeTo(out);
+ out.close();
+ return result;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ public ExternalResourceReadResult<Void> 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<Void> withContent(Action<? super InputStream> 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 <T> ExternalResourceReadResult<T> withContentIfPresent(Transformer<? extends T, ? super InputStream> 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 <T> ExternalResourceReadResult<T> withContentIfPresent(ContentAction<? extends T> 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<String> 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<Void> writeToIfPresent(File destination) throws ResourceException {
+ return null;
+ }
+
+ @Override
+ public ExternalResourceReadResult<Void> writeTo(OutputStream destination) throws ResourceException {
+ throw ResourceExceptions.getMissing(uri);
+ }
+
+ @Override
+ public ExternalResourceReadResult<Void> withContent(Action<? super InputStream> readAction) throws ResourceException {
+ throw ResourceExceptions.getMissing(uri);
+ }
+
+ @Nullable
+ @Override
+ public <T> ExternalResourceReadResult<T> withContentIfPresent(Transformer<? extends T, ? super InputStream> readAction) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public <T> ExternalResourceReadResult<T> withContentIfPresent(ContentAction<? extends T> readAction) {
+ return null;
+ }
+
+ @Override
+ public ExternalResourceWriteResult put(ReadableContent source) throws ResourceException {
+ throw ResourceExceptions.getMissing(uri);
+ }
+
+ @Nullable
+ @Override
+ public List<String> 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 <T> void alter(Object target, String name, UnaryOperator<T> 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> 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<Entry> 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<S, I> implements ArtifactProvider.Builder<S, I> {
+
+ public static <I> ArtifactProviderBuilder<I, I> begin(Class<I> type) {
+ return new ArtifactProviderBuilder<>(Function.identity());
+ }
+
+ private final Function<S, I> mapper;
+ private final Set<Predicate<I>> filters = new HashSet<>();
+
+ private ArtifactProviderBuilder(Function<S, I> mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public ArtifactProvider.Builder<S, I> filter(Predicate<I> filter) {
+ filters.add(filter);
+ return this;
+ }
+
+ @Override
+ public <D> ArtifactProvider.Builder<S, D> mapInfo(Function<I, D> 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<I> filter : filters) {
+ if (!filter.test(localInfo)) {
+ return null;
+ }
+ }
+ return mapper.apply(localInfo);
+ });
+ }
+
+ @Override
+ public ArtifactProvider.Builder.Complete<S, I> provide(ArtifactProvider<I> provider) {
+ return new Complete<>(mapper).provide(provider);
+ }
+
+ private static class Complete<S, I> implements ArtifactProvider.Builder.Complete<S, I> {
+
+ private final Set<ArtifactProvider<I>> providers = new HashSet<>();
+ private final Function<S, I> mapper;
+
+ private Complete(Function<S, I> mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public Builder.Complete<S, I> provide(ArtifactProvider<I> 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<I> 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<ArtifactIdentifier> provider) {
+ return new SimpleRepository(provider);
+ }
+
+ private final ArtifactProvider<ArtifactIdentifier> provider;
+
+ private SimpleRepository(ArtifactProvider<ArtifactIdentifier> 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<Pattern> 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);
+ }
+
+}