ExperimentDocumentService.java
package org.hammer.audio.experiment.document;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Objects;
import org.hammer.audio.plugin.AudioAnalyzerPlugin;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.dsl.WorkflowDslParser;
/**
* Shared application-facing service for safe preview and normalization of portable experiment
* documents.
*
* <p>Swing, HTTP and command-line adapters use this service instead of implementing independent
* parsing, plugin-resolution or migration paths.
*/
public final class ExperimentDocumentService {
private final ExperimentDocumentCodec codec;
private final PluginDocumentCatalog pluginCatalog;
private final WorkflowDslParser workflowParser = new WorkflowDslParser();
/** Create a service for the supplied already installed and trusted plugins. */
public ExperimentDocumentService(Collection<? extends AudioAnalyzerPlugin> installedPlugins) {
this(new ExperimentDocumentCodec(), new PluginDocumentCatalog(installedPlugins));
}
ExperimentDocumentService(ExperimentDocumentCodec codec, PluginDocumentCatalog pluginCatalog) {
this.codec = Objects.requireNonNull(codec, "codec");
this.pluginCatalog = Objects.requireNonNull(pluginCatalog, "pluginCatalog");
}
/** Parse and preview untrusted document bytes without mutating application state. */
public ExperimentDocumentPreview preview(byte[] source) throws ExperimentDocumentException {
return pluginCatalog.preview(codec.decode(source), codec);
}
/** Read, bound and preview one untrusted stream without mutating application state. */
public ExperimentDocumentPreview preview(InputStream source) throws IOException {
return preview(readBounded(source));
}
/** Parse and preview one bounded document from disk without mutating application state. */
public ExperimentDocumentPreview preview(Path source) throws IOException {
return pluginCatalog.preview(codec.load(source), codec);
}
/** Return canonical normalized bytes after the same preview and plugin-resolution path. */
public byte[] normalize(byte[] source) throws ExperimentDocumentException {
return codec.encode(preview(source).document());
}
/** Read one bounded stream and return its canonical normalized representation. */
public byte[] normalize(InputStream source) throws IOException {
return normalize(readBounded(source));
}
/**
* Save a normalized copy to a distinct path.
*
* <p>The imported source is never rewritten implicitly. Unknown optional sections remain in the
* normalized copy.
*/
public ExperimentDocumentPreview normalize(Path source, Path target) throws IOException {
Path normalizedSource = Objects.requireNonNull(source, "source").toAbsolutePath().normalize();
Path normalizedTarget = Objects.requireNonNull(target, "target").toAbsolutePath().normalize();
if (normalizedSource.equals(normalizedTarget)) {
throw new IOException("Normalize requires a distinct Save As target");
}
ExperimentDocumentPreview preview = preview(normalizedSource);
codec.save(normalizedTarget, preview.document());
return preview;
}
/** Rebuild the already hash-verified canonical workflow from a preview. */
public Workflow workflow(ExperimentDocumentPreview preview) {
Objects.requireNonNull(preview, "preview");
return workflowParser.parse(preview.document().workflow().content());
}
/** Return the checked-in public v1 schema without resolving any external URI. */
public byte[] schemaBytes() throws IOException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
loader = ClassLoader.getSystemClassLoader();
}
try (InputStream input = loader.getResourceAsStream(ExperimentDocumentFormat.SCHEMA_RESOURCE)) {
if (input == null) {
throw new IOException(
"Experiment document schema is unavailable: "
+ ExperimentDocumentFormat.SCHEMA_RESOURCE);
}
return readBounded(input);
}
}
private static byte[] readBounded(InputStream source) throws IOException {
Objects.requireNonNull(source, "source");
byte[] bytes = source.readNBytes(ExperimentDocumentFormat.MAX_DOCUMENT_BYTES + 1);
if (bytes.length > ExperimentDocumentFormat.MAX_DOCUMENT_BYTES) {
throw new ExperimentDocumentException(
"/", "max-bytes", "Experiment document exceeds the byte limit");
}
return bytes;
}
}