From a2fe1761eade11c81a2bcc52ff8e930556dd9050 Mon Sep 17 00:00:00 2001 From: LexManos Date: Tue, 16 Oct 2018 19:41:14 -0700 Subject: Work attempting to bypass gradle's crappy caching. It caches FAILURES and uses those over the successes we provide in the custom repos! Other work directed twards cleaning up the api, and moved to using maven local which bypasses SOME of the caching and prevents the artifacts from our custom repo from being copied to the gradle central cache. --- build.gradle | 1 + .../artifactural/api/artifact/Artifact.java | 8 +- .../artifactural/api/artifact/Internal.java | 16 +- .../api/artifact/MissingArtifactException.java | 1 + .../artifactural/api/cache/ArtifactCache.java | 2 +- .../gradle/GradleRepositoryAdapter.java | 411 +++++++++------------ .../artifactural/gradle/ReflectionUtils.java | 49 ++- .../artifactural/base/artifact/ArtifactBase.java | 9 +- .../base/artifact/SimpleArtifactIdentifier.java | 9 + .../base/artifact/SimpleArtifactMetadata.java | 5 + .../base/artifact/StreamableArtifact.java | 5 + .../artifactural/base/cache/ArtifactCacheBase.java | 9 +- .../base/cache/LocatedArtifactCache.java | 43 ++- .../artifactural/base/util/HashFunction.java | 112 ++++++ .../artifactural/base/util/PatternReplace.java | 140 +++++++ 15 files changed, 549 insertions(+), 271 deletions(-) create mode 100644 src/shared/java/com/amadornes/artifactural/base/util/HashFunction.java create mode 100644 src/shared/java/com/amadornes/artifactural/base/util/PatternReplace.java 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( - "^/(?\\S+(?:/\\S+)*)/(?\\S+)/(?\\S+)/" + + "^(?\\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); + 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 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::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 getArtifactCache() { return resolver.getArtifactCache(); } + @Override public InstantiatingAction 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 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; - } + 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 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 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; + } 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 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 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 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 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); + 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 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 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 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 Entry 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 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 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 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; + } + } +} -- cgit