From 7a07a604c117e0bc6f1a845bbc2b0950e039f868 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Sun, 29 Sep 2024 15:05:51 +0000 Subject: [PATCH] Add command-line tools Affects: https://github.com/io7m-com/laurel/issues/73 --- com.io7m.laurel.cmdline/pom.xml | 142 +++++ .../java/com/io7m/laurel/cmdline/LCMain.java | 128 +++++ .../laurel/cmdline/internal/LCExport.java | 220 ++++++++ .../laurel/cmdline/internal/LCImport.java | 189 +++++++ .../laurel/cmdline/internal/package-info.java | 24 + .../com/io7m/laurel/cmdline/package-info.java | 26 + .../src/main/java/module-info.java | 33 ++ .../src/main/resources/logback.xml | 23 + .../src/main/resources/logback.xsd | 523 ++++++++++++++++++ .../src/main/string-template/LCVersion.st | 34 ++ com.io7m.laurel.tests/pom.xml | 5 + .../io7m/laurel/tests/LCommandLineTest.java | 155 ++++++ .../src/main/java/module-info.java | 1 + pom.xml | 23 + 14 files changed, 1526 insertions(+) create mode 100644 com.io7m.laurel.cmdline/pom.xml create mode 100644 com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/LCMain.java create mode 100644 com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCExport.java create mode 100644 com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCImport.java create mode 100644 com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/package-info.java create mode 100644 com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/package-info.java create mode 100644 com.io7m.laurel.cmdline/src/main/java/module-info.java create mode 100644 com.io7m.laurel.cmdline/src/main/resources/logback.xml create mode 100644 com.io7m.laurel.cmdline/src/main/resources/logback.xsd create mode 100644 com.io7m.laurel.cmdline/src/main/string-template/LCVersion.st create mode 100644 com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LCommandLineTest.java diff --git a/com.io7m.laurel.cmdline/pom.xml b/com.io7m.laurel.cmdline/pom.xml new file mode 100644 index 0000000..9708473 --- /dev/null +++ b/com.io7m.laurel.cmdline/pom.xml @@ -0,0 +1,142 @@ + + + + + 4.0.0 + + com.io7m.laurel + com.io7m.laurel + 1.0.0-SNAPSHOT + + com.io7m.laurel.cmdline + + jar + com.io7m.laurel.cmdline + Image caption management (Command-line tools) + https://www.io7m.com/software/laurel/ + + + + ${project.groupId} + com.io7m.laurel.filemodel + ${project.version} + + + ${project.groupId} + com.io7m.laurel.model + ${project.version} + + + + com.io7m.quarrel + com.io7m.quarrel.core + + + com.io7m.quarrel + com.io7m.quarrel.ext.logback + + + com.io7m.quarrel + com.io7m.quarrel.ext.xstructural + + + com.io7m.seltzer + com.io7m.seltzer.api + + + com.io7m.jattribute + com.io7m.jattribute.core + + + + ch.qos.logback + logback-classic + + + org.slf4j + slf4j-api + + + + org.osgi + org.osgi.annotation.bundle + provided + + + org.osgi + org.osgi.annotation.versioning + provided + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + com.io7m.quarrel:com.io7m.quarrel.ext.xstructural::* + ch.qos.logback:logback-classic::* + + + + + + com.io7m.stmp + string-template-maven-plugin + + + generate-version + generate-sources + + renderTemplate + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-sources + + add-source + + + generate-sources + + + + + ${project.build.directory}/generated-sources/string-template + + + + + + + + + + diff --git a/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/LCMain.java b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/LCMain.java new file mode 100644 index 0000000..57a882a --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/LCMain.java @@ -0,0 +1,128 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.laurel.cmdline; + +import com.io7m.laurel.cmdline.internal.LCExport; +import com.io7m.laurel.cmdline.internal.LCImport; +import com.io7m.quarrel.core.QApplication; +import com.io7m.quarrel.core.QApplicationMetadata; +import com.io7m.quarrel.core.QApplicationType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * The command-line program. + */ + +public final class LCMain implements Runnable +{ + private static final Logger LOG = + LoggerFactory.getLogger(LCMain.class); + + private final List args; + private final QApplicationType application; + private int exitCode; + + /** + * The main entry point. + * + * @param inArgs Command-line arguments + */ + + public LCMain( + final String[] inArgs) + { + this.args = + Objects.requireNonNull(List.of(inArgs), "Command line arguments"); + + final var metadata = + new QApplicationMetadata( + "laurel", + "com.io7m.laurel", + LCVersion.MAIN_VERSION, + LCVersion.MAIN_BUILD, + "The laurel command-line application.", + Optional.of(URI.create("https://www.io7m.com/software/laurel/")) + ); + + final var builder = QApplication.builder(metadata); + builder.allowAtSyntax(true); + builder.addCommand(new LCImport()); + builder.addCommand(new LCExport()); + + this.application = builder.build(); + this.exitCode = 0; + } + + /** + * The main entry point. + * + * @param args Command line arguments + */ + + public static void main( + final String[] args) + { + System.exit(mainExitless(args)); + } + + /** + * The main (exitless) entry point. + * + * @param args Command line arguments + * + * @return The exit code + */ + + public static int mainExitless( + final String[] args) + { + final LCMain cm = new LCMain(args); + cm.run(); + return cm.exitCode(); + } + + /** + * @return The program exit code + */ + + public int exitCode() + { + return this.exitCode; + } + + @Override + public void run() + { + this.exitCode = this.application.run(LOG, this.args).exitCode(); + } + + @Override + public String toString() + { + return String.format( + "[LCMain 0x%s]", + Long.toUnsignedString(System.identityHashCode(this), 16) + ); + } +} diff --git a/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCExport.java b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCExport.java new file mode 100644 index 0000000..feaab82 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCExport.java @@ -0,0 +1,220 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.laurel.cmdline.internal; + +import com.io7m.laurel.filemodel.LExportRequest; +import com.io7m.laurel.filemodel.LFileModelEvent; +import com.io7m.laurel.filemodel.LFileModelEventError; +import com.io7m.laurel.filemodel.LFileModelEventType; +import com.io7m.laurel.filemodel.LFileModelStatusIdle; +import com.io7m.laurel.filemodel.LFileModelStatusLoading; +import com.io7m.laurel.filemodel.LFileModels; +import com.io7m.laurel.model.LException; +import com.io7m.quarrel.core.QCommandContextType; +import com.io7m.quarrel.core.QCommandMetadata; +import com.io7m.quarrel.core.QCommandStatus; +import com.io7m.quarrel.core.QCommandType; +import com.io7m.quarrel.core.QParameterNamed1; +import com.io7m.quarrel.core.QParameterNamedType; +import com.io7m.quarrel.core.QStringType; +import com.io7m.quarrel.ext.logback.QLogback; +import com.io7m.seltzer.api.SStructuredErrorType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +/** + * "export" + */ + +public final class LCExport implements QCommandType, + Flow.Subscriber +{ + private static final Logger LOG = + LoggerFactory.getLogger(LCExport.class); + + private static final QParameterNamed1 INPUT_FILE = + new QParameterNamed1<>( + "--input-file", + List.of(), + new QStringType.QConstant("The input file."), + Optional.empty(), + Path.class + ); + + private static final QParameterNamed1 OUTPUT_DIRECTORY = + new QParameterNamed1<>( + "--output-directory", + List.of(), + new QStringType.QConstant("The output directory."), + Optional.empty(), + Path.class + ); + + private static final QParameterNamed1 EXPORT_IMAGES = + new QParameterNamed1<>( + "--export-images", + List.of(), + new QStringType.QConstant("Whether to export images."), + Optional.of(Boolean.TRUE), + Boolean.class + ); + + private final QCommandMetadata metadata; + private final AtomicBoolean failed; + private QCommandContextType context; + + /** + * Construct a command. + */ + + public LCExport() + { + this.metadata = new QCommandMetadata( + "export", + new QStringType.QConstant("Export a dataset into a directory."), + Optional.empty() + ); + + this.failed = new AtomicBoolean(false); + } + + @Override + public List> onListNamedParameters() + { + return Stream.concat( + Stream.of(INPUT_FILE, OUTPUT_DIRECTORY, EXPORT_IMAGES), + QLogback.parameters().stream() + ).toList(); + } + + @Override + public QCommandStatus onExecute( + final QCommandContextType newContext) + { + System.setProperty("org.jooq.no-tips", "true"); + System.setProperty("org.jooq.no-logo", "true"); + + this.context = newContext; + QLogback.configure(this.context); + + final var inputFile = + this.context.parameterValue(INPUT_FILE); + final var outputDirectory = + this.context.parameterValue(OUTPUT_DIRECTORY); + + try { + try (var model = LFileModels.open(inputFile, true)) { + model.events().subscribe(this); + + LOG.info("Waiting for dataset to finish loading..."); + final var loadLatch = new CountDownLatch(1); + model.status().subscribe((oldValue, newValue) -> { + if (oldValue instanceof LFileModelStatusLoading + && newValue instanceof LFileModelStatusIdle) { + loadLatch.countDown(); + } + }); + loadLatch.await(); + + LOG.info("Exporting dataset..."); + model.export(new LExportRequest( + outputDirectory, + this.context.parameterValue(EXPORT_IMAGES) + .booleanValue() + )).get(); + + LOG.info("Export completed."); + return QCommandStatus.SUCCESS; + } + } catch (final LException e) { + logStructuredError(e); + } catch (final InterruptedException e) { + LOG.info("Interrupted"); + } catch (final ExecutionException e) { + final var cause = e.getCause(); + if (cause instanceof final SStructuredErrorType s) { + logStructuredError(s); + } else { + LOG.error("Exception: ", e); + } + } + return QCommandStatus.FAILURE; + } + + @Override + public QCommandMetadata metadata() + { + return this.metadata; + } + + @Override + public void onSubscribe( + final Flow.Subscription subscription) + { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext( + final LFileModelEventType item) + { + switch (item) { + case final LFileModelEvent event -> { + LOG.info("{}", event.message()); + } + case final LFileModelEventError error -> { + this.failed.set(true); + logStructuredError(error); + } + } + } + + private static void logStructuredError( + final SStructuredErrorType error) + { + LOG.error("{}: {}", error.errorCode(), error.message()); + for (final var entry : error.attributes().entrySet()) { + LOG.error(" {}: {}", entry.getKey(), entry.getValue()); + } + error.exception() + .ifPresent(throwable -> LOG.error(" Exception: ", throwable)); + } + + @Override + public void onError( + final Throwable throwable) + { + LOG.error("Exception: ", throwable); + this.failed.set(true); + } + + @Override + public void onComplete() + { + + } +} diff --git a/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCImport.java b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCImport.java new file mode 100644 index 0000000..569ce59 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/LCImport.java @@ -0,0 +1,189 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.laurel.cmdline.internal; + +import com.io7m.laurel.filemodel.LFileModelEvent; +import com.io7m.laurel.filemodel.LFileModelEventError; +import com.io7m.laurel.filemodel.LFileModelEventType; +import com.io7m.laurel.filemodel.LFileModels; +import com.io7m.quarrel.core.QCommandContextType; +import com.io7m.quarrel.core.QCommandMetadata; +import com.io7m.quarrel.core.QCommandStatus; +import com.io7m.quarrel.core.QCommandType; +import com.io7m.quarrel.core.QParameterNamed1; +import com.io7m.quarrel.core.QParameterNamedType; +import com.io7m.quarrel.core.QStringType; +import com.io7m.quarrel.ext.logback.QLogback; +import com.io7m.seltzer.api.SStructuredErrorType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +/** + * "import" + */ + +public final class LCImport implements QCommandType, Flow.Subscriber +{ + private static final Logger LOG = + LoggerFactory.getLogger(LCImport.class); + + private static final QParameterNamed1 INPUT_DIRECTORY = + new QParameterNamed1<>( + "--input-directory", + List.of(), + new QStringType.QConstant("The input directory."), + Optional.empty(), + Path.class + ); + + private static final QParameterNamed1 OUTPUT_FILE = + new QParameterNamed1<>( + "--output-file", + List.of(), + new QStringType.QConstant("The output file."), + Optional.empty(), + Path.class + ); + + private final QCommandMetadata metadata; + private final AtomicBoolean failed; + private QCommandContextType context; + + /** + * Construct a command. + */ + + public LCImport() + { + this.metadata = new QCommandMetadata( + "import", + new QStringType.QConstant("Import a directory into a dataset."), + Optional.empty() + ); + + this.failed = new AtomicBoolean(false); + } + + @Override + public List> onListNamedParameters() + { + return Stream.concat( + Stream.of(INPUT_DIRECTORY, OUTPUT_FILE), + QLogback.parameters().stream() + ).toList(); + } + + @Override + public QCommandStatus onExecute( + final QCommandContextType newContext) + { + System.setProperty("org.jooq.no-tips", "true"); + System.setProperty("org.jooq.no-logo", "true"); + + this.context = newContext; + QLogback.configure(this.context); + + final var inputDirectory = + this.context.parameterValue(INPUT_DIRECTORY); + final var outputFile = + this.context.parameterValue(OUTPUT_FILE); + + try (var importer = + LFileModels.createImport(inputDirectory, outputFile)) { + importer.events().subscribe(this); + importer.execute().get(); + } catch (final ExecutionException e) { + this.failed.set(true); + final var cause = e.getCause(); + if (cause instanceof final SStructuredErrorType s) { + logStructuredError(s); + } else { + LOG.error("Exception: ", e); + } + } catch (final InterruptedException e) { + this.failed.set(true); + LOG.info("Interrupted"); + } + + if (this.failed.get()) { + return QCommandStatus.FAILURE; + } + return QCommandStatus.SUCCESS; + } + + @Override + public QCommandMetadata metadata() + { + return this.metadata; + } + + @Override + public void onSubscribe( + final Flow.Subscription subscription) + { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext( + final LFileModelEventType item) + { + switch (item) { + case final LFileModelEvent event -> { + LOG.info("{}", event.message()); + } + case final LFileModelEventError error -> { + this.failed.set(true); + logStructuredError(error); + } + } + } + + private static void logStructuredError( + final SStructuredErrorType error) + { + LOG.error("{}: {}", error.errorCode(), error.message()); + for (final var entry : error.attributes().entrySet()) { + LOG.error(" {}: {}", entry.getKey(), entry.getValue()); + } + error.exception() + .ifPresent(throwable -> LOG.error(" Exception: ", throwable)); + } + + @Override + public void onError( + final Throwable throwable) + { + LOG.error("Exception: ", throwable); + this.failed.set(true); + } + + @Override + public void onComplete() + { + + } +} diff --git a/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/package-info.java b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/package-info.java new file mode 100644 index 0000000..628ac04 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/internal/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Image caption management (Command-line tools [internals]) + */ + +@Version("1.0.0") +package com.io7m.laurel.cmdline.internal; + +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/package-info.java b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/package-info.java new file mode 100644 index 0000000..8ca71b7 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/com/io7m/laurel/cmdline/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Image caption management (Command-line tools) + */ + +@Export +@Version("1.0.0") +package com.io7m.laurel.cmdline; + +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.laurel.cmdline/src/main/java/module-info.java b/com.io7m.laurel.cmdline/src/main/java/module-info.java new file mode 100644 index 0000000..ece53d4 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/java/module-info.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Image caption management (Command-line tools) + */ + +module com.io7m.laurel.cmdline +{ + requires static org.osgi.annotation.bundle; + requires static org.osgi.annotation.versioning; + + requires com.io7m.jattribute.core; + requires com.io7m.laurel.filemodel; + requires com.io7m.laurel.model; + requires com.io7m.quarrel.core; + requires com.io7m.quarrel.ext.logback; + + exports com.io7m.laurel.cmdline; +} diff --git a/com.io7m.laurel.cmdline/src/main/resources/logback.xml b/com.io7m.laurel.cmdline/src/main/resources/logback.xml new file mode 100644 index 0000000..6b9ec35 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + %level %logger: %msg%n + + System.out + + + + + + + + + + + diff --git a/com.io7m.laurel.cmdline/src/main/resources/logback.xsd b/com.io7m.laurel.cmdline/src/main/resources/logback.xsd new file mode 100644 index 0000000..16db5d6 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/resources/logback.xsd @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.io7m.laurel.cmdline/src/main/string-template/LCVersion.st b/com.io7m.laurel.cmdline/src/main/string-template/LCVersion.st new file mode 100644 index 0000000..00708f1 --- /dev/null +++ b/com.io7m.laurel.cmdline/src/main/string-template/LCVersion.st @@ -0,0 +1,34 @@ +LCVersion( + appVersion, + appBuild) ::= << + +/* + * Copyright © 2024 Mark Raynsford \ https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package com.io7m.laurel.cmdline; + +public final class LCVersion +{ + public static final String MAIN_VERSION = ""; + public static final String MAIN_BUILD = ""; + + private LCVersion() + { + + } +} + +>> diff --git a/com.io7m.laurel.tests/pom.xml b/com.io7m.laurel.tests/pom.xml index 3cc0b69..154b93e 100644 --- a/com.io7m.laurel.tests/pom.xml +++ b/com.io7m.laurel.tests/pom.xml @@ -24,6 +24,11 @@ + + ${project.groupId} + com.io7m.laurel.cmdline + ${project.version} + ${project.groupId} com.io7m.laurel.model diff --git a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LCommandLineTest.java b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LCommandLineTest.java new file mode 100644 index 0000000..9fea9ff --- /dev/null +++ b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LCommandLineTest.java @@ -0,0 +1,155 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.laurel.tests; + +import com.io7m.laurel.cmdline.LCMain; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipInputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class LCommandLineTest +{ + private static final Logger LOG = + LoggerFactory.getLogger(LCommandLineTest.class); + + private Path directory; + + @BeforeEach + public void setup( + final @TempDir Path directory) + { + this.directory = directory; + } + + @Test + public void testHelp() + { + LCMain.mainExitless(new String[]{ + "help", + "help" + }); + } + + @Test + public void testImport() + throws IOException + { + final var inputDir = + this.unpack("dataset_good.zip", "import"); + final var outputFile = + this.directory.resolve("output.db") + .toAbsolutePath(); + + final var r = LCMain.mainExitless(new String[]{ + "import", + "--input-directory", + inputDir.toAbsolutePath().toString(), + "--output-file", + outputFile.toString() + }); + assertEquals(0, r); + + assertTrue( + Files.isRegularFile(outputFile), + () -> "Output file %s must be a regular file".formatted(outputFile) + ); + } + + + @Test + public void testExport() + throws IOException + { + final var inputDir = + this.unpack("dataset_good.zip", "import"); + final var outputFile = + this.directory.resolve("output.db") + .toAbsolutePath(); + final var outputDirectory = + this.directory.resolve("export") + .toAbsolutePath(); + + var r = LCMain.mainExitless(new String[]{ + "import", + "--input-directory", + inputDir.toAbsolutePath().toString(), + "--output-file", + outputFile.toString() + }); + assertEquals(0, r); + + r = LCMain.mainExitless(new String[]{ + "export", + "--output-directory", + outputDirectory.toAbsolutePath().toString(), + "--input-file", + outputFile.toString() + }); + assertEquals(0, r); + + assertTrue( + Files.isDirectory(outputDirectory), + () -> "Output directory %s must be a directory".formatted(outputDirectory) + ); + } + + private Path unpack( + final String zipName, + final String outputName) + throws IOException + { + final var outputDirectory = + this.directory.resolve(outputName); + + Files.createDirectories(outputDirectory); + + final var zipPath = + "/com/io7m/laurel/tests/%s".formatted(zipName); + + try (var zipStream = + LFileModelExportTest.class.getResourceAsStream(zipPath)) { + + try (var zipInputStream = new ZipInputStream(zipStream)) { + while (true) { + final var entry = zipInputStream.getNextEntry(); + if (entry == null) { + break; + } + + final var outputFile = + outputDirectory.resolve(entry.getName()); + + LOG.debug("Copy {} -> {}", entry.getName(), outputFile); + Files.copy(zipInputStream, outputFile); + } + } + } + + return outputDirectory; + } +} diff --git a/com.io7m.laurel.tests/src/main/java/module-info.java b/com.io7m.laurel.tests/src/main/java/module-info.java index be6a0bf..69c2157 100644 --- a/com.io7m.laurel.tests/src/main/java/module-info.java +++ b/com.io7m.laurel.tests/src/main/java/module-info.java @@ -25,6 +25,7 @@ requires com.io7m.laurel.model; requires com.io7m.laurel.gui; requires com.io7m.laurel.filemodel; + requires com.io7m.laurel.cmdline; requires com.io7m.anethum.api; requires com.io7m.jattribute.core; diff --git a/pom.xml b/pom.xml index bede335..9872015 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ https://www.io7m.com/software/laurel + com.io7m.laurel.cmdline com.io7m.laurel.documentation com.io7m.laurel.filemodel com.io7m.laurel.gui.main @@ -55,6 +56,7 @@ 3.19.13 5.11.1 3.46.0.1 + 1.6.1 @@ -132,6 +134,21 @@ ${javafx.version} + + com.io7m.quarrel + com.io7m.quarrel.core + ${com.io7m.quarrel.version} + + + com.io7m.quarrel + com.io7m.quarrel.ext.logback + ${com.io7m.quarrel.version} + + + com.io7m.quarrel + com.io7m.quarrel.ext.xstructural + ${com.io7m.quarrel.version} + com.io7m.jdeferthrow com.io7m.jdeferthrow.core @@ -414,6 +431,12 @@ + + com.io7m.stmp + string-template-maven-plugin + 2.0.0 + + com.io7m.jxtrand com.io7m.jxtrand.maven_plugin