All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.takari.builder.internal.BuilderInputs Maven / Gradle / Ivy

package io.takari.builder.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.takari.builder.IArtifactMetadata;
import io.takari.builder.IDirectoryFiles;
import io.takari.builder.ResourceType;
import io.takari.builder.internal.Reflection.MultivalueFactory;
import io.takari.builder.internal.Reflection.ReflectionType;
import io.takari.builder.internal.digest.BytesHash;
import io.takari.builder.internal.digest.FileDigest;
import io.takari.builder.internal.digest.SHA1Digester;

/**
 * Encapsulates builder effective configuration (aka, "expanded parameter values") and builder
 * instance factory.
 */
public class BuilderInputs {

  /**
   * Returns all input files used by the builder.
   */
  public Set getInputFiles() {
    return inputFiles;
  }

  /**
   * Returns all directories the builder can create output files in.
   */
  public Set getOutputDirectories() {
    return outputDirectories;
  }

  /**
   * Returns all specific declared files that builder can write.
   */
  public Set getOutputFiles() {
    return outputFiles;
  }

  public Set getResourceRoots() {
    return resourceRoots;
  }

  public Set getCompileSourceRoots() {
    return compileSourceRoots;
  }

  public boolean isNonDeterministic() {
    return isNonDeterministic;
  }

  /**
   * returns digest of this builder inputs configuration. the digest is Serializable and can be
   * persisted on filesystem between builder invocations. Persisted digest from the previous build
   * can be used to determine if builder inputs have changed or not.
   */
  public Digest getDigest() {
    Map> members = new LinkedHashMap<>();
    this.members.forEach((field, value) -> members.put(field.getName(), value));
    return digest(members);
  }

  public static Digest emptyDigest() {
    return new Digest();
  }

  /**
   * Creates and returns new fully configured builder instance.
   */
  public Object newBuilder() throws ReflectiveOperationException {
    Constructor constructor = type.getDeclaredConstructor();
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    Object builder = constructor.newInstance();
    for (Map.Entry> member : members.entrySet()) {
      Field field = member.getKey();
      if (!field.isAccessible()) {
        field.setAccessible(true);
      }
      field.set(builder, member.getValue().value());
    }
    return builder;
  }

  //
  // Everything below is the implementation
  //

  final Class type;
  final Map> members; // TODO change to Map>
  final Set inputFiles;
  final Set outputDirectories;
  final Set outputFiles;
  final Set resourceRoots;
  final Set compileSourceRoots;
  final boolean isNonDeterministic;

  // effective parameter configuration and factory to create injectable parameter values
  // allows analysis of effective configuration without loading builder classes
  public static interface Value {
    public T value() throws ReflectiveOperationException;

    default public void accept(InputVisitor visitor) {};
  }

  static class StringValue implements Value {
    public final String configuration;
    public final Function converter;

    public StringValue(String configuration, Function converter) {
      this.configuration = configuration;
      this.converter = converter;
    }

    @Override
    public Object value() {
      return converter.apply(configuration);
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitString(this);
    }
  }

  abstract static class FileValue implements Value {
    final Class parameterType;
    final Path configuration;

    protected FileValue(Class parameterType, Path location) {
      this.parameterType = parameterType;
      this.configuration = location;
    }

    @Override
    public final Object value() {
      if (parameterType.isAssignableFrom(Path.class)) {
        return configuration;
      }
      return configuration.toFile();
    }
  }

  static class InputFileValue extends FileValue {
    public InputFileValue(Class parameterType, Path location) {
      super(parameterType, location);
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitInputFile(this);
    }
  }

  static class OutputDirectoryValue extends FileValue {

    public OutputDirectoryValue(Class parameterType, Path location) {
      super(parameterType, location);
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitOutputDirectory(this);
    }
  }

  static class OutputFileValue extends FileValue {

    public OutputFileValue(Class parameterType, Path location) {
      super(parameterType, location);
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitOutputFile(this);
    }
  }

  static class GeneratedResourcesDirectoryValue extends FileValue {
    final ResourceType type;
    final List includes;
    final List excludes;

    public GeneratedResourcesDirectoryValue(Class parameterType, Path location,
        ResourceType type, List includes, List excludes) {
      super(parameterType, location);
      this.type = type;
      this.includes = includes;
      this.excludes = excludes;
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitResourceRoot(this);
    }
  }

  static class GeneratedSourcesDirectoryValue extends FileValue {
    final ResourceType sourceType;

    public GeneratedSourcesDirectoryValue(Class parameterType, Path location,
        ResourceType sourceType) {
      super(parameterType, location);
      this.sourceType = sourceType;
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitCompileSourceRoot(this);
    }
  }

  static class CompositeValue implements Value {
    final Class type;
    final Map> configuration;

    public CompositeValue(Class type, Map> configuration) {
      this.type = type;
      this.configuration = Collections.unmodifiableMap(configuration);
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      Object value = type.newInstance();
      // boo, java8 streams don't work with checked exceptions used by reflection
      for (Map.Entry> memberValue : configuration.entrySet()) {
        Field member = memberValue.getKey();
        if (!member.isAccessible()) {
          member.setAccessible(true);
        }
        member.set(value, memberValue.getValue().value());
      }
      return value;
    }

    @Override
    public void accept(InputVisitor visitor) {
      configuration.values().forEach(v -> v.accept(visitor));
    }
  }

  @FunctionalInterface
  static interface InstanceFactory {
    T newInstance() throws ReflectiveOperationException;
  }

  static class CollectionValue implements Value {
    final MultivalueFactory factory;
    final List> configuration;

    public CollectionValue(MultivalueFactory factory, List> elements) {
      this.factory = factory;
      this.configuration = Collections.unmodifiableList(elements);
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      List elements = new ArrayList<>();
      for (Value element : configuration) {
        elements.add(element.value());
      }
      return factory.newInstance(elements);
    }

    @Override
    public void accept(InputVisitor visitor) {
      configuration.forEach(v -> v.accept(visitor));
    }
  }

  static class MapValue implements Value> {
    final InstanceFactory> supplier;
    public final Map configuration;
    public final Function converter;

    public MapValue(InstanceFactory> supplier, Map elements,
        Function converter) {
      this.supplier = supplier;
      this.configuration = Collections.unmodifiableMap(elements);
      this.converter = converter;
    }

    @Override
    public Map value() throws ReflectiveOperationException {
      Map map = supplier.newInstance();
      for (Map.Entry element : configuration.entrySet()) {
        map.put(element.getKey(), converter.apply(element.getValue()));
      }
      return map;
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitMap(this);
    }
  }

  static class DependencyMapValue implements Value> {
    final Class type;
    final InstanceFactory> supplier;
    public final Map elements;

    public DependencyMapValue(Class type,
        InstanceFactory> supplier,
        Map elements) {
      this.type = type;
      this.supplier = supplier;
      this.elements = Collections.unmodifiableMap(elements);
    }

    @Override
    public Map value() throws ReflectiveOperationException {
      Map map = supplier.newInstance();
      for (Map.Entry element : elements.entrySet()) {
        if (type.isAssignableFrom(File.class)) {
          map.put(element.getKey(), element.getValue().toFile());
        } else if (type.isAssignableFrom(Path.class)) {
          map.put(element.getKey(), element.getValue());
        }
      }
      return map;
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitDependencyMap(this);
    }
  }

  static class InputDirectoryValue implements Value, IDirectoryFiles {
    public final Class type;

    public final Path location;
    public final Set includes;
    public final Set excludes;

    public Set files;
    public final Set filePaths;
    public final Set filenames;

    public InputDirectoryValue(Class type, Path location, List includes,
        List excludes, Set files, Set filenames) {
      this.type = type;
      this.location = location;
      this.includes =
          includes != null ? Collections.unmodifiableSet(new LinkedHashSet<>(includes)) : null;
      this.excludes =
          excludes != null ? Collections.unmodifiableSet(new LinkedHashSet<>(excludes)) : null;
      this.filePaths = Collections.unmodifiableSet(new LinkedHashSet<>(files));
      this.filenames = Collections.unmodifiableSet(new LinkedHashSet<>(filenames));
      this.files = null;
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      if (type == File.class) {
        return location.toFile();
      } else if (type == Path.class) {
        return location;
      }
      return this;
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitInputDirectory(this);
    }

    @Override
    public File location() {
      return location.toFile();
    }

    @Override
    public Path locationPath() {
      return location;
    }

    @Override
    public Set includes() {
      return includes;
    }

    @Override
    public Set excludes() {
      return excludes;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set files() {
      if (files == null) {
        files = Collections.unmodifiableSet(filePaths.stream().map(f -> f.toFile())
            .collect(Collectors.collectingAndThen(Collectors.toSet(), LinkedHashSet::new)));
      }
      return files;
    }

    @Override
    public Set filePaths() {
      return filePaths;
    }

    @Override
    public Set filenames() {
      return filenames;
    }

  }

  static class InputFilesValue implements Value {
    final MultivalueFactory factory;
    final Set files;
    final ReflectionType type;

    public InputFilesValue(MultivalueFactory factory, Collection files, ReflectionType type) {
      this.factory = factory;
      this.files = new LinkedHashSet<>(files);
      this.type = type;
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      // TODO support URL
      List members = this.files.stream().map(getMapper()).collect(Collectors.toList());
      return factory.newInstance(members);
    }

    private Function getMapper() {
      Class realType = type.isArray() ? type.adaptee().getComponentType() : type.adaptee();
      if (realType.isAssignableFrom(File.class)) {
        return Path::toFile;
      } else {
        return Function.identity();
      }
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitCollectionFileURL(this);
    }
  }

  static class ArtifactResourcesValue implements Value {
    /**
     * List of input files corresponding to these dependency resources. For exploded directory-based
     * artifacts, list of selected resources files. For jar-based artifacts, the artifact jar.
     */
    public final Set files;
    public final IArtifactMetadata artifact;
    public final Set urls;

    public ArtifactResourcesValue(Set files, IArtifactMetadata artifact,
        Collection urls) {
      this.files = Collections.unmodifiableSet(files);
      this.artifact = artifact;
      TreeSet sorted = new TreeSet<>((a, b) -> a.getPath().compareTo(b.getPath()));
      sorted.addAll(urls);
      this.urls = Collections.unmodifiableSet(sorted);
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      return new ArtifactResourcesImpl(artifact, urls);
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitArtifactResources(this);
    }
  }

  /**
   * Artificial Value that aggregates resources from {@code List} to
   * {@code Collection}.
   */
  static class ListArtifactResourcesValue implements Value {
    final MultivalueFactory factory;
    final List members;

    public ListArtifactResourcesValue(MultivalueFactory factory,
        List members) {
      this.factory = factory;
      this.members = Collections.unmodifiableList(members);
    }

    @Override
    public Object value() throws ReflectiveOperationException {
      // keep artifact order, it is significant when different dependencies have the same resource
      // assumes artifact urls are sorted by their paths already
      LinkedHashSet urls = new LinkedHashSet<>();
      for (ArtifactResourcesValue member : members) {
        urls.addAll(member.urls);
      }
      return factory.newInstance(urls);
    }

    @Override
    public void accept(InputVisitor visitor) {
      for (ArtifactResourcesValue member : members) {
        member.accept(visitor);
      }
    }
  }

  static class DependencyValue implements Value {
    final Class type;
    final IArtifactMetadata artifact;
    final Path location;

    protected DependencyValue(Class type, IArtifactMetadata artifact, Path location) {
      this.type = type;
      this.artifact = artifact;
      this.location = location;
    }

    @Override
    public final Object value() {
      if (File.class.isAssignableFrom(type)) {
        return location.toFile();
      } else if (Path.class.isAssignableFrom(type)) {
        return location;
      } else if (IArtifactMetadata.class.isAssignableFrom(type)) {
        return artifact;
      }
      throw new IllegalStateException();
    }

    @Override
    public void accept(InputVisitor visitor) {
      visitor.visitDependency(this);
    }

    public boolean isFileType() {
      return File.class.isAssignableFrom(type) || Path.class.isAssignableFrom(type);
    }
  }

  static interface InputVisitor {
    public void visitInputFile(InputFileValue inputFile);

    public void visitInputDirectory(InputDirectoryValue value);

    public void visitOutputDirectory(OutputDirectoryValue value);

    public void visitOutputFile(OutputFileValue value);

    public void visitResourceRoot(GeneratedResourcesDirectoryValue value);

    public void visitCompileSourceRoot(GeneratedSourcesDirectoryValue value);

    public void visitString(StringValue value);

    public void visitMap(MapValue value);

    public void visitCollectionFileURL(InputFilesValue value);

    public void visitArtifactResources(ArtifactResourcesValue value);

    public void visitDependency(DependencyValue value);

    public void visitDependencyMap(DependencyMapValue value);
  }

  @SuppressWarnings("serial")
  static class Digest implements Serializable {
    // no serialVersionUID, want deserialization to fail if state format changes

    private final Map inputs;
    private final Map files;

    private Digest(Map inputs, Map files) {
      if (inputs == null || files == null) {
        throw new IllegalArgumentException();
      }

      this.inputs = inputs;
      this.files = files;
    }

    private Digest() {
      this.inputs = null;
      this.files = null;
    }

    public Set files() {
      return this.files == null
          ? Collections.emptySet()
          : this.files.keySet().stream().map(f -> f.toPath())
              .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public boolean equals(Object obj) {
      if (inputs == null || files == null) {
        return false; // empty digest isn't equal to anything
      }

      if (this == obj) {
        return true;
      }
      if (!(obj instanceof Digest)) {
        return false;
      }
      Digest other = (Digest) obj;
      return inputs.equals(other.inputs) //
          && files.equals(other.files);
    }
  }

  static Digest digest(Value input) {
    return digest(Collections.singletonMap("parameter", input));
  }

  private static Digest digest(Map> members) {
    TreeMap memberDigests = new TreeMap<>();
    TreeMap fileDigests = new TreeMap<>();

    for (Map.Entry> member : members.entrySet()) {
      MessageDigest digester = SHA1Digester.newInstance();

      member.getValue().accept(new InputVisitor() {

        private void digestInput(Path value) {
          if (Files.isRegularFile(value)) {
            fileDigests.put(value.toFile(), FileDigest.digest(value));
          } else if (Files.isDirectory(value)) {
            try {
              Files.walk(value) //
                  .filter(p -> Files.isRegularFile(p)) //
                  .forEach(f -> fileDigests.put(f.toFile(), FileDigest.digest(f)));
            } catch (IOException e) {
              throw new UncheckedIOException(e);
            }
          } else {
            // does not exist
            digest(value.toString());
          }
        }

        private void digestInput(URL url) {
          digest(url.getPath());
          try (InputStream is = url.openStream()) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = is.read(buf)) >= 0) {
              digester.update(buf, 0, len);
            }
          } catch (IOException e) {
            throw new UncheckedIOException(e);
          }
        }

        private void digest(Path value) {
          digester.update(value.toString().getBytes(UTF8));
        }

        private void digest(String value) {
          if (value != null) {
            digester.update(value.getBytes(UTF8));
          }
        }

        private void digest(IArtifactMetadata value) {
          digest(value.getGroupId());
          digest(value.getArtifactId());
          digest(value.getVersion());
          digest(value.getType());
          digest(value.getClassifier());
        }

        private void digest(byte value) {
          digester.update(value);
        }

        @Override
        public void visitOutputDirectory(OutputDirectoryValue outputDirectory) {
          digest(outputDirectory.configuration);
        }

        @Override
        public void visitOutputFile(OutputFileValue outputFile) {
          digest(outputFile.configuration);
        }

        @Override
        public void visitInputFile(InputFileValue value) {
          digestInput(value.configuration);
        }

        @Override
        public void visitInputDirectory(InputDirectoryValue value) {
          digest(value.location.toString());
          if (value.includes != null) {
            value.includes.forEach(include -> digest(include));
          }
          if (value.excludes != null) {
            value.excludes.forEach(exclude -> digest(exclude));
          }
          value.filePaths.forEach(file -> digestInput(file));
        }

        @Override
        public void visitResourceRoot(GeneratedResourcesDirectoryValue value) {
          digest(value.configuration);
          if (value.includes != null) {
            value.includes.forEach(include -> digest(include));
          }
          if (value.excludes != null) {
            value.excludes.forEach(exclude -> digest(exclude));
          }
          digest((byte) value.type.ordinal());
        }

        @Override
        public void visitCompileSourceRoot(GeneratedSourcesDirectoryValue value) {
          digest(value.configuration);
          digest((byte) value.sourceType.ordinal());
        }

        @Override
        public void visitString(StringValue simple) {
          digest(simple.configuration);
        }

        @Override
        public void visitMap(MapValue value) {
          Map map = value.configuration;
          if (map != null) {
            map.entrySet().forEach(entry -> digest(entry.toString()));
          }
        }

        @Override
        public void visitDependencyMap(DependencyMapValue value) {
          Map map = value.elements;
          if (map != null) {
            map.entrySet().forEach(entry -> {
              digest(entry.getKey());
              digestInput(entry.getValue());
            });
          }
        }

        @Override
        public void visitCollectionFileURL(InputFilesValue value) {
          value.files.forEach(file -> digestInput(file));
        };

        @Override
        public void visitArtifactResources(ArtifactResourcesValue value) {
          digest(value.artifact);
          value.urls.forEach(url -> digestInput(url));
        }

        @Override
        public void visitDependency(DependencyValue value) {
          digest(value.artifact);
          if (value.isFileType()) {
            digestInput(value.location);
          }
        }
      });
      memberDigests.put(member.getKey(), new BytesHash(digester.digest()));
    }

    return new Digest(memberDigests, fileDigests);
  }

  BuilderInputs(Class type, Map> values, boolean isNonDeterministic) {
    this.type = type;
    this.members = Collections.unmodifiableMap(values);
    this.isNonDeterministic = isNonDeterministic;

    Set inputFiles = new LinkedHashSet<>();
    Set outputDirectories = new LinkedHashSet<>();
    Set outputFiles = new LinkedHashSet<>();
    Set resources = new LinkedHashSet<>();
    Set compileSourceRoots = new LinkedHashSet<>();

    InputVisitor visitor = new InputVisitor() {

      @Override
      public void visitInputFile(InputFileValue value) {
        inputFiles.add(value.configuration);
      }

      @Override
      public void visitInputDirectory(InputDirectoryValue value) {
        inputFiles.addAll(value.filePaths);
      }

      @Override
      public void visitOutputDirectory(OutputDirectoryValue value) {
        outputDirectories.add(value.configuration);
      }

      @Override
      public void visitOutputFile(OutputFileValue value) {
        outputFiles.add(value.configuration);
      }

      @Override
      public void visitResourceRoot(GeneratedResourcesDirectoryValue value) {
        outputDirectories.add(value.configuration);
        resources.add(new ResourceRoot(value.configuration.toAbsolutePath().toString(), value.type,
            value.includes, value.excludes));
      }

      @Override
      public void visitCompileSourceRoot(GeneratedSourcesDirectoryValue value) {
        outputDirectories.add(value.configuration);
        compileSourceRoots.add(new CompileSourceRoot(
            value.configuration.toAbsolutePath().toString(), value.sourceType));
      }

      @Override
      public void visitString(StringValue value) {}

      @Override
      public void visitMap(MapValue value) {}

      @Override
      public void visitCollectionFileURL(InputFilesValue value) {
        value.files.forEach(file -> inputFiles.add(file));
      }

      @Override
      public void visitArtifactResources(ArtifactResourcesValue value) {
        inputFiles.addAll(value.files);
      }

      @Override
      public void visitDependency(DependencyValue value) {
        if (value.isFileType()) {
          addDependencyInput(inputFiles, value.location);
        }
      }

      @Override
      public void visitDependencyMap(DependencyMapValue value) {
        Map map = value.elements;
        if (map != null) {
          map.entrySet().forEach(entry -> {
            addDependencyInput(inputFiles, entry.getValue());
          });
        }
      }

      private void addDependencyInput(Set inputFiles, Path path) {
        if (Files.isRegularFile(path)) {
          inputFiles.add(path);
        } else if (Files.isDirectory(path)) {
          addFilesFromDirectory(inputFiles, path);
        }
      }

      private void addFilesFromDirectory(Set inputFiles, Path path) {
        try {
          Files.walk(path).filter(Files::isRegularFile).forEach(p -> inputFiles.add(p));
        } catch (IOException e) {
          throw new UncheckedIOException(e);
        }
      }
    };

    values.values().forEach(member -> member.accept(visitor));

    this.inputFiles = Collections.unmodifiableSet(inputFiles);
    this.outputDirectories = Collections.unmodifiableSet(outputDirectories);
    this.outputFiles = Collections.unmodifiableSet(outputFiles);
    this.resourceRoots = Collections.unmodifiableSet(resources);
    this.compileSourceRoots = Collections.unmodifiableSet(compileSourceRoots);
  }

  private static final Charset UTF8 = Charset.forName("UTF-8");

}