SchemaManifestService.java

package io.github.deweyjose.graphqlcodegen.services;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.HashSet;
import java.util.Set;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import nu.studer.java.util.OrderedProperties;
import nu.studer.java.util.OrderedProperties.OrderedPropertiesBuilder;

/** Manages a manifest of GraphQL schema files and their checksums for change detection. */
@Slf4j
public class SchemaManifestService {
  private Set<File> files;
  private final File manifestPath;
  private final File projectPath;

  /**
   * Constructs a SchemaFileManifest with a set of files, manifest path, and project path.
   *
   * @param files the set of schema files to track
   * @param manifestPath the manifest file path
   * @param projectPath the project base directory
   */
  public SchemaManifestService(Set<File> files, File manifestPath, File projectPath) {
    this.files = files;
    this.manifestPath = manifestPath;
    this.projectPath = projectPath;
  }

  /**
   * Constructs a SchemaFileManifest with a manifest path and project path.
   *
   * @param manifestDir the directory where the manifest file will be created
   * @param projectPath the project base directory
   */
  public SchemaManifestService(File manifestDir, File projectPath) {
    this.manifestPath = new File(manifestDir, "schema-manifest.props");
    this.projectPath = projectPath;
  }

  /**
   * Generates an MD5 checksum for the given file.
   *
   * @param path the file to checksum
   * @return the checksum as a hex string
   */
  @SneakyThrows
  public static String generateChecksum(File path) {
    byte[] data = Files.readAllBytes(Paths.get(path.toURI()));
    byte[] hash = MessageDigest.getInstance("MD5").digest(data);
    String checksum = new BigInteger(1, hash).toString(16);
    return checksum;
  }

  /**
   * Sets the files to be tracked by the manifest.
   *
   * @param files the set of files to track
   */
  public void setFiles(Set<File> files) {
    this.files = files;
  }

  /**
   * Computes the set of files that have changed or are new and need to trigger code generation.
   *
   * @return a set of changed or new files
   */
  public Set<File> getChangedFiles() {
    Set<File> changed = new HashSet<>();
    OrderedProperties manifest = loadManifest();
    for (File file : files) {
      String oldChecksum = manifest.getProperty(relativizeToProject(file));
      if (oldChecksum == null) {
        log.info("{} is new, will generate code", file.getName());
      } else if (!oldChecksum.equals(generateChecksum(file))) {
        log.info("{} has changed, will generate code", file.getName());
      } else {
        log.info("{} has not changed, will not generate code", file.getName());
        continue;
      }
      changed.add(file);
    }
    return changed;
  }

  /** Syncs the manifest with the files. */
  @SneakyThrows
  public void syncManifest() {
    OrderedProperties manifest =
        new OrderedPropertiesBuilder().withSuppressDateInComment(true).build();
    for (File file : files) {
      manifest.setProperty(relativizeToProject(file), generateChecksum(file));
    }

    if (!manifestPath.exists()) {
      manifestPath.getParentFile().mkdirs();
    }

    try (FileOutputStream fos = new FileOutputStream(manifestPath)) {
      manifest.store(fos, "Schema Manifest");
      fos.flush();
    }
  }

  /**
   * Loads the manifest from the manifest path, or returns an empty manifest if it does not exist.
   *
   * @return the loaded manifest properties
   * @throws java.io.IOException if an I/O error occurs reading the manifest
   */
  @SneakyThrows
  private OrderedProperties loadManifest() {
    OrderedProperties properties =
        new OrderedPropertiesBuilder().withSuppressDateInComment(true).build();
    if (manifestPath.exists()) {
      try (FileInputStream fis = new FileInputStream(manifestPath)) {
        properties.load(fis);
      }
    }
    return properties;
  }

  /**
   * Relativizes a file path to the project path.
   *
   * @param file the file to relativize
   * @return the relativized file path as a string
   */
  private String relativizeToProject(File file) {
    return projectPath.toPath().relativize(file.toPath()).toString();
  }
}