diff --git a/com.io7m.laurel.filemodel/pom.xml b/com.io7m.laurel.filemodel/pom.xml new file mode 100644 index 0000000..27602c9 --- /dev/null +++ b/com.io7m.laurel.filemodel/pom.xml @@ -0,0 +1,176 @@ + + + + + 4.0.0 + + com.io7m.laurel + com.io7m.laurel + 0.0.1-SNAPSHOT + + com.io7m.laurel.filemodel + + jar + com.io7m.laurel.filemodel + Image caption management (File model) + https://www.io7m.com/software/laurel/ + + + + ${project.groupId} + com.io7m.laurel.model + ${project.version} + + + + com.io7m.jattribute + com.io7m.jattribute.core + + + com.io7m.lanark + com.io7m.lanark.core + + + com.io7m.jmulticlose + com.io7m.jmulticlose.core + + + com.io7m.darco + com.io7m.darco.sqlite + + + com.io7m.darco + com.io7m.darco.api + + + org.jooq + jooq + + + org.xerial + sqlite-jdbc + + + org.slf4j + slf4j-api + + + io.opentelemetry + opentelemetry-api + + + + org.osgi + org.osgi.annotation.bundle + provided + + + org.osgi + org.osgi.annotation.versioning + provided + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + + com.io7m.trasco + com.io7m.trasco.api + ${com.io7m.trasco.version} + + + com.io7m.trasco + com.io7m.trasco.vanilla + ${com.io7m.trasco.version} + + + com.io7m.trasco + com.io7m.trasco.xml.schemas + ${com.io7m.trasco.version} + + + + + + generate-sql + generate-sources + + java + + + com.io7m.trasco.vanilla.TrSchemaRevisionSetSQLMain + true + false + + ${project.basedir}/src/main/resources/com/io7m/laurel/filemodel/internal/database.xml + ${project.build.directory}/database.sql + ROLES + GRANTS + FUNCTIONS + TRIGGERS + + + + + + + + org.jooq + jooq-codegen-maven + ${org.jooq.version} + + src/main/jooq/configuration.xml + + + + jooq-codegen + generate-sources + + generate + + + + + + org.jooq + jooq-meta-extensions + ${org.jooq.version} + + + org.jooq + jooq-postgres-extensions + ${org.jooq.version} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-jooq-sources + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/jooq + + + + + + + + + diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java new file mode 100644 index 0000000..a6f5960 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java @@ -0,0 +1,131 @@ +/* + * 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.filemodel; + +import com.io7m.jattribute.core.AttributeReadableType; +import com.io7m.laurel.model.LException; +import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LTag; + +import java.net.URI; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * The interface to a file model. + */ + +public interface LFileModelType + extends AutoCloseable +{ + /** + * Add a tag. + * + * @param text The tag + * + * @return The operation in progress + */ + + CompletableFuture tagAdd( + LTag text); + + /** + * Load an image and add it to the file. + * + * @param name The (unique) name + * @param file The file + * @param source The URI source, if any + * + * @return The operation in progress + */ + + CompletableFuture imageAdd( + String name, + Path file, + Optional source + ); + + /** + * Select an image. + * + * @param name The name + * + * @return The operation in progress + */ + + CompletableFuture imageSelect( + Optional name); + + @Override + void close() + throws LException; + + /** + * @return The currently selected image + */ + + AttributeReadableType> imageSelected(); + + /** + * @return The current complete list of images + */ + + AttributeReadableType> imageList(); + + /** + * @return The current complete list of tags + */ + + AttributeReadableType> tagList(); + + /** + * @return The list of tags assigned to the current image + */ + + AttributeReadableType> tagsAssigned(); + + /** + * @return Text describing the top of the undo stack, if any + */ + + AttributeReadableType> undoText(); + + /** + * Undo the operation on the top of the undo stack + * + * @return The operation in progress + */ + + CompletableFuture undo(); + + /** + * Redo the operation on the top of the redo stack + * + * @return The operation in progress + */ + + CompletableFuture redo(); + + /** + * @return Text describing the top of the redo stack, if any + */ + + AttributeReadableType> redoText(); +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModels.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModels.java new file mode 100644 index 0000000..140d668 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModels.java @@ -0,0 +1,54 @@ +/* + * 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.filemodel; + +import com.io7m.laurel.filemodel.internal.LFileModel; +import com.io7m.laurel.model.LException; + +import java.nio.file.Path; + +/** + * The file models. + */ + +public final class LFileModels +{ + private LFileModels() + { + + } + + /** + * Open a file model. + * + * @param file The file + * @param readOnly {@code true} if the file should be read-only + * + * @return A file model + * + * @throws LException On errors + */ + + public static LFileModelType open( + final Path file, + final boolean readOnly) + throws LException + { + return LFileModel.open(file, readOnly); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandAbstract.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandAbstract.java new file mode 100644 index 0000000..0763df4 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandAbstract.java @@ -0,0 +1,122 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseException; +import com.io7m.laurel.model.LException; + +import java.util.Objects; + +/** + * The abstract base of commands. + * + * @param The type of parameters + */ + +public abstract class LCommandAbstract + implements LCommandType +{ + private boolean executed; + + /** + * @return {@code true} if this command has ever successfully executed + */ + + public final boolean isExecuted() + { + return this.executed; + } + + /** + * Set the command as executed. + * + * @param e The state + */ + + public final void setExecuted( + final boolean e) + { + this.executed = e; + } + + protected abstract LCommandUndoable onExecute( + LFileModel model, + LDatabaseTransactionType transaction, + T parameters) + throws LException, DDatabaseException; + + protected abstract void onRedo( + LFileModel model, + LDatabaseTransactionType transaction) + throws LException, DDatabaseException; + + protected abstract void onUndo( + LFileModel model, + LDatabaseTransactionType transaction) + throws LException, DDatabaseException; + + @Override + public final LCommandUndoable execute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final T parameters) + throws LException, DDatabaseException + { + Objects.requireNonNull(model, "model"); + Objects.requireNonNull(transaction, "transaction"); + Objects.requireNonNull(parameters, "parameters"); + + final var r = this.onExecute(model, transaction, parameters); + this.setExecuted(true); + return r; + } + + @Override + public final void redo( + final LFileModel model, + final LDatabaseTransactionType transaction) + throws LException, DDatabaseException + { + Objects.requireNonNull(model, "model"); + Objects.requireNonNull(transaction, "transaction"); + + if (!this.isExecuted()) { + throw new IllegalStateException( + "Cannot redo a command that has not executed."); + } + + this.onRedo(model, transaction); + } + + @Override + public final void undo( + final LFileModel model, + final LDatabaseTransactionType transaction) + throws LException, DDatabaseException + { + Objects.requireNonNull(model, "model"); + Objects.requireNonNull(transaction, "transaction"); + + if (!this.isExecuted()) { + throw new IllegalStateException( + "Cannot undo a command that has not executed."); + } + + this.onUndo(model, transaction); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactory.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactory.java new file mode 100644 index 0000000..b0a5f54 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactory.java @@ -0,0 +1,29 @@ +/* + * 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.filemodel.internal; + +import java.util.Properties; +import java.util.function.Function; + +record LCommandFactory( + String commandClass, + Function> constructor) + implements LCommandFactoryType +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactoryType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactoryType.java new file mode 100644 index 0000000..e34bea0 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandFactoryType.java @@ -0,0 +1,44 @@ +/* + * 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.filemodel.internal; + +import java.util.Properties; +import java.util.function.Function; + +/** + * A command factory. + * + * @param The type of parameters + */ + +public interface LCommandFactoryType +{ + /** + * @return The canonical name of the command class + * + * @see Class#getCanonicalName() + */ + + String commandClass(); + + /** + * @return A function to create a command from a set of properties + */ + + Function> constructor(); +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageAdd.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageAdd.java new file mode 100644 index 0000000..8b9e7c3 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageAdd.java @@ -0,0 +1,266 @@ +/* + * 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.filemodel.internal; + +import com.io7m.laurel.model.LException; +import com.io7m.laurel.model.LHashSHA256; +import com.io7m.laurel.model.LImage; +import org.jooq.DSLContext; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.IMAGES; +import static com.io7m.laurel.filemodel.internal.Tables.IMAGE_BLOBS; + +/** + * Add an image. + */ + +public final class LCommandImageAdd + extends LCommandAbstract +{ + private Long savedBlobId; + private Long savedImageId; + private String savedSourceText; + private String savedFile; + private String savedName; + + /** + * Add an image. + */ + + public LCommandImageAdd() + { + + } + + /** + * @return A command factory + */ + + public static LCommandFactoryType provider() + { + return new LCommandFactory<>( + LCommandImageAdd.class.getCanonicalName(), + LCommandImageAdd::fromProperties + ); + } + + private static LCommandImageAdd fromProperties( + final Properties p) + { + final var c = new LCommandImageAdd(); + c.savedBlobId = Long.parseUnsignedLong(p.getProperty("blob")); + c.savedImageId = Long.parseUnsignedLong(p.getProperty("id")); + c.savedSourceText = p.getProperty("source"); + c.savedFile = p.getProperty("file"); + c.savedName = p.getProperty("name"); + c.setExecuted(true); + return c; + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final LImageRequest request) + throws LException + { + final var context = + transaction.get(DSLContext.class); + + final var file = request.file().toAbsolutePath(); + model.setAttribute("ImageFile", file); + + final var imageBytes = + loadImage(model, request); + final var imageHash = + hashOf(imageBytes); + + final var blobRec = + context.insertInto(IMAGE_BLOBS) + .set(IMAGE_BLOBS.IMAGE_BLOB_SHA256, imageHash.value()) + .set(IMAGE_BLOBS.IMAGE_BLOB_DATA, imageBytes) + .returning(IMAGE_BLOBS.IMAGE_BLOB_ID) + .fetchOne(); + + this.savedName = + request.name(); + this.savedBlobId = + blobRec.get(IMAGE_BLOBS.IMAGE_BLOB_ID); + this.savedFile = + file.toString(); + this.savedSourceText = + request.source().map(URI::toString).orElse(null); + + this.savedImageId = + context.insertInto(IMAGES) + .set(IMAGES.IMAGE_BLOB, this.savedBlobId) + .set(IMAGES.IMAGE_FILE, this.savedFile) + .set(IMAGES.IMAGE_SOURCE, this.savedSourceText) + .set(IMAGES.IMAGE_NAME, this.savedName) + .returning(IMAGES.IMAGE_ID) + .fetchOne() + .get(IMAGES.IMAGE_ID); + + model.setImagesAll(listImages(transaction)); + return LCommandUndoable.COMMAND_UNDOABLE; + } + + private static List listImages( + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + return context.select( + IMAGES.IMAGE_SOURCE, + IMAGES.IMAGE_NAME, + IMAGES.IMAGE_FILE, + IMAGE_BLOBS.IMAGE_BLOB_SHA256 + ) + .from(IMAGES) + .join(IMAGE_BLOBS) + .on(IMAGE_BLOBS.IMAGE_BLOB_ID.eq(IMAGES.IMAGE_BLOB)) + .orderBy(IMAGES.IMAGE_NAME) + .stream() + .map(LCommandImageAdd::mapRecord) + .toList(); + } + + private static LImage mapRecord( + final org.jooq.Record r) + { + return new LImage( + r.get(IMAGES.IMAGE_NAME), + Optional.ofNullable(r.get(IMAGES.IMAGE_FILE)).map(Paths::get), + Optional.ofNullable(r.get(IMAGES.IMAGE_SOURCE)).map(URI::create), + new LHashSHA256(r.get(IMAGE_BLOBS.IMAGE_BLOB_SHA256)) + ); + } + + private static LHashSHA256 hashOf( + final byte[] imageBytes) + { + final MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + + digest.update(imageBytes); + return new LHashSHA256(HexFormat.of().formatHex(digest.digest())); + } + + private static byte[] loadImage( + final LFileModel model, + final LImageRequest request) + throws LException + { + try { + final var imageBytes = + Files.readAllBytes(request.file()); + + try (var imageStream = new ByteArrayInputStream(imageBytes)) { + final var image = ImageIO.read(imageStream); + if (image == null) { + throw new LException( + "Failed to load image.", + "error-image-format", + model.attributes(), + Optional.empty() + ); + } + } + + return imageBytes; + } catch (final IOException e) { + throw new LException( + "Failed to open image file.", + e, + "error-io", + model.attributes(), + Optional.empty() + ); + } + } + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.deleteFrom(IMAGES) + .where(IMAGES.IMAGE_ID.eq(this.savedImageId)) + .execute(); + + model.setImagesAll(listImages(transaction)); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.insertInto(IMAGES) + .set(IMAGES.IMAGE_ID, this.savedImageId) + .set(IMAGES.IMAGE_BLOB, this.savedBlobId) + .set(IMAGES.IMAGE_FILE, this.savedFile) + .set(IMAGES.IMAGE_SOURCE, this.savedSourceText) + .set(IMAGES.IMAGE_NAME, this.savedName) + .execute(); + + model.setImagesAll(listImages(transaction)); + } + + @Override + public Properties toProperties() + { + final var p = new Properties(); + p.setProperty("id", Long.toUnsignedString(this.savedImageId)); + p.setProperty("blob", Long.toUnsignedString(this.savedBlobId)); + p.setProperty("file", this.savedFile); + p.setProperty("source", this.savedSourceText); + p.setProperty("name", this.savedName); + return p; + } + + @Override + public String describe() + { + return "Add image '%s'".formatted(this.savedName); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSelect.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSelect.java new file mode 100644 index 0000000..6432a34 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSelect.java @@ -0,0 +1,175 @@ +/* + * 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.filemodel.internal; + +import com.io7m.laurel.model.LHashSHA256; +import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LTag; +import org.jooq.DSLContext; + +import java.net.URI; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.IMAGES; +import static com.io7m.laurel.filemodel.internal.Tables.IMAGE_BLOBS; +import static com.io7m.laurel.filemodel.internal.Tables.IMAGE_TAGS; +import static com.io7m.laurel.filemodel.internal.Tables.TAGS; + +/** + * Select an image. + */ + +public final class LCommandImageSelect + extends LCommandAbstract> +{ + /** + * Select an image. + */ + + public LCommandImageSelect() + { + + } + + /** + * @return A command factory + */ + + public static LCommandFactoryType> provider() + { + return new LCommandFactory<>( + LCommandImageSelect.class.getCanonicalName(), + LCommandImageSelect::fromProperties + ); + } + + private static LCommandImageSelect fromProperties( + final Properties p) + { + final var c = new LCommandImageSelect(); + c.setExecuted(true); + return c; + } + + private static LTag mapRecord( + final org.jooq.Record r) + { + return new LTag(r.get(TAGS.TAG_TEXT)); + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final Optional request) + { + final var context = + transaction.get(DSLContext.class); + + if (request.isEmpty()) { + model.setTagsAssigned(List.of()); + model.setImageSelected(Optional.empty()); + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + final var imageName = + request.get(); + + final var imageRecOpt = + context.select( + IMAGES.IMAGE_BLOB, + IMAGES.IMAGE_FILE, + IMAGES.IMAGE_ID, + IMAGES.IMAGE_NAME, + IMAGES.IMAGE_SOURCE, + IMAGE_BLOBS.IMAGE_BLOB_SHA256 + ).from(IMAGES) + .join(IMAGE_BLOBS) + .on(IMAGE_BLOBS.IMAGE_BLOB_ID.eq(IMAGES.IMAGE_ID)) + .where(IMAGES.IMAGE_NAME.eq(imageName)) + .fetchOptional(); + + if (imageRecOpt.isEmpty()) { + model.setTagsAssigned(List.of()); + model.setImageSelected(Optional.empty()); + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + final var imageRec = + imageRecOpt.get(); + final var imageId = + imageRec.get(IMAGES.IMAGE_ID); + + final var tags = + context.select(TAGS.TAG_TEXT) + .from(TAGS) + .join(IMAGE_TAGS) + .on(IMAGE_TAGS.IMAGE_TAG_TAG.eq(TAGS.TAG_ID)) + .where(IMAGE_TAGS.IMAGE_TAG_IMAGE.eq(imageId)) + .orderBy(TAGS.TAG_TEXT.asc()) + .stream() + .map(LCommandImageSelect::mapRecord) + .toList(); + + model.setTagsAssigned(tags); + model.setImageSelected( + Optional.of( + new LImage( + imageName, + Optional.ofNullable(imageRec.get(IMAGES.IMAGE_FILE)) + .map(Paths::get), + Optional.ofNullable(imageRec.get(IMAGES.IMAGE_SOURCE)) + .map(URI::create), + new LHashSHA256(imageRec.get(IMAGE_BLOBS.IMAGE_BLOB_SHA256)) + ) + ) + ); + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + throw new UnsupportedOperationException(); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + throw new UnsupportedOperationException(); + } + + @Override + public Properties toProperties() + { + return new Properties(); + } + + @Override + public String describe() + { + return "Select image"; + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagAdd.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagAdd.java new file mode 100644 index 0000000..9c1cb86 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagAdd.java @@ -0,0 +1,160 @@ +/* + * 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.filemodel.internal; + +import com.io7m.laurel.model.LTag; +import org.jooq.DSLContext; + +import java.util.List; +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.TAGS; + +/** + * Add a tag. + */ + +public final class LCommandTagAdd + extends LCommandAbstract +{ + private long id; + private String text; + + /** + * Add a tag. + */ + + public LCommandTagAdd() + { + + } + + /** + * Add a tag. + * + * @return A command factory + */ + + public static LCommandFactoryType provider() + { + return new LCommandFactory<>( + LCommandTagAdd.class.getCanonicalName(), + LCommandTagAdd::fromProperties + ); + } + + private static LCommandTagAdd fromProperties( + final Properties p) + { + final var c = new LCommandTagAdd(); + c.id = Long.parseUnsignedLong(p.getProperty("id")); + c.text = p.getProperty("text"); + c.setExecuted(true); + return c; + } + + private static List listTags( + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + return context.select(TAGS.TAG_TEXT) + .from(TAGS) + .orderBy(TAGS.TAG_TEXT.asc()) + .stream() + .map(r -> new LTag(r.get(TAGS.TAG_TEXT))) + .toList(); + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final LTag tag) + { + final var context = + transaction.get(DSLContext.class); + + final var recOpt = + context.insertInto(TAGS) + .set(TAGS.TAG_TEXT, tag.text()) + .onDuplicateKeyIgnore() + .returning(TAGS.TAG_ID) + .fetchOptional(); + + if (recOpt.isEmpty()) { + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + final var rec = recOpt.get(); + this.id = rec.get(TAGS.TAG_ID).longValue(); + this.text = tag.text(); + + model.setTagsAll(listTags(transaction)); + return LCommandUndoable.COMMAND_UNDOABLE; + } + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.deleteFrom(TAGS) + .where(TAGS.TAG_ID.eq(this.id)) + .execute(); + + model.setTagsAll(listTags(transaction)); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.insertInto(TAGS) + .set(TAGS.TAG_ID, this.id) + .set(TAGS.TAG_TEXT, this.text) + .onDuplicateKeyUpdate() + .set(TAGS.TAG_TEXT, this.text) + .execute(); + + model.setTagsAll(listTags(transaction)); + } + + @Override + public Properties toProperties() + { + final var properties = new Properties(); + properties.setProperty("id", Long.toUnsignedString(this.id)); + properties.setProperty("text", this.text); + return properties; + } + + @Override + public String describe() + { + return "Add tag '%s'".formatted(this.text); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandType.java new file mode 100644 index 0000000..f3c4b8c --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandType.java @@ -0,0 +1,116 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseException; +import com.io7m.laurel.model.LException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +/** + * An internal command. + * + * @param

The type of parameters + */ + +public interface LCommandType

+{ + /** + * Execute the command. + * + * @param model The model + * @param transaction The database transaction + * @param parameters The parameters + * + * @return A value indicating if the command can be undone + * + * @throws LException On errors + * @throws DDatabaseException On errors + */ + + LCommandUndoable execute( + LFileModel model, + LDatabaseTransactionType transaction, + P parameters) + throws LException, DDatabaseException; + + /** + * Undo the command. + * + * @param model The model + * @param transaction The database transaction + * + * @throws LException On errors + * @throws DDatabaseException On errors + */ + + void undo( + LFileModel model, + LDatabaseTransactionType transaction) + throws LException, DDatabaseException; + + /** + * Redo the command. + * + * @param model The model + * @param transaction The database transaction + * + * @throws LException On errors + * @throws DDatabaseException On errors + */ + + void redo( + LFileModel model, + LDatabaseTransactionType transaction) + throws LException, DDatabaseException; + + /** + * @return The command as a set of properties + */ + + Properties toProperties(); + + /** + * @return A humanly-readable description of the operation + */ + + String describe(); + + /** + * Serialize this command as XML properties. + * + * @return The serialized bytes + */ + + default byte[] serialize() + { + try (var out = new ByteArrayOutputStream()) { + final var p = this.toProperties(); + p.setProperty("@Type", this.getClass().getCanonicalName()); + p.storeToXML(out, "", StandardCharsets.UTF_8); + out.flush(); + return out.toByteArray(); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandUndoable.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandUndoable.java new file mode 100644 index 0000000..854bb01 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandUndoable.java @@ -0,0 +1,39 @@ +/* + * 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.filemodel.internal; + +/** + * A value that indicates whether a command can be undone. Even commands that + * execute state changes might sometimes not support being undone (because, + * for example, the command execution didn't result in any actual state changes). + */ + +public enum LCommandUndoable +{ + /** + * The command can be undone. + */ + + COMMAND_UNDOABLE, + + /** + * The command can't be undone. + */ + + COMMAND_NOT_UNDOABLE +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommands.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommands.java new file mode 100644 index 0000000..e9592cc --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommands.java @@ -0,0 +1,62 @@ +/* + * 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.filemodel.internal; + +import java.util.Objects; +import java.util.Properties; +import java.util.ServiceLoader; + +/** + * The command directory. + */ + +public final class LCommands +{ + private LCommands() + { + + } + + /** + * Find a suitable command for the given properties. + * + * @param properties The properties + * + * @return The command + */ + + public static LCommandType forProperties( + final Properties properties) + { + final var type = properties.getProperty("@Type"); + Objects.requireNonNull(type, "type"); + + final var factories = + ServiceLoader.load(LCommandFactoryType.class); + + for (final LCommandFactoryType factory : factories) { + if (Objects.equals(factory.commandClass(), type)) { + return factory.constructor().apply(properties); + } + } + + throw new IllegalStateException( + "No command available of type %s".formatted(type) + ); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabase.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabase.java new file mode 100644 index 0000000..a59171d --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabase.java @@ -0,0 +1,69 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseAbstract; +import com.io7m.darco.api.DDatabaseException; +import com.io7m.jmulticlose.core.CloseableCollectionType; +import io.opentelemetry.api.trace.Span; +import org.sqlite.SQLiteDataSource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Map; + +final class LDatabase + extends DDatabaseAbstract< + LDatabaseConfiguration, + LDatabaseConnectionType, + LDatabaseTransactionType, + LDatabaseQueryProviderType> + implements LDatabaseType +{ + LDatabase( + final LDatabaseConfiguration inConfiguration, + final SQLiteDataSource inDataSource, + final Collection> queryProviders, + final CloseableCollectionType resources) + { + super(inConfiguration, inDataSource, queryProviders, resources); + } + + private static void setWALMode( + final Connection connection) + throws DDatabaseException + { + try (var st = connection.createStatement()) { + st.execute("PRAGMA journal_mode=WAL;"); + } catch (final SQLException e) { + throw DDatabaseException.ofException(e); + } + } + + @Override + protected LDatabaseConnectionType createConnection( + final Span span, + final Connection connection, + final Map, LDatabaseQueryProviderType> queries) + throws DDatabaseException + { + setWALMode(connection); + return new LDatabaseConnection(this, span, connection, queries); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConfiguration.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConfiguration.java new file mode 100644 index 0000000..6cca17b --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConfiguration.java @@ -0,0 +1,63 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseCreate; +import com.io7m.darco.api.DDatabaseTelemetryType; +import com.io7m.darco.api.DDatabaseUpgrade; +import com.io7m.darco.sqlite.DSDatabaseConfigurationType; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * The configuration information for the laurel SQLite database. + * + * @param telemetry The telemetry interface + * @param create The database creation option + * @param upgrade The database upgrade option + * @param file The database file + * @param readOnly If the database should be read-only + */ + +public record LDatabaseConfiguration( + DDatabaseTelemetryType telemetry, + DDatabaseCreate create, + DDatabaseUpgrade upgrade, + Path file, + boolean readOnly) + implements DSDatabaseConfigurationType +{ + /** + * The configuration information for the laurel SQLite database. + * + * @param telemetry The telemetry interface + * @param create The database creation option + * @param upgrade The database upgrade option + * @param file The database file + * @param readOnly If the database should be read-only + */ + + public LDatabaseConfiguration + { + Objects.requireNonNull(telemetry, "telemetry"); + Objects.requireNonNull(create, "create"); + Objects.requireNonNull(upgrade, "upgrade"); + Objects.requireNonNull(file, "file"); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnection.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnection.java new file mode 100644 index 0000000..31a2041 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnection.java @@ -0,0 +1,73 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseConnectionAbstract; +import com.io7m.darco.api.DDatabaseTransactionCloseBehavior; +import io.opentelemetry.api.trace.Span; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.conf.RenderNameCase; +import org.jooq.conf.Settings; +import org.jooq.impl.DSL; + +import java.sql.Connection; +import java.util.Map; + +final class LDatabaseConnection + extends DDatabaseConnectionAbstract< + LDatabaseConfiguration, + LDatabaseTransactionType, + LDatabaseQueryProviderType> + implements LDatabaseConnectionType +{ + private static final Settings SETTINGS = + new Settings().withRenderNameCase(RenderNameCase.LOWER); + + LDatabaseConnection( + final LDatabase database, + final Span span, + final Connection connection, + final Map, LDatabaseQueryProviderType> queries) + { + super(database.configuration(), span, connection, queries); + } + + @Override + protected LDatabaseTransactionType createTransaction( + final DDatabaseTransactionCloseBehavior closeBehavior, + final Span transactionSpan, + final Map, LDatabaseQueryProviderType> queries) + { + final var transaction = new LDatabaseTransaction( + closeBehavior, + this.configuration(), + this, + transactionSpan, + queries + ); + + transaction.put(DSLContext.class, this.createContext()); + return transaction; + } + + private DSLContext createContext() + { + return DSL.using(this.connection(), SQLDialect.SQLITE, SETTINGS); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnectionType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnectionType.java new file mode 100644 index 0000000..3f23c24 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseConnectionType.java @@ -0,0 +1,30 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseConnectionType; + +/** + * The type of laurel SQLite database connections. + */ + +public interface LDatabaseConnectionType + extends DDatabaseConnectionType +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactory.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactory.java new file mode 100644 index 0000000..0b77662 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactory.java @@ -0,0 +1,117 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseException; +import com.io7m.darco.sqlite.DSDatabaseFactory; +import com.io7m.jmulticlose.core.CloseableCollectionType; +import com.io7m.lanark.core.RDottedName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sqlite.SQLiteConfig; +import org.sqlite.SQLiteDataSource; + +import java.io.InputStream; +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +/** + * The main database factory. + */ + +public final class LDatabaseFactory + extends DSDatabaseFactory< + LDatabaseConfiguration, + LDatabaseConnectionType, + LDatabaseTransactionType, + LDatabaseQueryProviderType, + LDatabaseType> + implements LDatabaseFactoryType +{ + private static final Logger LOG = + LoggerFactory.getLogger(LDatabaseFactory.class); + + /** + * The main database factory. + */ + + public LDatabaseFactory() + { + + } + + @Override + protected RDottedName applicationId() + { + return new RDottedName("com.io7m.laurel"); + } + + @Override + protected Logger logger() + { + return LOG; + } + + @Override + protected LDatabaseType onCreateDatabase( + final LDatabaseConfiguration configuration, + final SQLiteDataSource source, + final List> queryProviders, + final CloseableCollectionType resources) + { + return new LDatabase( + configuration, + source, + queryProviders, + resources + ); + } + + @Override + protected InputStream onRequireDatabaseSchemaXML() + { + return LDatabaseFactory.class.getResourceAsStream( + "/com/io7m/laurel/filemodel/internal/database.xml" + ); + } + + @Override + protected void onEvent( + final String message) + { + + } + + @Override + protected void onAdjustSQLiteConfig( + final SQLiteConfig config) + { + + } + + @Override + protected List> onRequireDatabaseQueryProviders() + { + return ServiceLoader.load(LDatabaseQueryProviderType.class) + .stream() + .map(ServiceLoader.Provider::get) + .map(x -> (LDatabaseQueryProviderType) x) + .collect(Collectors.toList()); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactoryType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactoryType.java new file mode 100644 index 0000000..7666a06 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseFactoryType.java @@ -0,0 +1,35 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseFactoryType; + +/** + * The type of laurel SQLite database factories. + */ + +public interface LDatabaseFactoryType + extends DDatabaseFactoryType< + LDatabaseConfiguration, + LDatabaseConnectionType, + LDatabaseTransactionType, + LDatabaseQueryProviderType, + LDatabaseType> +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryAbstract.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryAbstract.java new file mode 100644 index 0000000..1def8cf --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryAbstract.java @@ -0,0 +1,37 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseQueryAbstract; + +/** + * An abstract query for the laurel SQLite database. + * + * @param

The query parameters + * @param The query results + */ + +public abstract class LDatabaseQueryAbstract + extends DDatabaseQueryAbstract +{ + protected LDatabaseQueryAbstract( + final LDatabaseTransactionType t) + { + super(t); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryProviderType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryProviderType.java new file mode 100644 index 0000000..016a09b --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseQueryProviderType.java @@ -0,0 +1,35 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseQueryProviderType; +import com.io7m.darco.api.DDatabaseQueryType; + +/** + * The type of laurel SQLite database query providers. + * + * @param

The type of query parameters + * @param The type of query results + * @param The precise type of query + */ + +public interface LDatabaseQueryProviderType> + extends DDatabaseQueryProviderType +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransaction.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransaction.java new file mode 100644 index 0000000..e0c12ba --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransaction.java @@ -0,0 +1,49 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseTransactionAbstract; +import com.io7m.darco.api.DDatabaseTransactionCloseBehavior; +import io.opentelemetry.api.trace.Span; + +import java.util.Map; + +final class LDatabaseTransaction + extends DDatabaseTransactionAbstract< + LDatabaseConfiguration, + LDatabaseConnectionType, + LDatabaseTransactionType, + LDatabaseQueryProviderType> + implements LDatabaseTransactionType +{ + LDatabaseTransaction( + final DDatabaseTransactionCloseBehavior closeBehavior, + final LDatabaseConfiguration inConfiguration, + final LDatabaseConnectionType inConnection, + final Span inTransactionScope, + final Map, LDatabaseQueryProviderType> queries) + { + super( + closeBehavior, + inConfiguration, + inConnection, + inTransactionScope, + queries + ); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransactionType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransactionType.java new file mode 100644 index 0000000..f738969 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseTransactionType.java @@ -0,0 +1,30 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseTransactionType; + +/** + * The type of laurel SQLite database transactions. + */ + +public interface LDatabaseTransactionType + extends DDatabaseTransactionType +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseType.java new file mode 100644 index 0000000..9c264e8 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LDatabaseType.java @@ -0,0 +1,34 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseType; + +/** + * The type of laurel SQLite databases. + */ + +public interface LDatabaseType + extends DDatabaseType< + LDatabaseConfiguration, + LDatabaseConnectionType, + LDatabaseTransactionType, + LDatabaseQueryProviderType> +{ + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java new file mode 100644 index 0000000..d70d44f --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java @@ -0,0 +1,618 @@ +/* + * 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.filemodel.internal; + +import com.io7m.darco.api.DDatabaseCreate; +import com.io7m.darco.api.DDatabaseException; +import com.io7m.darco.api.DDatabaseTelemetryNoOp; +import com.io7m.darco.api.DDatabaseUpgrade; +import com.io7m.jattribute.core.AttributeReadableType; +import com.io7m.jattribute.core.AttributeType; +import com.io7m.jattribute.core.Attributes; +import com.io7m.laurel.filemodel.LFileModelType; +import com.io7m.laurel.model.LException; +import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LTag; +import org.jooq.DSLContext; +import org.jooq.Record; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +import static com.io7m.laurel.filemodel.internal.Tables.REDO; +import static com.io7m.laurel.filemodel.internal.Tables.UNDO; +import static java.time.ZoneOffset.UTC; + +/** + * The file model. + */ + +public final class LFileModel implements LFileModelType +{ + private static final Logger LOG = + LoggerFactory.getLogger(LFileModel.class); + + private static final Attributes ATTRIBUTES = + Attributes.create(throwable -> { + LOG.error("Uncaught attribute exception: ", throwable); + }); + + private final AttributeType> imagesAll; + private final AttributeType> tagsAll; + private final AttributeType> tagsAssigned; + private final AttributeType>> redo; + private final AttributeType>> undo; + private final AttributeType> imageSelected; + private final AttributeType> redoText; + private final AttributeType> undoText; + private final ConcurrentHashMap attributes; + private final LDatabaseType database; + private final ReentrantLock commandLock; + + private LFileModel( + final LDatabaseType inDatabase) + { + this.database = + Objects.requireNonNull(inDatabase, "database"); + this.tagsAll = + ATTRIBUTES.create(List.of()); + this.tagsAssigned = + ATTRIBUTES.create(List.of()); + this.imagesAll = + ATTRIBUTES.create(List.of()); + this.imageSelected = + ATTRIBUTES.create(Optional.empty()); + this.undo = + ATTRIBUTES.create(Optional.empty()); + this.undoText = + this.undo.map(o -> o.map(LCommandType::describe)); + this.redo = + ATTRIBUTES.create(Optional.empty()); + this.redoText = + this.redo.map(o -> o.map(LCommandType::describe)); + this.commandLock = + new ReentrantLock(); + this.attributes = + new ConcurrentHashMap<>(); + } + + /** + * Open a file model. + * + * @param file The file + * @param readOnly {@code true} if the file should be read-only + * + * @return A file model + * + * @throws LException On errors + */ + + public static LFileModelType open( + final Path file, + final boolean readOnly) + throws LException + { + try { + final var databases = + new LDatabaseFactory(); + + if (readOnly) { + return new LFileModel( + databases.open( + new LDatabaseConfiguration( + DDatabaseTelemetryNoOp.get(), + DDatabaseCreate.DO_NOT_CREATE_DATABASE, + DDatabaseUpgrade.DO_NOT_UPGRADE_DATABASE, + file, + readOnly + ), + event -> { + + } + ) + ); + } + + return new LFileModel( + databases.open( + new LDatabaseConfiguration( + DDatabaseTelemetryNoOp.get(), + DDatabaseCreate.CREATE_DATABASE, + DDatabaseUpgrade.UPGRADE_DATABASE, + file, + readOnly + ), + event -> { + + } + ) + ); + } catch (final DDatabaseException e) { + throw new LException( + e.getMessage(), + e, + e.errorCode(), + e.attributes(), + e.remediatingAction() + ); + } + } + + private static long nowMilliseconds() + { + return OffsetDateTime.now(UTC) + .toInstant() + .toEpochMilli(); + } + + private static void dbUndoMoveToRedo( + final LDatabaseTransactionType t, + final Record oldCommandRec) + { + final var context = + t.get(DSLContext.class); + + final var id = + oldCommandRec.get(UNDO.UNDO_ID); + + context.deleteFrom(UNDO) + .where(UNDO.UNDO_ID.eq(id)) + .execute(); + + context.insertInto(REDO) + .set(REDO.REDO_ID, id) + .set(REDO.REDO_DESCRIPTION, oldCommandRec.get(UNDO.UNDO_DESCRIPTION)) + .set(REDO.REDO_DATA, oldCommandRec.get(UNDO.UNDO_DATA)) + .set(REDO.REDO_TIME, oldCommandRec.get(UNDO.UNDO_TIME)) + .execute(); + } + + private static void dbRedoMoveToUndo( + final LDatabaseTransactionType t, + final Record oldCommandRec) + { + final var context = + t.get(DSLContext.class); + + final var id = + oldCommandRec.get(REDO.REDO_ID); + + context.deleteFrom(REDO) + .where(REDO.REDO_ID.eq(id)) + .execute(); + + context.insertInto(UNDO) + .set(UNDO.UNDO_ID, id) + .set(UNDO.UNDO_DESCRIPTION, oldCommandRec.get(REDO.REDO_DESCRIPTION)) + .set(UNDO.UNDO_DATA, oldCommandRec.get(REDO.REDO_DATA)) + .set(UNDO.UNDO_TIME, oldCommandRec.get(REDO.REDO_TIME)) + .execute(); + } + + private static LCommandType parseUndoCommandFromProperties( + final org.jooq.Record rec) + throws IOException + { + final var properties = + new Properties(); + + try (var stream = new ByteArrayInputStream(rec.get(UNDO.UNDO_DATA))) { + properties.loadFromXML(stream); + } + + return LCommands.forProperties(properties); + } + + private static LCommandType parseRedoCommandFromProperties( + final org.jooq.Record rec) + throws IOException + { + final var properties = + new Properties(); + + try (var stream = new ByteArrayInputStream(rec.get(REDO.REDO_DATA))) { + properties.loadFromXML(stream); + } + + return LCommands.forProperties(properties); + } + + private static Optional dbUndoGetTip( + final LDatabaseTransactionType t) + { + final var context = + t.get(DSLContext.class); + + return context.select( + UNDO.UNDO_DATA, + UNDO.UNDO_TIME, + UNDO.UNDO_DESCRIPTION, + UNDO.UNDO_ID + ).from(UNDO) + .orderBy(UNDO.UNDO_TIME.desc(), UNDO.UNDO_ID.desc()) + .limit(1) + .fetchOptional() + .map(x -> x); + } + + private static Optional dbRedoGetTip( + final LDatabaseTransactionType t) + { + final var context = + t.get(DSLContext.class); + + return context.select( + REDO.REDO_DATA, + REDO.REDO_TIME, + REDO.REDO_DESCRIPTION, + REDO.REDO_ID + ).from(REDO) + .orderBy(REDO.REDO_TIME.asc(), REDO.REDO_ID.asc()) + .limit(1) + .fetchOptional() + .map(x -> x); + } + + void setAttribute( + final String name, + final String value) + { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(value, "value"); + + this.attributes.put(name, value); + } + + void setAttribute( + final String name, + final Object value) + { + this.setAttribute(name, value.toString()); + } + + @Override + public CompletableFuture tagAdd( + final LTag text) + { + return this.runCommand(new LCommandTagAdd(), text); + } + + @Override + public CompletableFuture imageAdd( + final String name, + final Path file, + final Optional source) + { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(file, "file"); + Objects.requireNonNull(source, "source"); + + return this.runCommand( + new LCommandImageAdd(), + new LImageRequest(name, file, source) + ); + } + + @Override + public CompletableFuture imageSelect( + final Optional name) + { + Objects.requireNonNull(name, "name"); + + return this.runCommand( + new LCommandImageSelect(), + name + ); + } + + private > + CompletableFuture + runCommand( + final C command, + final P parameters) + { + final var future = new CompletableFuture(); + Thread.ofVirtual() + .start(() -> { + try { + this.executeCommandLocked(command, parameters); + future.complete(null); + } catch (final Throwable e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private > + void executeCommandLocked( + final C command, + final P parameters) + throws Exception + { + this.commandLock.lock(); + + try { + this.attributes.clear(); + + try (var t = this.database.openTransaction()) { + final var undoable = + command.execute(this, t, parameters); + + switch (undoable) { + case COMMAND_UNDOABLE -> { + final var context = t.get(DSLContext.class); + context.insertInto(UNDO) + .set(UNDO.UNDO_DESCRIPTION, command.describe()) + .set(UNDO.UNDO_TIME, Long.valueOf(nowMilliseconds())) + .set(UNDO.UNDO_DATA, command.serialize()) + .execute(); + } + case COMMAND_NOT_UNDOABLE -> { + + } + } + + t.commit(); + + switch (undoable) { + case COMMAND_UNDOABLE -> { + this.undo.set(Optional.of(command)); + } + case COMMAND_NOT_UNDOABLE -> { + + } + } + } catch (final DDatabaseException e) { + throw new LException( + e.getMessage(), + e, + e.errorCode(), + e.attributes(), + e.remediatingAction() + ); + } + } finally { + this.commandLock.unlock(); + } + } + + @Override + public void close() + throws LException + { + try { + this.database.close(); + } catch (final DDatabaseException e) { + throw new LException( + e.getMessage(), + e, + e.errorCode(), + e.attributes(), + e.remediatingAction() + ); + } + } + + @Override + public AttributeReadableType> imageSelected() + { + return this.imageSelected; + } + + @Override + public AttributeReadableType> imageList() + { + return this.imagesAll; + } + + @Override + public AttributeReadableType> tagList() + { + return this.tagsAll; + } + + @Override + public AttributeReadableType> tagsAssigned() + { + return this.tagsAssigned; + } + + @Override + public AttributeReadableType> undoText() + { + return this.undoText; + } + + @Override + public CompletableFuture undo() + { + final var future = new CompletableFuture(); + Thread.ofVirtual() + .start(() -> { + try { + this.executeUndo(); + future.complete(null); + } catch (final Throwable e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private void executeUndo() + throws Exception + { + this.commandLock.lock(); + + try { + this.attributes.clear(); + + try (var t = this.database.openTransaction()) { + final var oldCommandRecOpt = dbUndoGetTip(t); + if (oldCommandRecOpt.isEmpty()) { + return; + } + + final var oldCommandRec = + oldCommandRecOpt.get(); + final var oldCommand = + parseUndoCommandFromProperties(oldCommandRec); + + oldCommand.undo(this, t); + dbUndoMoveToRedo(t, oldCommandRec); + t.commit(); + + this.redo.set(Optional.of(oldCommand)); + + final var newCommandRecOpt = dbUndoGetTip(t); + if (newCommandRecOpt.isPresent()) { + this.undo.set(Optional.of( + parseUndoCommandFromProperties(newCommandRecOpt.get())) + ); + } else { + this.undo.set(Optional.empty()); + } + + } catch (final DDatabaseException e) { + this.attributes.putAll(e.attributes()); + + throw new LException( + e.getMessage(), + e, + e.errorCode(), + Map.copyOf(this.attributes), + e.remediatingAction() + ); + } + } finally { + this.commandLock.unlock(); + } + } + + @Override + public CompletableFuture redo() + { + final var future = new CompletableFuture(); + Thread.ofVirtual() + .start(() -> { + try { + this.executeRedo(); + future.complete(null); + } catch (final Throwable e) { + future.completeExceptionally(e); + } + }); + return future; + } + + @Override + public AttributeReadableType> redoText() + { + return this.redoText; + } + + private void executeRedo() + throws Exception + { + this.commandLock.lock(); + + try { + this.attributes.clear(); + + try (var t = this.database.openTransaction()) { + final var oldCommandRecOpt = dbRedoGetTip(t); + if (oldCommandRecOpt.isEmpty()) { + return; + } + + final var oldCommandRec = + oldCommandRecOpt.get(); + final var oldCommand = + parseRedoCommandFromProperties(oldCommandRec); + + oldCommand.redo(this, t); + dbRedoMoveToUndo(t, oldCommandRec); + t.commit(); + + this.undo.set(Optional.of(oldCommand)); + + final var newCommandRecOpt = dbRedoGetTip(t); + if (newCommandRecOpt.isPresent()) { + this.redo.set(Optional.of( + parseRedoCommandFromProperties(newCommandRecOpt.get())) + ); + } else { + this.redo.set(Optional.empty()); + } + } catch (final DDatabaseException e) { + throw new LException( + e.getMessage(), + e, + e.errorCode(), + e.attributes(), + e.remediatingAction() + ); + } + } finally { + this.commandLock.unlock(); + } + } + + void setImagesAll( + final List images) + { + this.imagesAll.set(Objects.requireNonNull(images, "images")); + } + + void setTagsAll( + final List tags) + { + this.tagsAll.set(Objects.requireNonNull(tags, "tags")); + } + + Map attributes() + { + return Map.copyOf(this.attributes); + } + + void setTagsAssigned( + final List tags) + { + this.tagsAssigned.set(tags); + } + + void setImageSelected( + final Optional image) + { + this.imageSelected.set(image); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageRequest.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageRequest.java new file mode 100644 index 0000000..a52cc6b --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageRequest.java @@ -0,0 +1,52 @@ +/* + * 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.filemodel.internal; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +/** + * An image request. + * + * @param name The name + * @param file The file + * @param source The source + */ + +public record LImageRequest( + String name, + Path file, + Optional source) +{ + /** + * An image request. + * + * @param name The name + * @param file The file + * @param source The source + */ + + public LImageRequest + { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(file, "file"); + Objects.requireNonNull(source, "source"); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/package-info.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/package-info.java new file mode 100644 index 0000000..945ede5 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/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 (File model) + */ + +@Version("1.0.0") +package com.io7m.laurel.filemodel.internal; + +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/package-info.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/package-info.java new file mode 100644 index 0000000..d0cdc09 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/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 (File model) + */ + +@Export +@Version("1.0.0") +package com.io7m.laurel.filemodel; + +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.laurel.filemodel/src/main/java/module-info.java b/com.io7m.laurel.filemodel/src/main/java/module-info.java new file mode 100644 index 0000000..035767f --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/module-info.java @@ -0,0 +1,54 @@ +/* + * 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. + */ + +import com.io7m.laurel.filemodel.internal.LCommandFactoryType; +import com.io7m.laurel.filemodel.internal.LCommandImageAdd; +import com.io7m.laurel.filemodel.internal.LCommandImageSelect; +import com.io7m.laurel.filemodel.internal.LCommandTagAdd; + +/** + * Image caption management (File model) + */ + +module com.io7m.laurel.filemodel +{ + uses com.io7m.laurel.filemodel.internal.LDatabaseQueryProviderType; + uses com.io7m.laurel.filemodel.internal.LCommandFactoryType; + + requires static org.osgi.annotation.bundle; + requires static org.osgi.annotation.versioning; + + requires com.io7m.darco.api; + requires com.io7m.darco.sqlite; + requires com.io7m.jmulticlose.core; + requires com.io7m.lanark.core; + requires io.opentelemetry.api; + requires org.jooq; + requires org.slf4j; + requires org.xerial.sqlitejdbc; + requires com.io7m.laurel.model; + requires com.io7m.jattribute.core; + requires java.desktop; + + provides LCommandFactoryType with + LCommandTagAdd, + LCommandImageAdd, + LCommandImageSelect; + + exports com.io7m.laurel.filemodel; + exports com.io7m.laurel.filemodel.internal + to com.io7m.laurel.tests; +} diff --git a/com.io7m.laurel.filemodel/src/main/jooq/configuration.xml b/com.io7m.laurel.filemodel/src/main/jooq/configuration.xml new file mode 100644 index 0000000..576ddc6 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/jooq/configuration.xml @@ -0,0 +1,73 @@ + + + + false + + + + org.jooq.meta.extensions.ddl.DDLDatabase + + + + BIGINT + .* + INTEGER + ALL + COLUMN + + + + + + + + scripts + target/database.sql + + + + + sort + semantic + + + + + unqualifiedSchema + none + + + + + defaultNameCase + as_is + + + + + + com.io7m.laurel.filemodel.internal + target/generated-sources/jooq + + + \ No newline at end of file diff --git a/com.io7m.laurel.filemodel/src/main/resources/com/io7m/laurel/filemodel/internal/database.xml b/com.io7m.laurel.filemodel/src/main/resources/com/io7m/laurel/filemodel/internal/database.xml new file mode 100644 index 0000000..85ef6ec --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/resources/com/io7m/laurel/filemodel/internal/database.xml @@ -0,0 +1,147 @@ + + + + + + + The schema version table stores the current version of the database schema. Implementations are expected to query + this table on connecting to the database in order to ensure that the calling code is compatible with the tables in + the database. + + + + + + + + The image_blobs table stores the image blobs. + + + + + + The images table stores the complete set of images. + + + + + + The tags table stores the complete set of available tags. + + + + + + The image_tags table stores the associations between images and tags. + + + + + + The undo table stores the undo history. + + + + + + The redo table stores the redo history. + + + + + + diff --git a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/model/LModel.java b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/model/LModel.java index a08d0fe..b6a2aaa 100644 --- a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/model/LModel.java +++ b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/model/LModel.java @@ -18,7 +18,7 @@ package com.io7m.laurel.gui.internal.model; import com.io7m.laurel.gui.internal.model.LModelFileStatusType.None; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaption; import com.io7m.laurel.model.LImageCaptionID; import com.io7m.laurel.model.LImageID; @@ -219,7 +219,7 @@ public LImageSet createImageSet() final var outCaptions = new TreeMap(); final var outImages = - new TreeMap(); + new TreeMap(); for (final var entry : this.captions.entrySet()) { final var caption = @@ -240,7 +240,7 @@ public LImageSet createImageSet() directory.relativize(imageFileName); final var image = - new LImage( + new LOldImage( entry.getKey(), savedImageFileName.toString(), new TreeSet<>( diff --git a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/LImporters.java b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/LImporters.java index 70fd539..6b4a892 100644 --- a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/LImporters.java +++ b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/LImporters.java @@ -18,7 +18,7 @@ package com.io7m.laurel.io; import com.io7m.jdeferthrow.core.ExceptionTracker; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaption; import com.io7m.laurel.model.LImageCaptionID; import com.io7m.laurel.model.LImageID; @@ -83,7 +83,7 @@ public static LImageSet importDirectory( new ExceptionTracker(); final var images = - new TreeMap(); + new TreeMap(); final var captions = new TreeMap(); final var captionTexts = @@ -125,7 +125,7 @@ public static LImageSet importDirectory( return new LImageSet(List.of(), captions, images); } - private static LImage processImageFile( + private static LOldImage processImageFile( final ExceptionTracker exceptionTracker, final int fileIndex, final Path sourceDirectory, @@ -198,7 +198,7 @@ private static LImage processImageFile( ++captionIndex; } - return new LImage( + return new LOldImage( new LImageID( UUID.nameUUIDFromBytes( file.toAbsolutePath() diff --git a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImage.java b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImage.java index e9d38cc..4fc9de6 100644 --- a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImage.java +++ b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImage.java @@ -21,7 +21,7 @@ import com.io7m.blackthorne.core.BTElementHandlerType; import com.io7m.blackthorne.core.BTElementParsingContextType; import com.io7m.blackthorne.core.BTQualifiedName; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaptionID; import com.io7m.laurel.model.LImageID; import org.xml.sax.Attributes; @@ -37,7 +37,7 @@ */ public final class L1EImage - implements BTElementHandlerType + implements BTElementHandlerType { private final TreeSet captions; private LImageID imageId; @@ -88,10 +88,10 @@ public void onChildValueProduced( } @Override - public LImage onElementFinished( + public LOldImage onElementFinished( final BTElementParsingContextType context) { - return new LImage( + return new LOldImage( this.imageId, this.fileName, this.captions diff --git a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImageSet.java b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImageSet.java index f8fb9bc..1f9d6bd 100644 --- a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImageSet.java +++ b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImageSet.java @@ -21,7 +21,7 @@ import com.io7m.blackthorne.core.BTElementHandlerType; import com.io7m.blackthorne.core.BTElementParsingContextType; import com.io7m.blackthorne.core.BTQualifiedName; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaption; import com.io7m.laurel.model.LImageCaptionID; import com.io7m.laurel.model.LImageID; @@ -41,7 +41,7 @@ public final class L1EImageSet implements BTElementHandlerType { - private final TreeMap images; + private final TreeMap images; private final TreeMap captions; private final ArrayList globalPrefixCaptions; @@ -79,9 +79,9 @@ public void onChildValueProduced( switch (result) { case final List xs when !xs.isEmpty() -> { switch (xs.get(0)) { - case final LImage ignored0 -> { + case final LOldImage ignored0 -> { for (final var x : xs) { - final var i = (LImage) x; + final var i = (LOldImage) x; this.images.put(i.imageID(), i); } } diff --git a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImages.java b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImages.java index 8f2aaa0..825fa00 100644 --- a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImages.java +++ b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/L1EImages.java @@ -21,7 +21,7 @@ import com.io7m.blackthorne.core.BTElementHandlerType; import com.io7m.blackthorne.core.BTElementParsingContextType; import com.io7m.blackthorne.core.BTQualifiedName; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import java.util.ArrayList; import java.util.List; @@ -34,9 +34,9 @@ */ public final class L1EImages - implements BTElementHandlerType> + implements BTElementHandlerType> { - private final ArrayList images; + private final ArrayList images; /** * An element handler. @@ -51,7 +51,7 @@ public L1EImages( } @Override - public Map> + public Map> onChildHandlersRequested( final BTElementParsingContextType context) { @@ -66,13 +66,13 @@ public L1EImages( @Override public void onChildValueProduced( final BTElementParsingContextType context, - final LImage result) + final LOldImage result) { this.images.add(result); } @Override - public List onElementFinished( + public List onElementFinished( final BTElementParsingContextType context) { return List.copyOf(this.images); diff --git a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/LSerializer.java b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/LSerializer.java index 1d8f891..f55e85f 100644 --- a/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/LSerializer.java +++ b/com.io7m.laurel.io/src/main/java/com/io7m/laurel/io/internal/LSerializer.java @@ -19,7 +19,7 @@ import com.io7m.anethum.api.SerializationException; import com.io7m.laurel.io.LSchemas; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaption; import com.io7m.laurel.model.LImageSet; import com.io7m.laurel.writer.api.LSerializerType; @@ -143,7 +143,7 @@ private void writeImages( } private void writeImage( - final LImage image) + final LOldImage image) throws XMLStreamException { this.writer.writeStartElement("Image"); diff --git a/com.io7m.laurel.model/pom.xml b/com.io7m.laurel.model/pom.xml index 4272d3a..89cd577 100644 --- a/com.io7m.laurel.model/pom.xml +++ b/com.io7m.laurel.model/pom.xml @@ -19,6 +19,10 @@ https://www.io7m.com/software/laurel/ + + com.io7m.seltzer + com.io7m.seltzer.api + com.io7m.jaffirm com.io7m.jaffirm.core diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LException.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LException.java new file mode 100644 index 0000000..46837cc --- /dev/null +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LException.java @@ -0,0 +1,248 @@ +/* + * 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.model; + +import com.io7m.seltzer.api.SStructuredErrorExceptionType; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * The type of exceptions raised by the package. + */ + +public class LException + extends Exception + implements SStructuredErrorExceptionType +{ + private final String errorCode; + private final Map attributes; + private final Optional remediatingAction; + + /** + * Construct an exception. + * + * @param cause The cause + * @param inErrorCode The error code + * @param inAttributes The attributes + * @param inRemediatingAction The remediating action + */ + + public LException( + final Throwable cause, + final String inErrorCode, + final Map inAttributes, + final Optional inRemediatingAction) + { + super( + Objects.requireNonNullElse( + cause.getMessage(), + cause.getClass().getSimpleName() + ), + Objects.requireNonNull(cause, "cause") + ); + + this.errorCode = + Objects.requireNonNull(inErrorCode, "errorCode"); + this.attributes = + Map.copyOf(inAttributes); + this.remediatingAction = + Objects.requireNonNull(inRemediatingAction, "remediatingAction"); + } + + /** + * Construct an exception. + * + * @param message The message + * @param inErrorCode The error code + * @param inAttributes The attributes + * @param inRemediatingAction The remediating action + */ + + public LException( + final String message, + final String inErrorCode, + final Map inAttributes, + final Optional inRemediatingAction) + { + super(Objects.requireNonNull(message, "message")); + + this.errorCode = + Objects.requireNonNull(inErrorCode, "errorCode"); + this.attributes = + Map.copyOf(inAttributes); + this.remediatingAction = + Objects.requireNonNull(inRemediatingAction, "remediatingAction"); + } + + /** + * Construct an exception. + * + * @param message The message + * @param cause The cause + * @param inErrorCode The error code + * @param inAttributes The attributes + * @param inRemediatingAction The remediating action + */ + + public LException( + final String message, + final Throwable cause, + final String inErrorCode, + final Map inAttributes, + final Optional inRemediatingAction) + { + super( + Objects.requireNonNull(message, "message"), + Objects.requireNonNull(cause, "cause") + ); + + this.errorCode = + Objects.requireNonNull(inErrorCode, "errorCode"); + this.attributes = + Map.copyOf(inAttributes); + this.remediatingAction = + Objects.requireNonNull(inRemediatingAction, "remediatingAction"); + } + + /** + * Construct an exception. + * + * @param message The message + * @param cause The cause + * @param inErrorCode The error code + */ + + public LException( + final String message, + final Throwable cause, + final String inErrorCode) + { + this(message, cause, inErrorCode, Map.of(), Optional.empty()); + } + + /** + * Construct an exception. + * + * @param message The message + * @param cause The cause + * @param inErrorCode The error code + * @param inAttributes The attributes + */ + + public LException( + final String message, + final Throwable cause, + final String inErrorCode, + final Map inAttributes) + { + this(message, cause, inErrorCode, inAttributes, Optional.empty()); + } + + /** + * Construct an exception. + * + * @param message The message + * @param cause The cause + * @param inErrorCode The error code + * @param inRemediatingAction The remediating action + */ + + public LException( + final String message, + final Throwable cause, + final String inErrorCode, + final Optional inRemediatingAction) + { + this(message, cause, inErrorCode, Map.of(), inRemediatingAction); + } + + /** + * Construct an exception. + * + * @param cause The cause + * @param inErrorCode The error code + * @param inRemediatingAction The remediating action + */ + + public LException( + final Throwable cause, + final String inErrorCode, + final Optional inRemediatingAction) + { + this(cause, inErrorCode, Map.of(), inRemediatingAction); + } + + /** + * Construct an exception. + * + * @param message The message + * @param inErrorCode The error code + * @param inRemediatingAction The remediating action + */ + + public LException( + final String message, + final String inErrorCode, + final Optional inRemediatingAction) + { + this(message, inErrorCode, Map.of(), inRemediatingAction); + } + + /** + * Construct an exception. + * + * @param message The message + * @param inErrorCode The error code + * @param inAttributes The attributes + */ + + public LException( + final String message, + final String inErrorCode, + final Map inAttributes) + { + this(message, inErrorCode, inAttributes, Optional.empty()); + } + + @Override + public final String errorCode() + { + return this.errorCode; + } + + @Override + public final Map attributes() + { + return this.attributes; + } + + @Override + public final Optional remediatingAction() + { + return this.remediatingAction; + } + + @Override + public final Optional exception() + { + return Optional.of(this); + } +} + diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashSHA256.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashSHA256.java new file mode 100644 index 0000000..053a8fd --- /dev/null +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashSHA256.java @@ -0,0 +1,55 @@ +/* + * 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.model; + +import java.util.regex.Pattern; + +/** + * A SHA-256 hash value. + * + * @param value The value + */ + +public record LHashSHA256( + String value) + implements LHashType +{ + private static final Pattern VALID_HASH = + Pattern.compile("[a-f0-9]{64}"); + + /** + * A SHA-256 hash value. + * + * @param value The value + */ + + public LHashSHA256 + { + if (!VALID_HASH.matcher(value).matches()) { + throw new IllegalArgumentException( + "Hash values must match %s".formatted(VALID_HASH) + ); + } + } + + @Override + public String name() + { + return "SHA-256"; + } +} diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashType.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashType.java new file mode 100644 index 0000000..e80225d --- /dev/null +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LHashType.java @@ -0,0 +1,40 @@ +/* + * 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.model; + +/** + * A hash value. + */ + +public sealed interface LHashType + permits LHashSHA256 +{ + /** + * @return The lowercase hex hash value + */ + + String value(); + + /** + * @return The hash algorithm name + * + * @see "https://docs.oracle.com/en/java/javase/21/docs/specs/security/standard-names.html#messagedigest-algorithms" + */ + + String name(); +} diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImage.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImage.java index 7c3d756..e6353b8 100644 --- a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImage.java +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImage.java @@ -17,47 +17,40 @@ package com.io7m.laurel.model; -import java.util.Collections; -import java.util.Comparator; +import java.net.URI; +import java.nio.file.Path; import java.util.Objects; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.Optional; /** * An image. * - * @param imageID The image ID - * @param fileName The file name - * @param captions The captions + * @param name The name + * @param file The file + * @param source The source + * @param hash The hash */ public record LImage( - LImageID imageID, - String fileName, - SortedSet captions) - implements Comparable + String name, + Optional file, + Optional source, + LHashType hash) { /** * An image. * - * @param imageID The image ID - * @param fileName The file name - * @param captions The captions + * @param name The name + * @param file The file + * @param source The source + * @param hash The hash */ public LImage { - Objects.requireNonNull(imageID, "imageID"); - Objects.requireNonNull(fileName, "fileName"); - captions = Collections.unmodifiableSortedSet(new TreeSet<>(captions)); - } - - @Override - public int compareTo( - final LImage other) - { - return Comparator.comparing(LImage::fileName) - .thenComparing(LImage::imageID) - .compare(this, other); + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(file, "file"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(hash, "hash"); } } diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImageSet.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImageSet.java index 1a9fed5..1318daa 100644 --- a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImageSet.java +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LImageSet.java @@ -39,7 +39,7 @@ public record LImageSet( List globalPrefixCaptions, SortedMap captions, - SortedMap images) + SortedMap images) { /** * An image set. diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LOldImage.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LOldImage.java new file mode 100644 index 0000000..af3d829 --- /dev/null +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LOldImage.java @@ -0,0 +1,63 @@ +/* + * 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.model; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Objects; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * An image. + * + * @param imageID The image ID + * @param fileName The file name + * @param captions The captions + */ + +public record LOldImage( + LImageID imageID, + String fileName, + SortedSet captions) + implements Comparable +{ + /** + * An image. + * + * @param imageID The image ID + * @param fileName The file name + * @param captions The captions + */ + + public LOldImage + { + Objects.requireNonNull(imageID, "imageID"); + Objects.requireNonNull(fileName, "fileName"); + captions = Collections.unmodifiableSortedSet(new TreeSet<>(captions)); + } + + @Override + public int compareTo( + final LOldImage other) + { + return Comparator.comparing(LOldImage::fileName) + .thenComparing(LOldImage::imageID) + .compare(this, other); + } +} diff --git a/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LTag.java b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LTag.java new file mode 100644 index 0000000..4b15c75 --- /dev/null +++ b/com.io7m.laurel.model/src/main/java/com/io7m/laurel/model/LTag.java @@ -0,0 +1,66 @@ +/* + * 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.model; + + +import java.util.Comparator; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A caption. + * + * @param text The caption text + */ + +public record LTag(String text) + implements Comparable +{ + /** + * The pattern that defines a valid caption. + */ + + public static final Pattern VALID_CAPTION = + Pattern.compile("[a-z0-9A-Z_-][a-z0-9A-Z_ \\-']*"); + + /** + * A caption. + * + * @param text The caption text + */ + + public LTag + { + Objects.requireNonNull(text, "text"); + text = text.trim(); + + if (!VALID_CAPTION.matcher(text).matches()) { + throw new IllegalArgumentException( + "Caption must match %s".formatted(VALID_CAPTION) + ); + } + } + + @Override + public int compareTo( + final LTag other) + { + return Comparator.comparing(LTag::text) + .compare(this, other); + } +} diff --git a/com.io7m.laurel.model/src/main/java/module-info.java b/com.io7m.laurel.model/src/main/java/module-info.java index 106719a..36592b6 100644 --- a/com.io7m.laurel.model/src/main/java/module-info.java +++ b/com.io7m.laurel.model/src/main/java/module-info.java @@ -23,6 +23,7 @@ requires static org.osgi.annotation.bundle; requires static org.osgi.annotation.versioning; + requires com.io7m.seltzer.api; requires com.io7m.jaffirm.core; exports com.io7m.laurel.model; diff --git a/com.io7m.laurel.tests/pom.xml b/com.io7m.laurel.tests/pom.xml index 325a213..e5e6807 100644 --- a/com.io7m.laurel.tests/pom.xml +++ b/com.io7m.laurel.tests/pom.xml @@ -49,6 +49,11 @@ com.io7m.laurel.gui ${project.version} + + ${project.groupId} + com.io7m.laurel.filemodel + ${project.version} + commons-io @@ -83,6 +88,7 @@ org.slf4j slf4j-api + org.junit.jupiter junit-jupiter-api @@ -91,6 +97,23 @@ org.junit.jupiter junit-jupiter-engine + + org.junit.platform + junit-platform-commons + + + org.junit.platform + junit-platform-engine + + + org.junit.platform + junit-platform-launcher + + + + com.io7m.zelador + com.io7m.zelador.test_extension + org.osgi diff --git a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java new file mode 100644 index 0000000..6599825 --- /dev/null +++ b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java @@ -0,0 +1,345 @@ +/* + * 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.filemodel.LFileModelType; +import com.io7m.laurel.filemodel.LFileModels; +import com.io7m.laurel.model.LException; +import com.io7m.laurel.model.LTag; +import com.io7m.zelador.test_extension.CloseableResourcesType; +import com.io7m.zelador.test_extension.ZeladorExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +@ExtendWith({ZeladorExtension.class}) +public final class LFileModelTest +{ + private static final Logger LOG = + LoggerFactory.getLogger(LFileModelTest.class); + + private static final long TIMEOUT = 10L; + + private static final OpenOption[] OPEN_OPTIONS = { + StandardOpenOption.WRITE, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + }; + + private Path file; + private LFileModelType model; + private Path imageFile; + private Path textFile; + + @BeforeEach + public void setup( + final @TempDir Path directory, + final CloseableResourcesType resources) + throws Exception + { + this.file = + directory.resolve("file.lau"); + this.imageFile = + directory.resolve("image.png"); + this.textFile = + directory.resolve("file.txt"); + + Files.writeString(this.textFile, "Not an image."); + + this.model = + LFileModels.open(this.file, false); + + resources.addPerTestResource( + this.model.undoText() + .subscribe((oldValue, newValue) -> LOG.debug("Undo: {}", newValue)) + ); + + resources.addPerTestResource( + this.model.redoText() + .subscribe((oldValue, newValue) -> LOG.debug("Redo: {}", newValue)) + ); + + try (var stream = LFileModelTest.class.getResourceAsStream( + "/com/io7m/laurel/tests/001.png")) { + Files.write(this.imageFile, stream.readAllBytes(), OPEN_OPTIONS); + } + } + + @Test + public void testImageSelectNonexistent() + throws Exception + { + assertEquals(Optional.empty(), this.model.imageSelected().get()); + this.model.imageSelect(Optional.of("nonexistent")).get(TIMEOUT, SECONDS); + assertEquals(Optional.empty(), this.model.imageSelected().get()); + } + + @Test + public void testImageSelect() + throws Exception + { + this.model.imageAdd( + "image-a", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + assertEquals(List.of(), this.model.tagsAssigned().get()); + assertEquals(Optional.empty(), this.model.imageSelected().get()); + this.model.imageSelect(Optional.of("image-a")).get(TIMEOUT, SECONDS); + + final var image = this.model.imageSelected().get().get(); + assertEquals("image-a", image.name()); + assertEquals(List.of(), this.model.tagsAssigned().get()); + + this.model.imageSelect(Optional.empty()).get(TIMEOUT, SECONDS); + assertEquals(Optional.empty(), this.model.imageSelected().get()); + assertEquals(List.of(), this.model.tagsAssigned().get()); + } + + @Test + public void testImageAdd() + throws Exception + { + assertEquals(Optional.empty(), this.model.undoText().get()); + assertEquals(0, this.model.imageList().get().size()); + + this.model.imageAdd( + "image-a", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + assertEquals( + Optional.of("Add image 'image-a'"), + this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + assertEquals(1, this.model.imageList().get().size()); + + this.model.imageAdd( + "image-b", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + assertEquals( + Optional.of("Add image 'image-b'"), + this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + assertEquals(2, this.model.imageList().get().size()); + + this.model.imageAdd( + "image-c", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + assertEquals( + Optional.of("Add image 'image-c'"), + this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + assertEquals(3, this.model.imageList().get().size()); + + /* + * Now undo the operations. + */ + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals( + Optional.of("Add image 'image-b'"), + this.model.undoText().get()); + assertEquals( + Optional.of("Add image 'image-c'"), + this.model.redoText().get()); + assertEquals(2, this.model.imageList().get().size()); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals( + Optional.of("Add image 'image-a'"), + this.model.undoText().get()); + assertEquals( + Optional.of("Add image 'image-b'"), + this.model.redoText().get()); + assertEquals(1, this.model.imageList().get().size()); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(Optional.empty(), this.model.undoText().get()); + assertEquals( + Optional.of("Add image 'image-a'"), + this.model.redoText().get()); + assertEquals(0, this.model.imageList().get().size()); + + /* + * Now redo the operations. + */ + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals( + Optional.of("Add image 'image-a'"), + this.model.undoText().get()); + assertEquals( + Optional.of("Add image 'image-b'"), + this.model.redoText().get()); + assertEquals(1, this.model.imageList().get().size()); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals( + Optional.of("Add image 'image-b'"), + this.model.undoText().get()); + assertEquals( + Optional.of("Add image 'image-c'"), + this.model.redoText().get()); + assertEquals(2, this.model.imageList().get().size()); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals( + Optional.of("Add image 'image-c'"), + this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + assertEquals(3, this.model.imageList().get().size()); + } + + @Test + public void testImageAddImageNonexistent() + throws Exception + { + assertEquals(Optional.empty(), this.model.undoText().get()); + + final var ex = + Assertions.assertThrows(ExecutionException.class, () -> { + this.model.imageAdd( + "image-a", + this.file.getParent().resolve("nonexistent.txt"), + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + }); + + final var ee = assertInstanceOf(LException.class, ex.getCause()); + assertEquals("error-io", ee.errorCode()); + + assertEquals(Optional.empty(), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + } + + @Test + public void testImageAddImageCorrupt() + throws Exception + { + assertEquals(Optional.empty(), this.model.undoText().get()); + + final var ex = + Assertions.assertThrows(ExecutionException.class, () -> { + this.model.imageAdd( + "image-a", + this.textFile, + Optional.of(this.textFile.toUri()) + ).get(TIMEOUT, SECONDS); + }); + + final var ee = assertInstanceOf(LException.class, ex.getCause()); + assertEquals("error-image-format", ee.errorCode()); + + assertEquals(Optional.empty(), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + } + + @Test + public void testTagAdd() + throws Exception + { + final var ta = new LTag("A"); + final var tb = new LTag("B"); + final var tc = new LTag("C"); + + assertEquals(Optional.empty(), this.model.undoText().get()); + + this.model.tagAdd(ta).get(TIMEOUT, SECONDS); + assertEquals(List.of(ta), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'A'"), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + + this.model.tagAdd(tb).get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'B'"), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + + this.model.tagAdd(tc).get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb, tc), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'C'"), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + + this.model.tagAdd(ta).get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb, tc), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'C'"), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + + /* + * Now undo the operations. + */ + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'B'"), this.model.undoText().get()); + assertEquals(Optional.of("Add tag 'C'"), this.model.redoText().get()); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ta), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'A'"), this.model.undoText().get()); + assertEquals(Optional.of("Add tag 'B'"), this.model.redoText().get()); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(), this.model.tagList().get()); + assertEquals(Optional.empty(), this.model.undoText().get()); + assertEquals(Optional.of("Add tag 'A'"), this.model.redoText().get()); + + /* + * Now redo the operations. + */ + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ta), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'A'"), this.model.undoText().get()); + assertEquals(Optional.of("Add tag 'B'"), this.model.redoText().get()); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'B'"), this.model.undoText().get()); + assertEquals(Optional.of("Add tag 'C'"), this.model.redoText().get()); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ta, tb, tc), this.model.tagList().get()); + assertEquals(Optional.of("Add tag 'C'"), this.model.undoText().get()); + assertEquals(Optional.empty(), this.model.redoText().get()); + } +} diff --git a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/arbitraries/LArbImageSet.java b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/arbitraries/LArbImageSet.java index 0c29d98..c8050de 100644 --- a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/arbitraries/LArbImageSet.java +++ b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/arbitraries/LArbImageSet.java @@ -17,7 +17,7 @@ package com.io7m.laurel.tests.arbitraries; -import com.io7m.laurel.model.LImage; +import com.io7m.laurel.model.LOldImage; import com.io7m.laurel.model.LImageCaption; import com.io7m.laurel.model.LImageCaptionID; import com.io7m.laurel.model.LImageID; @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -51,7 +50,7 @@ public LArbImageSet() .ofMaxSize(10) ).as((id, captions, images) -> { - final var imagesConstructed = new ArrayList(); + final var imagesConstructed = new ArrayList(); for (final var imageId : images) { final var imageCaps = new TreeSet(); for (final var caption : captions) { @@ -59,7 +58,7 @@ public LArbImageSet() imageCaps.add(caption.id()); } } - imagesConstructed.add(new LImage(imageId, imageId + ".png", imageCaps)); + imagesConstructed.add(new LOldImage(imageId, imageId + ".png", imageCaps)); } return new LImageSet( @@ -79,8 +78,8 @@ private static Map captionMap( .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private static Map imageMap( - final Collection images) + private static Map imageMap( + final Collection images) { return images.stream() .map(x -> Map.entry(x.imageID(), x)) 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 0c293f5..de2673f 100644 --- a/com.io7m.laurel.tests/src/main/java/module-info.java +++ b/com.io7m.laurel.tests/src/main/java/module-info.java @@ -27,20 +27,23 @@ requires com.io7m.laurel.reader.api; requires com.io7m.laurel.writer.api; requires com.io7m.laurel.gui; + requires com.io7m.laurel.filemodel; requires com.io7m.anethum.api; requires com.io7m.jattribute.core; requires com.io7m.jmulticlose.core; + requires com.io7m.zelador.test_extension; requires javafx.base; requires javafx.controls; requires net.jqwik.api; requires org.apache.commons.io; requires org.slf4j; - requires transitive org.junit.jupiter.api; - requires transitive org.junit.jupiter.engine; - requires transitive org.junit.platform.commons; - requires transitive org.junit.platform.engine; + requires org.junit.jupiter.api; + requires org.junit.jupiter.engine; + requires org.junit.platform.commons; + requires org.junit.platform.engine; + requires org.junit.platform.launcher; uses ArbitraryProvider; diff --git a/com.io7m.laurel.tests/src/main/resources/logback-test.xml b/com.io7m.laurel.tests/src/main/resources/logback-test.xml new file mode 100644 index 0000000..6b9ec35 --- /dev/null +++ b/com.io7m.laurel.tests/src/main/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + + + + %level %logger: %msg%n + + System.out + + + + + + + + + + + diff --git a/com.io7m.laurel.tests/src/main/resources/logback.xsd b/com.io7m.laurel.tests/src/main/resources/logback.xsd new file mode 100644 index 0000000..16db5d6 --- /dev/null +++ b/com.io7m.laurel.tests/src/main/resources/logback.xsd @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 24e8313..ef178a4 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ https://www.io7m.com/software/laurel + com.io7m.laurel.filemodel com.io7m.laurel.gui.main com.io7m.laurel.gui com.io7m.laurel.io @@ -31,13 +32,24 @@ + + 0.0.2-SNAPSHOT + 21 + + + 1.0.0-beta0001 2.1.0 2.1.0 + 1.1.0 1.8.0 - 0.0.2-SNAPSHOT - 21 + 1.0.0 + + + 1.41.0 21.0.4 - 5.11.0 + 3.19.11 + 5.11.0 + 3.46.0.1 @@ -275,15 +287,19 @@ logback-classic 1.5.7 + - org.junit.jupiter - junit-jupiter-api - ${junit.version} + org.junit + junit-bom + ${org.junit.version} + pom + import + - org.junit.jupiter - junit-jupiter-engine - ${junit.version} + com.io7m.zelador + com.io7m.zelador.test_extension + ${com.io7m.zelador.version} org.osgi @@ -315,6 +331,35 @@ jqwik-engine 1.9.0 + + com.io7m.darco + com.io7m.darco.sqlite + ${com.io7m.darco.version} + + + com.io7m.darco + com.io7m.darco.api + ${com.io7m.darco.version} + + + org.jooq + jooq + ${org.jooq.version} + + + org.xerial + sqlite-jdbc + ${org.xerial.sqlite.version} + + + + + io.opentelemetry + opentelemetry-bom + ${io.opentelemetry.version} + pom + import +