From ca16bb0d00ac8b5e25857aa82e552f5b32e73e74 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Tue, 10 Sep 2024 18:36:00 +0000 Subject: [PATCH] Add tag assignments. Centralize model updates. --- .../io7m/laurel/filemodel/LFileModelType.java | 30 ++ .../internal/LCommandCategoriesAdd.java | 20 +- .../LCommandCategoriesSetRequired.java | 21 +- .../LCommandCategoriesUnsetRequired.java | 23 +- .../internal/LCommandCategoryTagsAssign.java | 271 +++++++++++++++++ .../LCommandCategoryTagsUnassign.java | 274 ++++++++++++++++++ .../internal/LCommandModelUpdates.java | 123 ++++++++ .../filemodel/internal/LCommandTagsAdd.java | 20 +- .../laurel/filemodel/internal/LFileModel.java | 61 +++- .../src/main/java/module-info.java | 12 +- .../com/io7m/laurel/tests/LFileModelTest.java | 83 ++++++ 11 files changed, 846 insertions(+), 92 deletions(-) create mode 100644 com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsAssign.java create mode 100644 com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsUnassign.java create mode 100644 com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandModelUpdates.java 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 index 838200f..9407e5f 100644 --- 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 @@ -18,6 +18,7 @@ package com.io7m.laurel.filemodel; import com.io7m.jattribute.core.AttributeReadableType; +import com.io7m.laurel.filemodel.internal.LCategoryAndTags; import com.io7m.laurel.model.LCategory; import com.io7m.laurel.model.LException; import com.io7m.laurel.model.LImage; @@ -28,6 +29,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.SortedMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; @@ -115,6 +117,28 @@ CompletableFuture imageAdd( CompletableFuture imageSelect( Optional name); + /** + * Assign the given tags to the given categories. + * + * @param categories The categories/tags + * + * @return The operation in progress + */ + + CompletableFuture categoryTagsAssign( + List categories); + + /** + * Unassign the given tags from the given categories. + * + * @param categories The categories/tags + * + * @return The operation in progress + */ + + CompletableFuture categoryTagsUnassign( + List categories); + @Override void close() throws LException; @@ -191,4 +215,10 @@ void close() */ AttributeReadableType> categoryList(); + + /** + * @return The tags for every available category + */ + + AttributeReadableType>> categoryTags(); } diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesAdd.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesAdd.java index 4a39f71..0564dda 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesAdd.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesAdd.java @@ -93,20 +93,6 @@ private static LCommandCategoriesAdd fromProperties( return c; } - private static List listCategories( - final LDatabaseTransactionType transaction) - { - final var context = - transaction.get(DSLContext.class); - - return context.select(CATEGORIES.CATEGORY_TEXT) - .from(CATEGORIES) - .orderBy(CATEGORIES.CATEGORY_TEXT.asc()) - .stream() - .map(r -> new LCategory(r.get(CATEGORIES.CATEGORY_TEXT))) - .toList(); - } - @Override protected LCommandUndoable onExecute( final LFileModel model, @@ -148,7 +134,7 @@ protected LCommandUndoable onExecute( ); } - model.setCategoriesAll(listCategories(transaction)); + LCommandModelUpdates.updateTagsAndCategories(context, model); model.eventWithoutProgress("Added %d categories.", this.savedData.size()); if (!this.savedData.isEmpty()) { @@ -180,7 +166,7 @@ protected void onUndo( .execute(); } - model.setCategoriesAll(listCategories(transaction)); + LCommandModelUpdates.updateTagsAndCategories(context, model); model.eventWithoutProgress("Removed %d categories.", Integer.valueOf(max)); } @@ -210,7 +196,7 @@ protected void onRedo( .execute(); } - model.setCategoriesAll(listCategories(transaction)); + LCommandModelUpdates.updateTagsAndCategories(context, model); model.eventWithoutProgress("Added %d categories.", Integer.valueOf(max)); } diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesSetRequired.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesSetRequired.java index 4b1aade..652de96 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesSetRequired.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesSetRequired.java @@ -139,7 +139,7 @@ protected LCommandUndoable onExecute( } model.eventWithoutProgress("Updated %d categories.", this.savedData.size()); - model.setCategoriesRequired(listRequired(transaction)); + LCommandModelUpdates.updateTagsAndCategories(context, model); if (!this.savedData.isEmpty()) { return LCommandUndoable.COMMAND_UNDOABLE; @@ -172,7 +172,7 @@ protected void onUndo( } model.eventWithoutProgress("Updated %d categories.", Integer.valueOf(max)); - model.setCategoriesRequired(listRequired(transaction)); + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override @@ -199,22 +199,7 @@ protected void onRedo( } model.eventWithoutProgress("Updated %d categories.", Integer.valueOf(max)); - model.setCategoriesRequired(listRequired(transaction)); - } - - private static List listRequired( - final LDatabaseTransactionType transaction) - { - final var context = - transaction.get(DSLContext.class); - - return context.select(CATEGORIES.CATEGORY_TEXT) - .from(CATEGORIES) - .where(CATEGORIES.CATEGORY_REQUIRED.eq(1L)) - .orderBy(CATEGORIES.CATEGORY_TEXT.asc()) - .stream() - .map(r -> new LCategory(r.get(CATEGORIES.CATEGORY_TEXT))) - .toList(); + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesUnsetRequired.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesUnsetRequired.java index 43b7c3a..845ffa4 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesUnsetRequired.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoriesUnsetRequired.java @@ -139,8 +139,7 @@ protected LCommandUndoable onExecute( } model.eventWithoutProgress("Updated %d categories.", this.savedData.size()); - model.setCategoriesRequired(listRequired(transaction)); - + LCommandModelUpdates.updateTagsAndCategories(context, model); if (!this.savedData.isEmpty()) { return LCommandUndoable.COMMAND_UNDOABLE; @@ -173,8 +172,7 @@ protected void onUndo( } model.eventWithoutProgress("Updated %d categories.", Integer.valueOf(max)); - model.setCategoriesRequired(listRequired(transaction)); - + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override @@ -201,22 +199,7 @@ protected void onRedo( } model.eventWithoutProgress("Updated %d categories.", Integer.valueOf(max)); - model.setCategoriesRequired(listRequired(transaction)); - } - - private static List listRequired( - final LDatabaseTransactionType transaction) - { - final var context = - transaction.get(DSLContext.class); - - return context.select(CATEGORIES.CATEGORY_TEXT) - .from(CATEGORIES) - .where(CATEGORIES.CATEGORY_REQUIRED.eq(1L)) - .orderBy(CATEGORIES.CATEGORY_TEXT.asc()) - .stream() - .map(r -> new LCategory(r.get(CATEGORIES.CATEGORY_TEXT))) - .toList(); + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsAssign.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsAssign.java new file mode 100644 index 0000000..8346237 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsAssign.java @@ -0,0 +1,271 @@ +/* + * 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 org.jooq.DSLContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.CATEGORIES; +import static com.io7m.laurel.filemodel.internal.Tables.TAGS; +import static com.io7m.laurel.filemodel.internal.Tables.TAG_CATEGORIES; + +/** + * Assign tags to categories. + */ + +public final class LCommandCategoryTagsAssign + extends LCommandAbstract> +{ + private final ArrayList savedData; + + private record SavedData( + long categoryId, + long tagId) + { + + } + + /** + * Assign tags to categories. + */ + + public LCommandCategoryTagsAssign() + { + this.savedData = new ArrayList<>(); + } + + /** + * Assign tags to categories. + * + * @return A command factory + */ + + public static LCommandFactoryType> provider() + { + return new LCommandFactory<>( + LCommandCategoryTagsAssign.class.getCanonicalName(), + LCommandCategoryTagsAssign::fromProperties + ); + } + + private static LCommandCategoryTagsAssign fromProperties( + final Properties p) + { + final var c = new LCommandCategoryTagsAssign(); + + for (int index = 0; index < Integer.MAX_VALUE; ++index) { + final var categoryIdKey = + "category.%d.id".formatted(Integer.valueOf(index)); + final var categoryTagKey = + "category.%d.tag".formatted(Integer.valueOf(index)); + + if (!p.containsKey(categoryIdKey)) { + break; + } + + final var data = + new SavedData( + Long.parseUnsignedLong(p.getProperty(categoryIdKey)), + Long.parseUnsignedLong(p.getProperty(categoryTagKey)) + ); + + c.savedData.add(data); + } + + c.setExecuted(true); + return c; + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final List categories) + { + final var context = + transaction.get(DSLContext.class); + + final var max = + categories.stream() + .mapToInt(c -> c.tags().size()) + .sum(); + + final var entries = + categories.stream() + .flatMap(c -> { + return c.tags() + .stream() + .map(t -> Map.entry(c.category(), t)); + }) + .toList(); + + int index = 0; + for (final var entry : entries) { + final var category = + entry.getKey(); + final var tag = + entry.getValue(); + + model.eventWithProgressCurrentMax( + index, + max, + "Assigning tag to category '%s'.", + category + ); + + final var categoryId = + context.select(CATEGORIES.CATEGORY_ID) + .from(CATEGORIES) + .where(CATEGORIES.CATEGORY_TEXT.eq(category.text())); + + final var tagId = + context.select(TAGS.TAG_ID) + .from(TAGS) + .where(TAGS.TAG_TEXT.eq(tag.text())); + + final var recOpt = + context.insertInto(TAG_CATEGORIES) + .set(TAG_CATEGORIES.TAG_CATEGORY_ID, categoryId) + .set(TAG_CATEGORIES.TAG_TAG_ID, tagId) + .onConflictDoNothing() + .returning( + TAG_CATEGORIES.TAG_CATEGORY_ID, + TAG_CATEGORIES.TAG_TAG_ID) + .fetchOptional(); + + ++index; + + if (recOpt.isEmpty()) { + model.eventWithProgressCurrentMax( + index, + max, + "Category/tag either did not exist or was already assigned.", + category + ); + continue; + } + + final var rec = recOpt.get(); + this.savedData.add( + new SavedData( + rec.get(TAG_CATEGORIES.TAG_CATEGORY_ID).longValue(), + rec.get(TAG_CATEGORIES.TAG_TAG_ID).longValue() + ) + ); + } + + model.eventWithoutProgress("Assigned %d tags.", this.savedData.size()); + LCommandModelUpdates.updateTagsAndCategories(context, model); + + if (!this.savedData.isEmpty()) { + return LCommandUndoable.COMMAND_UNDOABLE; + } + + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + final var max = this.savedData.size(); + for (int index = 0; index < max; ++index) { + final var data = this.savedData.get(index); + + model.eventWithProgressCurrentMax( + index, + max, + "Unassigning tag from category." + ); + + final var matches = + TAG_CATEGORIES.TAG_CATEGORY_ID.eq(data.categoryId()) + .and(TAG_CATEGORIES.TAG_TAG_ID.eq(data.tagId())); + + context.deleteFrom(TAG_CATEGORIES) + .where(matches) + .execute(); + } + + model.eventWithoutProgress("Unassigned %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + final var max = this.savedData.size(); + for (int index = 0; index < max; ++index) { + final var data = this.savedData.get(index); + + model.eventWithProgressCurrentMax( + index, + max, + "Reassigning tag to category." + ); + + context.insertInto(TAG_CATEGORIES) + .set(TAG_CATEGORIES.TAG_CATEGORY_ID, data.categoryId) + .set(TAG_CATEGORIES.TAG_TAG_ID, data.tagId) + .onConflictDoNothing() + .execute(); + } + + model.eventWithoutProgress("Assigned %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); + } + + @Override + public Properties toProperties() + { + final var p = new Properties(); + + for (int index = 0; index < this.savedData.size(); ++index) { + final var categoryIdKey = + "category.%d.id".formatted(Integer.valueOf(index)); + final var categoryTagKey = + "category.%d.tag".formatted(Integer.valueOf(index)); + + final var data = this.savedData.get(index); + p.setProperty(categoryIdKey, Long.toUnsignedString(data.categoryId)); + p.setProperty(categoryTagKey, Long.toUnsignedString(data.tagId)); + } + + return p; + } + + @Override + public String describe() + { + return "Assign tags to categories"; + } + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsUnassign.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsUnassign.java new file mode 100644 index 0000000..7208dc4 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandCategoryTagsUnassign.java @@ -0,0 +1,274 @@ +/* + * 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 org.jooq.DSLContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.CATEGORIES; +import static com.io7m.laurel.filemodel.internal.Tables.TAGS; +import static com.io7m.laurel.filemodel.internal.Tables.TAG_CATEGORIES; + +/** + * Unassign tags from categories. + */ + +public final class LCommandCategoryTagsUnassign + extends LCommandAbstract> +{ + private final ArrayList savedData; + + private record SavedData( + long categoryId, + long tagId) + { + + } + + /** + * Unassign tags from categories. + */ + + public LCommandCategoryTagsUnassign() + { + this.savedData = new ArrayList<>(); + } + + /** + * Unassign tags from categories. + * + * @return A command factory + */ + + public static LCommandFactoryType> provider() + { + return new LCommandFactory<>( + LCommandCategoryTagsUnassign.class.getCanonicalName(), + LCommandCategoryTagsUnassign::fromProperties + ); + } + + private static LCommandCategoryTagsUnassign fromProperties( + final Properties p) + { + final var c = new LCommandCategoryTagsUnassign(); + + for (int index = 0; index < Integer.MAX_VALUE; ++index) { + final var categoryIdKey = + "category.%d.id".formatted(Integer.valueOf(index)); + final var categoryTagKey = + "category.%d.tag".formatted(Integer.valueOf(index)); + + if (!p.containsKey(categoryIdKey)) { + break; + } + + final var data = + new SavedData( + Long.parseUnsignedLong(p.getProperty(categoryIdKey)), + Long.parseUnsignedLong(p.getProperty(categoryTagKey)) + ); + + c.savedData.add(data); + } + + c.setExecuted(true); + return c; + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final List categories) + { + final var context = + transaction.get(DSLContext.class); + + final var max = + categories.stream() + .mapToInt(c -> c.tags().size()) + .sum(); + + final var entries = + categories.stream() + .flatMap(c -> { + return c.tags() + .stream() + .map(t -> Map.entry(c.category(), t)); + }) + .toList(); + + int index = 0; + for (final var entry : entries) { + final var category = + entry.getKey(); + final var tag = + entry.getValue(); + + model.eventWithProgressCurrentMax( + index, + max, + "Unassigning tag '%s' from category '%s'.", + tag, + category + ); + + final var categoryId = + context.select(CATEGORIES.CATEGORY_ID) + .from(CATEGORIES) + .where(CATEGORIES.CATEGORY_TEXT.eq(category.text())); + + final var tagId = + context.select(TAGS.TAG_ID) + .from(TAGS) + .where(TAGS.TAG_TEXT.eq(tag.text())); + + final var matches = + TAG_CATEGORIES.TAG_CATEGORY_ID.eq(categoryId) + .and(TAG_CATEGORIES.TAG_TAG_ID.eq(tagId)); + + final var recOpt = + context.deleteFrom(TAG_CATEGORIES) + .where(matches) + .returning( + TAG_CATEGORIES.TAG_CATEGORY_ID, + TAG_CATEGORIES.TAG_TAG_ID) + .fetchOptional(); + + ++index; + + if (recOpt.isEmpty()) { + model.eventWithProgressCurrentMax( + index, + max, + "Category/tag either did not exist or was not assigned.", + category + ); + continue; + } + + final var rec = recOpt.get(); + this.savedData.add( + new SavedData( + rec.get(TAG_CATEGORIES.TAG_CATEGORY_ID).longValue(), + rec.get(TAG_CATEGORIES.TAG_TAG_ID).longValue() + ) + ); + } + + model.eventWithoutProgress("Unassigned %d tags.", this.savedData.size()); + LCommandModelUpdates.updateTagsAndCategories(context, model); + + if (!this.savedData.isEmpty()) { + return LCommandUndoable.COMMAND_UNDOABLE; + } + + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + final var max = this.savedData.size(); + for (int index = 0; index < max; ++index) { + final var data = this.savedData.get(index); + + model.eventWithProgressCurrentMax( + index, + max, + "Reassigning tag to category." + ); + + context.insertInto(TAG_CATEGORIES) + .set(TAG_CATEGORIES.TAG_CATEGORY_ID, data.categoryId()) + .set(TAG_CATEGORIES.TAG_TAG_ID, data.tagId()) + .onConflictDoNothing() + .execute(); + } + + model.eventWithoutProgress("Reassigned %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + final var max = this.savedData.size(); + for (int index = 0; index < max; ++index) { + final var data = this.savedData.get(index); + + model.eventWithProgressCurrentMax( + index, + max, + "Unassigning tag from category." + ); + + final var matches = + TAG_CATEGORIES.TAG_CATEGORY_ID.eq(data.categoryId()) + .and(TAG_CATEGORIES.TAG_TAG_ID.eq(data.tagId())); + + context.deleteFrom(TAG_CATEGORIES) + .where(matches) + .execute(); + } + + model.eventWithoutProgress("Unassigned %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); + } + + @Override + public Properties toProperties() + { + final var p = new Properties(); + + for (int index = 0; index < this.savedData.size(); ++index) { + final var categoryIdKey = + "category.%d.id".formatted(Integer.valueOf(index)); + final var categoryTagKey = + "category.%d.tag".formatted(Integer.valueOf(index)); + + final var data = this.savedData.get(index); + p.setProperty(categoryIdKey, Long.toUnsignedString(data.categoryId)); + p.setProperty(categoryTagKey, Long.toUnsignedString(data.tagId)); + } + + return p; + } + + @Override + public String describe() + { + return "Assign tags to categories"; + } + +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandModelUpdates.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandModelUpdates.java new file mode 100644 index 0000000..aba1ee8 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandModelUpdates.java @@ -0,0 +1,123 @@ +/* + * 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.LCategory; +import com.io7m.laurel.model.LTag; +import org.jooq.DSLContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import static com.io7m.laurel.filemodel.internal.Tables.CATEGORIES; +import static com.io7m.laurel.filemodel.internal.Tables.TAGS; +import static com.io7m.laurel.filemodel.internal.Tables.TAG_CATEGORIES; + +/** + * Functions to perform model updates on database changes. + */ + +public final class LCommandModelUpdates +{ + private LCommandModelUpdates() + { + + } + + private static List listTags( + final DSLContext context) + { + return context.select(TAGS.TAG_TEXT) + .from(TAGS) + .orderBy(TAGS.TAG_TEXT.asc()) + .stream() + .map(r -> new LTag(r.get(TAGS.TAG_TEXT))) + .toList(); + } + + private static SortedMap> listCategoriesTags( + final DSLContext context) + { + final var map = + new TreeMap>(); + + final var results = + context.select(CATEGORIES.CATEGORY_TEXT, TAGS.TAG_TEXT) + .from(TAG_CATEGORIES) + .join(TAGS) + .on(TAG_CATEGORIES.TAG_TAG_ID.eq(TAGS.TAG_ID)) + .join(CATEGORIES) + .on(TAG_CATEGORIES.TAG_CATEGORY_ID.eq(CATEGORIES.CATEGORY_ID)) + .orderBy(CATEGORIES.CATEGORY_TEXT, TAGS.TAG_TEXT) + .fetch(); + + for (final var rec : results) { + final var category = + new LCategory(rec.get(CATEGORIES.CATEGORY_TEXT)); + final var tag = + new LTag(rec.get(TAGS.TAG_TEXT)); + + var existing = map.get(category); + if (existing == null) { + existing = new ArrayList<>(); + } + existing.add(tag); + map.put(category, existing); + } + + return Collections.unmodifiableSortedMap(map); + } + + private static List listCategoriesAll( + final DSLContext context) + { + return context.select(CATEGORIES.CATEGORY_TEXT) + .from(CATEGORIES) + .orderBy(CATEGORIES.CATEGORY_TEXT.asc()) + .stream() + .map(r -> new LCategory(r.get(CATEGORIES.CATEGORY_TEXT))) + .toList(); + } + + private static List listCategoriesRequired( + final DSLContext context) + { + return context.select(CATEGORIES.CATEGORY_TEXT) + .from(CATEGORIES) + .where(CATEGORIES.CATEGORY_REQUIRED.eq(1L)) + .orderBy(CATEGORIES.CATEGORY_TEXT.asc()) + .stream() + .map(r -> new LCategory(r.get(CATEGORIES.CATEGORY_TEXT))) + .toList(); + } + + static void updateTagsAndCategories( + final DSLContext context, + final LFileModel model) + { + model.setCategoriesAndTags( + listTags(context), + listCategoriesAll(context), + listCategoriesRequired(context), + listCategoriesTags(context) + ); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagsAdd.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagsAdd.java index 2cf3bd6..ed14416 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagsAdd.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandTagsAdd.java @@ -93,20 +93,6 @@ private static LCommandTagsAdd fromProperties( 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, @@ -147,8 +133,8 @@ protected LCommandUndoable onExecute( ); } - model.setTagsAll(listTags(transaction)); model.eventWithoutProgress("Added %d tags.", this.savedData.size()); + LCommandModelUpdates.updateTagsAndCategories(context, model); if (!this.savedData.isEmpty()) { return LCommandUndoable.COMMAND_UNDOABLE; @@ -179,8 +165,8 @@ protected void onUndo( .execute(); } - model.setTagsAll(listTags(transaction)); model.eventWithoutProgress("Removed %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override @@ -208,8 +194,8 @@ protected void onRedo( .execute(); } - model.setTagsAll(listTags(transaction)); model.eventWithoutProgress("Added %d tags.", Integer.valueOf(max)); + LCommandModelUpdates.updateTagsAndCategories(context, model); } @Override 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 index a1af2e3..a8daef9 100644 --- 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 @@ -44,6 +44,7 @@ import java.net.URI; import java.nio.file.Path; import java.time.OffsetDateTime; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -51,6 +52,7 @@ import java.util.OptionalDouble; import java.util.Properties; import java.util.Set; +import java.util.SortedMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.SubmissionPublisher; @@ -79,6 +81,7 @@ public final class LFileModel implements LFileModelType private final AttributeType> imagesAll; private final AttributeType> tagsAll; private final AttributeType> tagsAssigned; + private final AttributeType>> categoryTags; private final AttributeType>> redo; private final AttributeType>> undo; private final AttributeType> imageSelected; @@ -99,6 +102,8 @@ private LFileModel( ATTRIBUTES.create(List.of()); this.categoriesRequired = ATTRIBUTES.create(List.of()); + this.categoryTags = + ATTRIBUTES.create(Collections.emptySortedMap()); this.tagsAll = ATTRIBUTES.create(List.of()); this.tagsAssigned = @@ -393,6 +398,30 @@ public CompletableFuture imageSelect( ); } + @Override + public CompletableFuture categoryTagsAssign( + final List categories) + { + Objects.requireNonNull(categories, "categories"); + + return this.runCommand( + new LCommandCategoryTagsAssign(), + categories + ); + } + + @Override + public CompletableFuture categoryTagsUnassign( + final List categories) + { + Objects.requireNonNull(categories, "categories"); + + return this.runCommand( + new LCommandCategoryTagsUnassign(), + categories + ); + } + private > CompletableFuture runCommand( @@ -625,6 +654,12 @@ public AttributeReadableType> categoryList() return this.categoriesAll; } + @Override + public AttributeReadableType>> categoryTags() + { + return this.categoryTags; + } + private void executeRedo() throws Exception { @@ -672,12 +707,6 @@ void setImagesAll( 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); @@ -731,15 +760,15 @@ void eventWithProgressCurrentMax( )); } - void setCategoriesAll( - final List categories) - { - this.categoriesAll.set(categories); - } - - void setCategoriesRequired( - final List categories) - { - this.categoriesRequired.set(categories); + void setCategoriesAndTags( + final List newTagsAll, + final List newCategoriesAll, + final List newCategoriesRequired, + final SortedMap> newCategoryTags + ) { + this.tagsAll.set(newTagsAll); + this.categoriesAll.set(newCategoriesAll); + this.categoriesRequired.set(newCategoriesRequired); + this.categoryTags.set(newCategoryTags); } } diff --git a/com.io7m.laurel.filemodel/src/main/java/module-info.java b/com.io7m.laurel.filemodel/src/main/java/module-info.java index 2967da3..438b0ce 100644 --- a/com.io7m.laurel.filemodel/src/main/java/module-info.java +++ b/com.io7m.laurel.filemodel/src/main/java/module-info.java @@ -17,6 +17,8 @@ import com.io7m.laurel.filemodel.internal.LCommandCategoriesAdd; import com.io7m.laurel.filemodel.internal.LCommandCategoriesSetRequired; import com.io7m.laurel.filemodel.internal.LCommandCategoriesUnsetRequired; +import com.io7m.laurel.filemodel.internal.LCommandCategoryTagsAssign; +import com.io7m.laurel.filemodel.internal.LCommandCategoryTagsUnassign; import com.io7m.laurel.filemodel.internal.LCommandFactoryType; import com.io7m.laurel.filemodel.internal.LCommandImageSelect; import com.io7m.laurel.filemodel.internal.LCommandImagesAdd; @@ -49,11 +51,13 @@ provides LCommandFactoryType with LCommandCategoriesAdd, - LCommandImagesAdd, - LCommandImageSelect, - LCommandTagsAdd, LCommandCategoriesSetRequired, - LCommandCategoriesUnsetRequired; + LCommandCategoriesUnsetRequired, + LCommandCategoryTagsAssign, + LCommandCategoryTagsUnassign, + LCommandImageSelect, + LCommandImagesAdd, + LCommandTagsAdd; exports com.io7m.laurel.filemodel; exports com.io7m.laurel.filemodel.internal 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 index 690599e..d846c40 100644 --- 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 @@ -19,6 +19,7 @@ import com.io7m.laurel.filemodel.LFileModelType; import com.io7m.laurel.filemodel.LFileModels; +import com.io7m.laurel.filemodel.internal.LCategoryAndTags; import com.io7m.laurel.gui.internal.LPerpetualSubscriber; import com.io7m.laurel.model.LCategory; import com.io7m.laurel.model.LException; @@ -477,4 +478,86 @@ public void testCategorySetRequired() this.model.redo().get(TIMEOUT, SECONDS); assertEquals(List.of(tc), this.model.categoriesRequired().get()); } + + @Test + public void testCategoryTagAssign() + throws Exception + { + final var ca = new LCategory("A"); + final var cb = new LCategory("B"); + + final var tx = new LTag("TX"); + final var ty = new LTag("TY"); + final var tz = new LTag("TZ"); + + assertEquals(List.of(), this.model.categoriesRequired().get()); + assertEquals(Optional.empty(), this.model.undoText().get()); + + this.model.categoryAdd(ca).get(TIMEOUT, SECONDS); + this.model.categoryAdd(cb).get(TIMEOUT, SECONDS); + + this.model.tagAdd(tx).get(TIMEOUT, SECONDS); + this.model.tagAdd(ty).get(TIMEOUT, SECONDS); + this.model.tagAdd(tz).get(TIMEOUT, SECONDS); + + this.model.categoryTagsAssign(List.of( + new LCategoryAndTags(ca, List.of(tx, ty)) + )).get(TIMEOUT, SECONDS); + + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(null, this.model.categoryTags().get().get(cb)); + + this.model.categoryTagsAssign(List.of( + new LCategoryAndTags(cb, List.of(ty, tz)) + )).get(TIMEOUT, SECONDS); + + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(null, this.model.categoryTags().get().get(cb)); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(null, this.model.categoryTags().get().get(ca)); + assertEquals(null, this.model.categoryTags().get().get(cb)); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(null, this.model.categoryTags().get().get(cb)); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.categoryTagsUnassign( + List.of(new LCategoryAndTags(ca, List.of(tx))) + ).get(TIMEOUT, SECONDS); + + assertEquals(List.of(ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.categoryTagsUnassign( + List.of(new LCategoryAndTags(cb, List.of(tz))) + ).get(TIMEOUT, SECONDS); + + assertEquals(List.of(ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty), this.model.categoryTags().get().get(cb)); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.undo().get(TIMEOUT, SECONDS); + assertEquals(List.of(tx, ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty, tz), this.model.categoryTags().get().get(cb)); + + this.model.redo().get(TIMEOUT, SECONDS); + assertEquals(List.of(ty), this.model.categoryTags().get().get(ca)); + assertEquals(List.of(ty), this.model.categoryTags().get().get(cb)); + } }