ReceiptFacade.java
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.receipt;
import java.io.IOException;
import java.nio.file.Path;
import com.fasterxml.jackson.annotation.JsonInclude;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import org.egothor.methodatlas.CliConfig;
/**
* Single public entry point used by {@code MethodAtlasApp} to materialise a
* reproducibility receipt.
*
* <p>
* The facade exists because {@link ReceiptBuilder} and {@link ReceiptWriter}
* are intentionally package-private — that keeps their helper methods,
* canonical-form constants, and Jackson configuration off the public API
* surface. The CLI orchestrator lives in a different package and would
* otherwise need each of those classes promoted to {@code public}, which
* would invite incidental external coupling.
* </p>
*
* <p>
* The shared {@link ObjectMapper} is cached on the facade so the CLI never
* pays Jackson's first-call cost more than once per JVM invocation; reuse
* across calls is safe because the mapper is configured idempotently.
* </p>
*/
public final class ReceiptFacade {
/** Default filename when {@code -receipt-file} is not supplied. */
private static final String DEFAULT_RECEIPT_FILENAME = "methodatlas-receipt.json";
private ReceiptFacade() {
// Utility class.
}
/**
* Initialisation-on-demand holder for the shared mapper. JVM class
* initialisation semantics give thread-safe, race-free lazy creation
* without {@code volatile} or {@code synchronized}.
*/
private static final class MapperHolder {
/**
* Shared, fully configured mapper instance; safe to reuse across calls.
* It is built once with indented output and {@code NON_NULL} inclusion so
* that callers never reconfigure it (which would not be thread-safe).
*/
/* default */ static final ObjectMapper INSTANCE = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.changeDefaultPropertyInclusion(
v -> JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))
.build();
}
/**
* Builds and writes a reproducibility receipt for the supplied scan
* configuration.
*
* <p>
* Resolves the output path from {@link CliConfig#receiptFile()} when set;
* otherwise falls back to {@code methodatlas-receipt.json} in the current
* working directory. The Jackson mapper is shared across calls; see the
* class-level Javadoc for the rationale.
* </p>
*
* @param config parsed CLI configuration; must not be {@code null}
* @param toolVersion resolved tool version string ({@code "dev"} when the
* JAR manifest has no implementation version)
* @param outputModeName textual name of the chosen output mode (e.g.
* {@code "SARIF"}); persisted in the receipt
* @throws IOException if any input file cannot be read for hashing or the
* receipt file cannot be written
*/
public static void emit(CliConfig config, String toolVersion, String outputModeName)
throws IOException {
ReproducibilityReceipt receipt = ReceiptBuilder.build(config, toolVersion, outputModeName);
Path target = config.receiptFile() != null
? config.receiptFile()
: Path.of(DEFAULT_RECEIPT_FILENAME);
ReceiptWriter.write(receipt, mapper(), target);
}
/**
* Returns the shared Jackson mapper, instantiated on first reference to
* the inner {@link MapperHolder} class.
*
* @return shared {@link ObjectMapper}; never {@code null}
*/
private static ObjectMapper mapper() {
return MapperHolder.INSTANCE;
}
}