diff options
15 files changed, 549 insertions, 271 deletions
diff --git a/build.gradle b/build.gradle index 4c40c97..ba4e0be 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ configurations { dependencies { sharedImplementation sourceSets.api.output + sharedImplementation 'commons-io:commons-io:2.4' gradlecompImplementation sourceSets.shared.output gradlecompImplementation gradleApi() diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java b/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java index 7e797fb..4271d70 100644 --- a/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java +++ b/src/api/java/com/amadornes/artifactural/api/artifact/Artifact.java @@ -23,10 +23,10 @@ public interface Artifact { Artifact apply(ArtifactTransformer transformer); - Artifact.Cached cache(ArtifactCache cache, String specifier); + Artifact.Cached cache(ArtifactCache cache); - default Artifact.Cached optionallyCache(ArtifactCache cache, String specifier) { - return this instanceof Artifact.Cached ? (Artifact.Cached) this : cache(cache, specifier); + default Artifact.Cached optionallyCache(ArtifactCache cache) { + return this instanceof Artifact.Cached ? (Artifact.Cached) this : cache(cache); } boolean isPresent(); @@ -35,8 +35,10 @@ public interface Artifact { interface Cached extends Artifact { + // Gets the file location, AND writes the file to disc if it hasn't already. File asFile() throws IOException, MissingArtifactException; + // Gets the file location, but doesn't guarantee that it exists. As the wrapped Artifact may not of been written. What's the point of this? File getFileLocation() throws IOException, MissingArtifactException; } diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java b/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java index c7fd883..321b4fc 100644 --- a/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java +++ b/src/api/java/com/amadornes/artifactural/api/artifact/Internal.java @@ -35,9 +35,18 @@ final class Internal { return "missing"; } + @Override + public String toString() { + return "NO_IDENTIFIER"; + } + }; static final Artifact NO_ARTIFACT = new Artifact.Cached() { + @Override + public String toString() { + return "NO_ARTIFACT"; + } @Override public ArtifactIdentifier getIdentifier() { @@ -56,6 +65,11 @@ final class Internal { public String getHash() { return "ERROR"; } + + @Override + public String toString() { + return "NO_METADATA"; + } }; } @@ -75,7 +89,7 @@ final class Internal { } @Override - public Artifact.Cached cache(ArtifactCache cache, String specifier) { + public Artifact.Cached cache(ArtifactCache cache) { return this; } diff --git a/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java b/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java index 479909c..8ceef19 100644 --- a/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java +++ b/src/api/java/com/amadornes/artifactural/api/artifact/MissingArtifactException.java @@ -1,6 +1,7 @@ package com.amadornes.artifactural.api.artifact; public class MissingArtifactException extends RuntimeException { + private static final long serialVersionUID = 4902516963452435653L; public MissingArtifactException(ArtifactIdentifier identifier) { super("Could not find artifact: " + identifier); diff --git a/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java b/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java index 3209656..8099d30 100644 --- a/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java +++ b/src/api/java/com/amadornes/artifactural/api/cache/ArtifactCache.java @@ -4,6 +4,6 @@ import com.amadornes.artifactural.api.artifact.Artifact; public interface ArtifactCache { - Artifact.Cached store(Artifact artifact, String specifier); + Artifact.Cached store(Artifact artifact); } diff --git a/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java index 4bac887..2418ea9 100644 --- a/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/GradleRepositoryAdapter.java @@ -2,108 +2,181 @@ 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.MissingArtifactException; import com.amadornes.artifactural.api.repository.Repository; import com.amadornes.artifactural.base.artifact.SimpleArtifactIdentifier; -import com.google.common.io.CountingInputStream; -import org.apache.commons.io.IOUtils; -import org.gradle.api.Action; -import org.gradle.api.NamedDomainObjectCollection; -import org.gradle.api.Transformer; +import com.amadornes.artifactural.base.cache.LocatedArtifactCache; + +import org.gradle.api.artifacts.ComponentMetadataSupplierDetails; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; 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.attributes.AttributeContainer; +import org.gradle.api.internal.artifacts.BaseRepositoryFactory; import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.ConfiguredModuleComponentRepository; +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.ModuleComponentRepositoryAccess; +import org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ResolvableArtifact; import org.gradle.api.internal.artifacts.repositories.AbstractArtifactRepository; -import org.gradle.api.internal.artifacts.repositories.DefaultMavenArtifactRepository; +import org.gradle.api.internal.artifacts.repositories.DefaultMavenLocalArtifactRepository; 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.resource.AbstractExternalResource; -import org.gradle.internal.resource.ExternalResource; +import org.gradle.api.internal.artifacts.repositories.resolver.MetadataFetchingCost; +import org.gradle.api.internal.attributes.AttributesSchemaInternal; +import org.gradle.api.internal.attributes.ImmutableAttributesFactory; +import org.gradle.api.internal.component.ArtifactType; +import org.gradle.internal.action.InstantiatingAction; +import org.gradle.internal.component.external.model.ComponentVariant; +import org.gradle.internal.component.external.model.ModuleComponentArtifactMetadata; +import org.gradle.internal.component.external.model.ModuleComponentResolveMetadata; +import org.gradle.internal.component.external.model.ModuleDependencyMetadata; +import org.gradle.internal.component.external.model.MutableModuleComponentResolveMetadata; +import org.gradle.internal.component.model.ComponentArtifactMetadata; +import org.gradle.internal.component.model.ComponentOverrideMetadata; +import org.gradle.internal.component.model.ComponentResolveMetadata; +import org.gradle.internal.component.model.ConfigurationMetadata; +import org.gradle.internal.component.model.ModuleSource; +import org.gradle.internal.hash.HashValue; +import org.gradle.internal.impldep.com.google.common.base.Optional; +import org.gradle.internal.impldep.com.google.common.collect.ImmutableList; +import org.gradle.internal.nativeintegration.filesystem.FileSystem; +import org.gradle.internal.nativeintegration.services.FileSystems; +import org.gradle.internal.resolve.result.BuildableArtifactResolveResult; +import org.gradle.internal.resolve.result.BuildableArtifactSetResolveResult; +import org.gradle.internal.resolve.result.BuildableComponentArtifactsResolveResult; +import org.gradle.internal.resolve.result.BuildableModuleComponentMetaDataResolveResult; +import org.gradle.internal.resolve.result.BuildableModuleVersionListingResolveResult; import org.gradle.internal.resource.ExternalResourceName; -import org.gradle.internal.resource.ExternalResourceReadResult; import org.gradle.internal.resource.ExternalResourceRepository; -import org.gradle.internal.resource.ExternalResourceWriteResult; -import org.gradle.internal.resource.ReadableContent; -import org.gradle.internal.resource.ResourceExceptions; -import org.gradle.internal.resource.metadata.DefaultExternalResourceMetaData; +import org.gradle.internal.resource.LocalBinaryResource; +import org.gradle.internal.resource.local.FileResourceRepository; +import org.gradle.internal.resource.local.LocalFileStandInExternalResource; +import org.gradle.internal.resource.local.LocallyAvailableExternalResource; import org.gradle.internal.resource.metadata.ExternalResourceMetaData; +import org.gradle.internal.resource.transfer.DefaultCacheAwareExternalResourceAccessor; -import javax.annotation.Nullable; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; import java.util.List; +import java.util.Map; +import java.util.Set; 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+)/" + + "^(?<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); + public static GradleRepositoryAdapter add(RepositoryHandler handler, String name, File local, Repository repository) { + BaseRepositoryFactory factory = ReflectionUtils.get(handler, "repositoryFactory"); // We reflect here and create it manually so it DOESN'T get attached. + DefaultMavenLocalArtifactRepository maven = (DefaultMavenLocalArtifactRepository)factory.createMavenLocalRepository(); // We use maven local because it bypasses the caching and coping to .m2 + maven.setUrl(local); + maven.setName(name); - // Add our own custom test instead, using the real one in the background - GradleRepositoryAdapter repo = new GradleRepositoryAdapter((DefaultMavenArtifactRepository) maven, repository); + GradleRepositoryAdapter repo = new GradleRepositoryAdapter(repository, maven); + repo.setName(name); handler.add(repo); return repo; } - private final DefaultMavenArtifactRepository maven; private final Repository repository; + private final DefaultMavenLocalArtifactRepository local; - private GradleRepositoryAdapter(DefaultMavenArtifactRepository maven, Repository repository) { - this.maven = maven; + private GradleRepositoryAdapter(Repository repository, DefaultMavenLocalArtifactRepository local) { 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 + this.local = local; } @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 + return local.getDisplayName(); } @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); + MavenResolver resolver = (MavenResolver)local.createResolver(); + + GeneratingFileResourceRepository repo = new GeneratingFileResourceRepository(); + ReflectionUtils.alter(resolver, "repository", prev -> repo); // ExternalResourceResolver.repository + //ReflectionUtils.alter(resolver, "metadataSources", ); //ExternalResourceResolver.metadataSources We need to fix these from returning 'missing' + // MavenResolver -> MavenMetadataLoader -> FileCacheAwareExternalResourceAccessor -> DefaultCacheAwareExternalResourceAccessor + DefaultCacheAwareExternalResourceAccessor accessor = ReflectionUtils.get(resolver, "mavenMetaDataLoader.cacheAwareExternalResourceAccessor.delegate"); + ReflectionUtils.alter(accessor, "delegate", prev -> repo); // DefaultCacheAwareExternalResourceAccessor.delegate + ReflectionUtils.alter(accessor, "fileResourceRepository", prev -> repo); // DefaultCacheAwareExternalResourceAccessor.fileResourceRepository + //ReflectionUtils.alter(resolver, "localAccess", AccessWrapper<MavenLocalRepositoryAccess>::new); + + //return resolver; + //return new Resolver(resolver); + return new ConfiguredModuleComponentRepository() { + private final ModuleComponentRepositoryAccess local = wrap(resolver.getLocalAccess()); + private final ModuleComponentRepositoryAccess remote = wrap(resolver.getRemoteAccess()); + @Override public String getId() { return resolver.getId(); } + @Override public String getName() { return resolver.getName(); } + @Override public ModuleComponentRepositoryAccess getLocalAccess() { return local; } + @Override public ModuleComponentRepositoryAccess getRemoteAccess() { return remote; } + @Override public Map<ComponentArtifactIdentifier, ResolvableArtifact> getArtifactCache() { return resolver.getArtifactCache(); } + @Override public InstantiatingAction<ComponentMetadataSupplierDetails> getComponentMetadataSupplier() { return resolver.getComponentMetadataSupplier(); } + @Override public boolean isDynamicResolveMode() { return resolver.isDynamicResolveMode(); } + @Override public boolean isLocal() { return resolver.isLocal(); } + + private ModuleComponentRepositoryAccess wrap(ModuleComponentRepositoryAccess delegate) { + return new ModuleComponentRepositoryAccess() { + @Override + public void resolveComponentMetaData(ModuleComponentIdentifier moduleComponentIdentifier, ComponentOverrideMetadata requestMetaData, BuildableModuleComponentMetaDataResolveResult result) { + delegate.resolveComponentMetaData(moduleComponentIdentifier, requestMetaData, result); + if (result.getState() == BuildableModuleComponentMetaDataResolveResult.State.Resolved) { + ModuleComponentResolveMetadata meta = result.getMetaData(); + if (meta.isMissing()) { + MutableModuleComponentResolveMetadata mutable = meta.asMutable(); + mutable.setChanging(true); + mutable.setMissing(false); + result.resolved(mutable.asImmutable()); + } + } + } + + @Override + public void listModuleVersions(ModuleDependencyMetadata dependency, BuildableModuleVersionListingResolveResult result) { + delegate.listModuleVersions(dependency, result); + } + + @Override + public void resolveArtifacts(ComponentResolveMetadata component, BuildableComponentArtifactsResolveResult result) { + delegate.resolveArtifacts(component, result); + } + + @Override + public void resolveArtifactsWithType(ComponentResolveMetadata component, ArtifactType artifactType, BuildableArtifactSetResolveResult result) { + delegate.resolveArtifactsWithType(component, artifactType, result); + } + + @Override + public void resolveArtifact(ComponentArtifactMetadata artifact, ModuleSource moduleSource, BuildableArtifactResolveResult result) { + delegate.resolveArtifact(artifact, moduleSource, result); + } + + @Override + public MetadataFetchingCost estimateMetadataFetchingCost(ModuleComponentIdentifier moduleComponentIdentifier) { + return delegate.estimateMetadataFetchingCost(moduleComponentIdentifier); + } + }; + } + }; + } - return resolver; + private static String cleanRoot(URI uri) { + String ret = uri.normalize().getPath().replace('\\', '/'); + if (!ret.endsWith("/")) ret += '/'; + return ret; } - private class StreamingRepo implements ExternalResourceRepository { + private class GeneratingFileResourceRepository implements FileResourceRepository { + private final FileSystem fileSystem = FileSystems.getDefault(); + private final String root = cleanRoot(GradleRepositoryAdapter.this.local.getUrl()); + private final LocatedArtifactCache cache = new LocatedArtifactCache(new File(root)); @Override public ExternalResourceRepository withProgressLogging() { @@ -111,204 +184,66 @@ public class GradleRepositoryAdapter extends AbstractArtifactRepository implemen } @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 SimpleArtifactIdentifier( - 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); + public LocalBinaryResource localResource(File file) { + System.out.println("localResource: " + file); + return null; } @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; + public LocallyAvailableExternalResource resource(File file) { + System.out.println("resource(File): " + file); + return findArtifact(file.getAbsolutePath().replace('\\', '/')); } @Override - public String getDisplayName() { - return uri.toString(); + public LocallyAvailableExternalResource resource(ExternalResourceName location) { + return resource(location, false); } @Override - public URI getURI() { - return uri; + public LocallyAvailableExternalResource resource(ExternalResourceName location, boolean revalidate) { + System.out.println("resource(ExternalResourceName,boolean): " + location + ", " + revalidate); + return findArtifact(location.getUri().getPath().replace('\\', '/')); } - @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; - } + public LocallyAvailableExternalResource resource(File file, URI originUri, ExternalResourceMetaData originMetadata) { + System.out.println("resource(File,URI,ExternalResourceMetaData): " + file + ", " + originUri + ", " + originMetadata); + return findArtifact(file.getAbsolutePath().replace('\\', '/')); } - @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); + private LocallyAvailableExternalResource findArtifact(String path) { + if (path.startsWith(root)) { + String relative = path.substring(root.length()); + System.out.println(" Relative: " + relative); + Matcher matcher = URL_PATTERN.matcher(relative); + if (!matcher.matches()) { + System.out.println(" Matcher Failed: " + relative); + } else { + ArtifactIdentifier identifier = new SimpleArtifactIdentifier( + matcher.group("group").replace('/', '.'), + matcher.group("name"), + matcher.group("version"), + matcher.group("classifier"), + matcher.group("extension")); + Artifact artifact = repository.getArtifact(identifier); + return wrap(artifact, identifier); } - }); - } - - @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; + } else { + System.out.println("Unknown root: " + path); } + return new LocalFileStandInExternalResource(new File(path), fileSystem); } - @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() { + private LocallyAvailableExternalResource wrap(Artifact artifact, ArtifactIdentifier id) { + if (!artifact.isPresent()) + return new LocalFileStandInExternalResource(cache.getPath(artifact), fileSystem); + Artifact.Cached cached = artifact.optionallyCache(cache); 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; + return new LocalFileStandInExternalResource(cached.asFile(), fileSystem); + } catch (MissingArtifactException | IOException e) { + throw new RuntimeException(e); } } - - } - - 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 index 5dcb9a3..f43b33c 100644 --- a/src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java +++ b/src/gradlecomp/java/com/amadornes/artifactural/gradle/ReflectionUtils.java @@ -6,28 +6,42 @@ import java.util.function.UnaryOperator; public class ReflectionUtils { + @SuppressWarnings("unchecked") 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); + int idx = name.lastIndexOf('.'); + if (idx != -1) { + target = drillField(target, name.substring(0, idx)); + if (target == null) throw new IllegalStateException("Could not find field '" + name + "'"); + name = name.substring(idx + 1); } + Field f = findField(target.getClass(), name); if (f == null) throw new IllegalStateException("Could not find '" + name + "'"); - f.set(prev, operator.apply((T) target)); + f.set(target, operator.apply((T)f.get(target))); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } } + private static Object drillField(Object obj, String path) { + for (String name : path.split("\\.")) { + if (obj == null) return null; + Field f = findField(obj.getClass(), name); + if (f == null) return null; + try { + obj = f.get(obj); + } catch (IllegalAccessException e) { + return null; + } + } + return obj; + } + private static Field findField(Class<?> clazz, String name) { while (clazz != Object.class) { for (Field f : clazz.getDeclaredFields()) { if (f.getName().equals(name)) { + f.setAccessible(true); return f; } } @@ -39,6 +53,7 @@ public class ReflectionUtils { /** * Invokes a method (can be private). */ + @SuppressWarnings("unchecked") public static <T> T invoke(Object target, Class<?> type, String name, Object... args) { try { Method method = type.getDeclaredMethod(name); @@ -49,4 +64,20 @@ public class ReflectionUtils { } } + @SuppressWarnings("unchecked") + public static <T> T get(Object target, String name) { + try { + int idx = name.lastIndexOf('.'); + if (idx != -1) { + target = drillField(target, name.substring(0, idx)); + if (target == null) throw new IllegalStateException("Could not find field '" + name + "'"); + name = name.substring(idx + 1); + } + Field f = findField(target.getClass(), name); + if (f == null) throw new IllegalStateException("Could not find '" + name + "'"); + return (T)f.get(target); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } } diff --git a/src/shared/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java b/src/shared/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java index e7dd0c2..b164feb 100644 --- a/src/shared/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java +++ b/src/shared/java/com/amadornes/artifactural/base/artifact/ArtifactBase.java @@ -41,8 +41,13 @@ public abstract class ArtifactBase implements Artifact { } @Override - public Artifact.Cached cache(ArtifactCache cache, String specifier) { - return cache.store(this, specifier); + public Artifact.Cached cache(ArtifactCache cache) { + return cache.store(this); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + identifier + ", " + type +", " + metadata; } } diff --git a/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactIdentifier.java b/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactIdentifier.java index 231f68b..71f8cc6 100644 --- a/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactIdentifier.java +++ b/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactIdentifier.java @@ -39,4 +39,13 @@ public class SimpleArtifactIdentifier implements ArtifactIdentifier { return extension; } + @Override + public String toString() { + String ret = getGroup() + ':' + getName() + ':' + getVersion(); + if (classifier != null) + ret += ':' + getClassifier(); + if ("jar".equals(extension)) + ret += '@' + getExtension(); + return ret; + } } diff --git a/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java b/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java index 860a655..6dd6195 100644 --- a/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java +++ b/src/shared/java/com/amadornes/artifactural/base/artifact/SimpleArtifactMetadata.java @@ -43,6 +43,11 @@ public class SimpleArtifactMetadata implements ArtifactMetadata { } } + @Override + public String toString() { + return "SimpleArtifactMetadata(" + entries.toString() + ", " + getHash() + ")"; + } + private static class Entry { private final String key, value; diff --git a/src/shared/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java b/src/shared/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java index 1b4377a..c0a9f57 100644 --- a/src/shared/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java +++ b/src/shared/java/com/amadornes/artifactural/base/artifact/StreamableArtifact.java @@ -82,6 +82,11 @@ public class StreamableArtifact extends ArtifactBase { return file; } + @Override + public String toString() { + return "StreamableFileArtifact(" + file + ")"; + } + } } diff --git a/src/shared/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java b/src/shared/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java index 93ca358..37c9d97 100644 --- a/src/shared/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java +++ b/src/shared/java/com/amadornes/artifactural/base/cache/ArtifactCacheBase.java @@ -74,8 +74,8 @@ public abstract class ArtifactCacheBase implements ArtifactCache { } @Override - public Artifact.Cached cache(ArtifactCache cache, String specifier) { - return artifact.cache(cache, specifier); + public Artifact.Cached cache(ArtifactCache cache) { + return artifact.cache(cache); } @Override @@ -100,7 +100,10 @@ public abstract class ArtifactCacheBase implements ArtifactCache { public File getFileLocation() throws MissingArtifactException { return file; } - + @Override + public String toString() { + return "wrapped(" + artifact + ", " + file + ")"; + } }; } diff --git a/src/shared/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java b/src/shared/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java index 198f240..71fbc9d 100644 --- a/src/shared/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java +++ b/src/shared/java/com/amadornes/artifactural/base/cache/LocatedArtifactCache.java @@ -2,11 +2,17 @@ package com.amadornes.artifactural.base.cache; import com.amadornes.artifactural.api.artifact.Artifact; import com.amadornes.artifactural.api.artifact.ArtifactIdentifier; +import com.amadornes.artifactural.base.util.PatternReplace; import java.io.File; +import java.util.AbstractMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class LocatedArtifactCache extends ArtifactCacheBase { - + private static final String PATTERN = "[group]/[name](/[meta_hash])/[version]/[name]-[version](-[classifier])(-[specifier]).[extension]"; private final File path; public LocatedArtifactCache(File path) { @@ -14,22 +20,31 @@ public class LocatedArtifactCache extends ArtifactCacheBase { } @Override - public Artifact.Cached store(Artifact artifact, String specifier) { + public Artifact.Cached store(Artifact artifact) { + return doStore(getPath(artifact), artifact); + } + + public File getPath(Artifact artifact) { 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); + Map<String, String> names = Stream.of( + entry("group", identifier.getGroup()), + entry("name", identifier.getName()), + entry("version", identifier.getVersion()), + entry("classifier", identifier.getClassifier()), + entry("extension", identifier.getExtension()), + //entry("specifier", specifier), /? + entry("meta_hash", artifact.getMetadata().getHash()) + ).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + return new File(path, PatternReplace.replace(PATTERN, names)); } - public static File expand(File path) { - return new File(path, "${GROUP}/${NAME}/${META_HASH}/${NAME}-${VERSION}-${CLASSIFIER}-${SPECIFIER}.${EXTENSION}"); + private static <K,V> Entry<K,V> entry(K key, V value) { + return new AbstractMap.SimpleEntry<>(key, value); + } + + @Override + public String toString() { + return "LocatedArtifactCache(" + path + ")"; } } diff --git a/src/shared/java/com/amadornes/artifactural/base/util/HashFunction.java b/src/shared/java/com/amadornes/artifactural/base/util/HashFunction.java new file mode 100644 index 0000000..275b53c --- /dev/null +++ b/src/shared/java/com/amadornes/artifactural/base/util/HashFunction.java @@ -0,0 +1,112 @@ +package com.amadornes.artifactural.base.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +import org.apache.commons.io.IOUtils; + +//These are all standard hashing functions the JRE is REQUIRED to have, so add a nice factory that doesnt require catching annoying exceptions; +public enum HashFunction { + MD5("md5", 32), + SHA1("SHA-1", 40), + SHA256("SHA-256", 64); + + private String algo; + private String pad; + + private HashFunction(String algo, int length) { + this.algo = algo; + this.pad = String.format("%0" + length + "d", 0); + } + + public String getExtension() { + return this.name().toLowerCase(Locale.ENGLISH); + } + + public Instance create() { + return new Instance(); + } + + public MessageDigest get() { + try { + return MessageDigest.getInstance(algo); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); //Never happens + } + } + + public String hash(File file) throws IOException { + try (FileInputStream fin = new FileInputStream(file)) { + return hash(fin); + } + } + + public String hash(Iterable<File> files) throws IOException { + MessageDigest hash = get(); + byte[] buf = new byte[1024]; + + for (File file : files) { + if (!file.exists()) + continue; + + try (FileInputStream fin = new FileInputStream(file)) { + int count = -1; + while ((count = fin.read(buf)) != -1) + hash.update(buf, 0, count); + } + } + return pad(new BigInteger(1, hash.digest()).toString(16)); + } + + public String hash(String data) { + return hash(data.getBytes(StandardCharsets.UTF_8)); + } + + public String hash(InputStream stream) throws IOException { + return hash(IOUtils.toByteArray(stream)); + } + + public String hash(byte[] data) { + return pad(new BigInteger(1, get().digest(data)).toString(16)); + } + + public String pad(String hash) { + return (pad + hash).substring(hash.length()); + } + + public class Instance { + private MessageDigest digest = HashFunction.this.get(); + public void update(byte input) { + digest.update(input); + } + public void update(byte[] input) { + digest.update(input); + } + public void update(byte[] input, int offset, int length) { + digest.update(input, offset, length); + } + public void update(ByteBuffer input) { + digest.update(input); + } + public void update(String input) { + update(input.getBytes(StandardCharsets.UTF_8)); + } + public void update(int input) { + update(new byte[] { (byte)((input & 0xFF000000) >> 24), (byte)((input & 0xFF000000) >> 16), (byte)((input & 0xFF000000) >> 8), (byte)((input & 0xFF000000))}); + } + public byte[] digest() { + return digest.digest(); + } + public String finish() { + return pad(new BigInteger(1, digest()).toString(16)); + } + } +} diff --git a/src/shared/java/com/amadornes/artifactural/base/util/PatternReplace.java b/src/shared/java/com/amadornes/artifactural/base/util/PatternReplace.java new file mode 100644 index 0000000..80216d6 --- /dev/null +++ b/src/shared/java/com/amadornes/artifactural/base/util/PatternReplace.java @@ -0,0 +1,140 @@ +package com.amadornes.artifactural.base.util; + +import java.util.Map; + +public class PatternReplace { + /* + * Replaces a patterened string, with support for optional groups. + * Example: + * Values: + * group: net/minecraftforge + * name: forge + * version: 1.0 + * ext: jar + * + * Example: [group]/[name]/[version]/[name]-[version](-[classifier]).[ext] + * {classifier: test} net/minecraftforge/forge/1.0/forge-1.0-test.jar + * {classifier: null} net/minecraftforge/forge/1.0/forge-1.0.jar + * + * Nested Optionals are supported: + * Example: [group]/[name]/[version]/[name]-[version](-[classifier](-[suffix])).[ext] + * {classifier: test, suffix: foo} net/minecraftforge/forge/1.0/forge-1.0-test-foo.jar + * {classifier: test, suffix: foo} net/minecraftforge/forge/1.0/forge-1.0-test.jar + * {classifier: null, suffix: foo} net/minecraftforge/forge/1.0/forge-1.0.jar + * + * Compound optionals are supported: + * Example: [group]/[name]/[version]/[name]-[version](-[classifier]-[suffix]).[ext] + * {classifier: test, suffix: foo} net/minecraftforge/forge/1.0/forge-1.0-test-foo.jar + * {classifier: test, suffix: null} net/minecraftforge/forge/1.0/forge-1.0.jar + * {classifier: null, suffix: foo} net/minecraftforge/forge/1.0/forge-1.0.jar + * + * + * TODO: Support nested names? + * Example: [group]/[name]/[version]/[name]-[version](-[classifier[suffix]]).[ext] + * {classifierFoo: test, suffix: Foo} net/minecraftforge/forge/1.0/forge-1.0-test.jar + * {classifierFoo: null, suffix: Foo} net/minecraftforge/forge/1.0/forge-1.0.jar + */ + public static String replace(String pattern, Map<String, String> values) { + if (pattern == null) return null; + if (pattern.isEmpty()) return ""; + + Optional optional = null; + StringBuffer name = null; + StringBuffer ret = new StringBuffer(); + + char[] chars = pattern.toCharArray(); + for (int x = 0; x < chars.length; x++) { + char c = chars[x]; + if (c == '\\') { + if (x == chars.length -1) + throw new IllegalArgumentException("Escape character can not be end of pattern: " + pattern); + x++; + ret.append(chars[x]); + continue; + } + switch (c) { + case '[': + if (name != null) + throw new IllegalArgumentException("Nested names are not supported @ " + x + " : " + pattern); + name = new StringBuffer(); + break; + case ']': + if (name == null) + throw new IllegalArgumentException("Name closing found without opening @ " + x + " : " + pattern); + String key = name.toString(); + if (key.isEmpty()) + throw new IllegalArgumentException("Name can not be empty @ " + x + ": " + pattern); + if (optional != null) + optional.setKey(key, values); + else + ret.append(values.get(key)); // appends 'null' if missing, if you want "" then use ([name]) + // Should we have this default to not replacing at all if value is not set to allow chaining? + // Meaning: '[key]' == '[key]' if 'key' is not set. + // Current: '[key]' == 'null' + name = null; + break; + case '(': + optional = new Optional(optional); + break; + case ')': + if (optional == null) + throw new IllegalArgumentException("Optional closing found without opening @ " + x + ": " + pattern); + optional = optional.finish(x, pattern, ret); + break; + default: + if (name != null) + name.append(c); + else if (optional != null) + optional.append(c); + else + ret.append(c); + } + } + if (optional != null) + throw new IllegalArgumentException("Missing closing of optional value: " + pattern); + if (name != null) + throw new IllegalArgumentException("Missing closing of name entry: " + pattern); + return ret.toString(); + } + + public static String quote(String value) { + return value.replaceAll("\\", "\\\\") + .replaceAll("(", "\\(") + .replaceAll(")", "\\)") + .replaceAll("[", "\\[") + .replaceAll("]", "\\]"); + } + + private static class Optional { + private final Optional parent; + private final StringBuffer buf = new StringBuffer(); + private boolean hadAll = true; + private boolean hadValue = false; + + private Optional(Optional parent) { + this.parent = parent; + } + + public void append(char c) { + buf.append(c); + } + + private void setKey(String key, Map<String, String> values) { + hadValue = true; + String value = values.get(key); + if (value != null && !value.isEmpty()) { + hadAll &= true; + buf.append(value); + } else + hadAll = false; + } + + public Optional finish(int position, String pattern, StringBuffer ret) { + if (!hadValue) + throw new IllegalArgumentException("Invalid optional, missing inner name @ " + position +": " + pattern); + if (hadAll) + (parent == null ? ret : parent.buf).append(buf); + return parent; + } + } +} |