TaxonomyVersion.java
package org.egothor.methodatlas.ai;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Objects;
/**
* Resolves the taxonomy version identifier that is stamped into each
* AI-classified output record (the {@code ai_taxonomy_version} column / field).
*
* <p>
* The identifier answers "under which controlled vocabulary was this record
* classified?" and so must change whenever that vocabulary changes:
* </p>
* <ul>
* <li><strong>Built-in taxonomies</strong> report their explicit revision, e.g.
* {@code built-in/default@1}; see {@link DefaultSecurityTaxonomy#version()} and
* {@link OptimizedSecurityTaxonomy#version()}.</li>
* <li><strong>Custom taxonomy files</strong> (the free-text {@code -ai-taxonomy}
* format has no version field) report a content fingerprint,
* {@code sha256:<64 lowercase hex>}, of the file's raw bytes. This is the same
* digest the reproducibility receipt records as {@code taxonomyFileSha256}, so
* the two always agree.</li>
* </ul>
*
* <p>
* This class is a stateless, thread-safe utility holder.
* </p>
*
* @see AiOptions#taxonomyFile()
* @see AiOptions#taxonomyMode()
* @since 6.1.0
*/
public final class TaxonomyVersion {
/** Prefix of a content-fingerprint version identifier. */
public static final String FINGERPRINT_PREFIX = "sha256:";
private TaxonomyVersion() {
}
/**
* Returns the version identifier of a built-in taxonomy.
*
* @param mode built-in taxonomy variant; must not be {@code null}
* @return version identifier such as {@code built-in/default@1}
* @throws NullPointerException if {@code mode} is {@code null}
*/
public static String builtIn(AiOptions.TaxonomyMode mode) {
Objects.requireNonNull(mode, "mode");
return switch (mode) {
case DEFAULT -> DefaultSecurityTaxonomy.version();
case OPTIMIZED -> OptimizedSecurityTaxonomy.version();
};
}
/**
* Returns the version identifier of a custom taxonomy file: the SHA-256 of its
* raw bytes, prefixed with {@value #FINGERPRINT_PREFIX}.
*
* @param taxonomyFile path to the taxonomy file; must not be {@code null}
* and must be a readable regular file
* @return {@code sha256:} followed by 64 lowercase hex characters; the same
* file content always yields the same value
* @throws IOException if the file cannot be read
* @throws NullPointerException if {@code taxonomyFile} is {@code null}
*/
public static String ofFile(Path taxonomyFile) throws IOException {
Objects.requireNonNull(taxonomyFile, "taxonomyFile");
byte[] bytes = Files.readAllBytes(taxonomyFile);
try {
return FINGERPRINT_PREFIX
+ HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (NoSuchAlgorithmException e) {
// SHA-256 is mandated by the Java SE specification; this cannot happen.
throw new IllegalStateException("SHA-256 not available", e);
}
}
/**
* Resolves the version of the taxonomy that the supplied options select: the
* custom file when {@link AiOptions#taxonomyFile()} is set, otherwise the
* built-in {@link AiOptions#taxonomyMode()}.
*
* @param options AI options; must not be {@code null}
* @return version identifier; never {@code null} or blank
* @throws IOException if a configured taxonomy file cannot be read
* @throws NullPointerException if {@code options} is {@code null}
*/
public static String resolve(AiOptions options) throws IOException {
Objects.requireNonNull(options, "options");
if (options.taxonomyFile() != null) {
return ofFile(options.taxonomyFile());
}
return builtIn(options.taxonomyMode());
}
}