diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java index 026d19f6833..57f50056ed4 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java @@ -35,7 +35,17 @@ import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_openResultObject; import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_siblingCommands; import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.HasBaseline_changeBaseline; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager_exportSelected; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager_makeSelectedExportable; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.HasBaseline_nextHour; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.HasBaseline_previousHour; import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_deleteSelectedPendingOrFailed; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_deleteSelectedSucceededOrExcluded; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_excludeSelectedFromReplay; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_importCommands; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_replayOrRetrySelected; import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext; import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_delete; import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_excludeFromReplay; @@ -76,19 +86,17 @@ ReplayableCommand_replayOrRetry.class, ReplayableCommand_excludeFromReplay.class, ReplayableCommand_delete.class, - CommandExportManager.changeBaseline.class, - CommandExportManager.previousHour.class, - CommandExportManager.nextHour.class, - CommandExportManager.exportSelected.class, - CommandExportManager.makeSelectedExportable.class, - CommandReplayManager.changeBaseline.class, - CommandExportManager.previousHour.class, - CommandReplayManager.nextHour.class, - CommandReplayManager.importCommands.class, - CommandReplayManager.replayOrRetrySelected.class, - CommandReplayManager.excludeSelectedFromReplay.class, - CommandReplayManager.deleteSelectedSucceededOrExcluded.class, - CommandReplayManager.deleteSelectedPendingOrFailed.class, + HasBaseline_changeBaseline.class, + HasBaseline_previousHour.class, + HasBaseline_previousHour.class, + HasBaseline_nextHour.class, + CommandExportManager_exportSelected.class, + CommandExportManager_makeSelectedExportable.class, + CommandReplayManager_importCommands.class, + CommandReplayManager_replayOrRetrySelected.class, + CommandReplayManager_excludeSelectedFromReplay.class, + CommandReplayManager_deleteSelectedSucceededOrExcluded.class, + CommandReplayManager_deleteSelectedPendingOrFailed.class, // @Component's RunBackgroundCommandsJob.class, diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java index 523b470f22d..34172393184 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java @@ -47,7 +47,9 @@ import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry; import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository; import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager_changeLimit; import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager_importCommands; import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext; import org.jspecify.annotations.NonNull; import org.springframework.lang.Nullable; @@ -90,7 +92,9 @@ public static abstract class ActionDomainEvent ) @ActionLayout(cssClassFa = "fa-bolt", sequence="10") public class activeCommands { - public class DomainEvent extends ActionDomainEvent { } + public class DomainEvent extends ActionDomainEvent { + public DomainEvent() { } + } @MemberSupport public List act() { return commandLogEntryRepository.findCurrent(); @@ -124,7 +128,9 @@ public class DomainEvent extends ActionDomainEvent { } ) @ActionLayout(cssClassFa = "fa-search", sequence="30") public class findCommands { - public class DomainEvent extends ActionDomainEvent { } + public class DomainEvent extends ActionDomainEvent { + public DomainEvent() { } + } @MemberSupport public List act( final @Nullable LocalDate from, @@ -151,7 +157,9 @@ public class DomainEvent extends ActionDomainEvent { } ) @ActionLayout(cssClassFa = "fa-search", sequence="40") public class findAll { - public class DomainEvent extends ActionDomainEvent { } + public class DomainEvent extends ActionDomainEvent { + public DomainEvent() { } + } @MemberSupport public List act() { return commandLogEntryRepository.findAll(); @@ -167,13 +175,15 @@ public class DomainEvent extends ActionDomainEvent { } ) @ActionLayout(cssClassFa = "solid share-from-square", sequence="50") public class exportManager { - public class DomainEvent extends ActionDomainEvent { } + public class DomainEvent extends ActionDomainEvent { + public DomainEvent() { } + } @MemberSupport public CommandExportManager act( @ParameterLayout(describedAs = "Limits the commands shown; only commands since this timestamp are available for export. Set to a time immediately before the commands to be replayed.") final java.sql.Timestamp since - ) { - return new CommandExportManager(since, replayContext); + ) { + return new CommandExportManager(new CommandExportManager.State(since, CommandExportManager_changeLimit.MAX_LIMIT, CommandExportManager.Mode.EXPORT), replayContext); } @MemberSupport public java.sql.Timestamp defaultSince() { @@ -191,7 +201,9 @@ public class DomainEvent extends ActionDomainEvent { } ) @ActionLayout(cssClassFa = "solid circle-play", sequence="51") public class replayManager { - public class DomainEvent extends ActionDomainEvent { } + public class DomainEvent extends ActionDomainEvent { + public DomainEvent() { } + } @MemberSupport public CommandReplayManager act( @Parameter( @@ -207,8 +219,8 @@ public class DomainEvent extends ActionDomainEvent { } .orElse(commandReplayManager); } - private CommandReplayManager.importCommands importCommands(CommandReplayManager commandReplayManager) { - return factoryService.mixin(CommandReplayManager.importCommands.class, commandReplayManager); + private CommandReplayManager_importCommands importCommands(CommandReplayManager commandReplayManager) { + return factoryService.mixin(CommandReplayManager_importCommands.class, commandReplayManager); } } diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java index 0636fa19e0a..f217f514b3f 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java @@ -151,6 +151,8 @@ public static class Nq { public static final String FIND_MOST_RECENT_COMPLETED = LOGICAL_TYPE_NAME + ".findMostRecentCompleted"; public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayState"; + public static final String FIND_FOREGROUND_BY_TIMESTAMP_BEFORE_AND_REPLAY_STATE + = LOGICAL_TYPE_NAME + ".findForegroundByTimestampBeforeAndReplayState"; public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayStates"; public static final String FIND_BACKGROUND_AND_NOT_YET_STARTED = LOGICAL_TYPE_NAME + ".findBackgroundAndNotYetStarted"; diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java index cf964950e37..4fcc3141154 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java @@ -122,9 +122,21 @@ List findByTargetAndFromAndTo( */ List findSince(final UUID interactionId, final Integer batchSize); - List findForegroundSinceTimestampAndCanBeExported(final Timestamp since); + default List findForegroundSinceTimestampAndCanBeExported(final Timestamp since) { + return findForegroundSinceTimestampAndCanBeExported(since, null); + } + + List findForegroundSinceTimestampAndCanBeExported(final Timestamp since, final Integer limitIfAny); + + List findForegroundBeforeTimestampAndCanBeExported(final Timestamp before, final Integer limitIfAny); + + default List findForegroundSinceTimestampAndHasBeenExported(final Timestamp since) { + return findForegroundSinceTimestampAndHasBeenExported(since, null); + } + + List findForegroundSinceTimestampAndHasBeenExported(final Timestamp since, final Integer limitIfAny); - List findForegroundSinceTimestampAndHasBeenExported(final Timestamp since); + List findForegroundBeforeTimestampAndHasBeenExported(final Timestamp before, final Integer limitIfAny); /** * Command Replay feature: Can replay or retry. diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java index d81fdebf1e1..46f82395ae6 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java @@ -29,7 +29,8 @@ import javax.inject.Inject; import javax.inject.Provider; - + +import org.apache.causeway.applib.query.NamedQuery; import org.apache.causeway.applib.query.Query; import org.apache.causeway.applib.query.QueryRange; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -388,32 +389,37 @@ private C findByInteractionIdElseNull(final UUID interactionId) { private List findSince( final Timestamp timestamp, - final Integer batchSize) { - - // DN generates incorrect SQL for SQL Server if count set to 1; so we set to 2 and then trim - // XXX that's a historic workaround, should rather be fixed upstream - var needsTrimFix = batchSize != null && batchSize == 1; + final Integer batchSizeIfAny) { - var q = Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_SINCE) - .withParameter("timestamp", timestamp) - .withRange(QueryRange.limit( - needsTrimFix ? 2L : batchSize - )); + var query = Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_SINCE) + .withParameter("timestamp", timestamp); - final List commandJdos = _Casts.uncheckedCast(repositoryService().allMatches(q)); - return needsTrimFix && commandJdos.size() > 1 - ? commandJdos.subList(0,1) - : commandJdos; + return allMatches(query, batchSizeIfAny); } @Override public List findForegroundSinceTimestampAndCanBeExported(final Timestamp since) { - return findForegroundSinceTimestampWithState(since, ReplayState.UNDEFINED); + return findForegroundSinceTimestampAndCanBeExported(since, null); } @Override - public List findForegroundSinceTimestampAndHasBeenExported(final Timestamp since) { - return findForegroundSinceTimestampWithState(since, ReplayState.EXPORTED); + public List findForegroundSinceTimestampAndCanBeExported(final Timestamp since, Integer batchSizeIfAny) { + return findForegroundSinceTimestampWithState(since, ReplayState.UNDEFINED, batchSizeIfAny); + } + + @Override + public List findForegroundBeforeTimestampAndCanBeExported(final Timestamp before, Integer batchSizeIfAny) { + return findForegroundBeforeTimestampWithState(before, ReplayState.UNDEFINED, batchSizeIfAny); + } + + @Override + public List findForegroundSinceTimestampAndHasBeenExported(final Timestamp since, Integer batchSizeIfAny) { + return findForegroundSinceTimestampWithState(since, ReplayState.EXPORTED, batchSizeIfAny); + } + + @Override + public List findForegroundBeforeTimestampAndHasBeenExported(final Timestamp before, Integer batchSizeIfAny) { + return findForegroundBeforeTimestampWithState(before, ReplayState.EXPORTED, batchSizeIfAny); } @Override @@ -429,14 +435,23 @@ public List findSinceAndWithReplayOkOrExcluded(final Timestamp return findForegroundSinceTimestampWithStates(since, ReplayState.OK, ReplayState.EXCLUDED); } - private List findForegroundSinceTimestampWithState(Timestamp from, ReplayState replayState) { - return _Casts.uncheckedCast( - repositoryService().allMatches( - Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE) - .withParameter("from", from) - .withParameter("replayState", replayState))); + private List findForegroundSinceTimestampWithState(Timestamp from, ReplayState replayState, Integer batchSizeIfAny) { + var query = Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE) + .withParameter("from", from) + .withParameter("replayState", replayState); + + return allMatches(query, batchSizeIfAny); + } + + private List findForegroundBeforeTimestampWithState(Timestamp to, ReplayState replayState, Integer batchSizeIfAny) { + var query = Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_BEFORE_AND_REPLAY_STATE) + .withParameter("to", to) + .withParameter("replayState", replayState); + + return allMatches(query, batchSizeIfAny); } + private List findForegroundSinceTimestampWithStates(Timestamp from, ReplayState replayState1, ReplayState replayState2) { return _Casts.uncheckedCast( repositoryService().allMatches( @@ -461,6 +476,23 @@ private static Timestamp toTimestampStartOfDayWithOffset( : null; } + private List allMatches(NamedQuery query, Integer batchSizeIfAny) { + + // DN generates incorrect SQL for SQL Server if count set to 1; so we set to 2 and then trim + // XXX that's a historic workaround, should rather be fixed upstream + var needsTrimFix = batchSizeIfAny != null && batchSizeIfAny == 1; + + if(batchSizeIfAny != null) { + query = query.withRange(QueryRange.limit(needsTrimFix ? 2L : batchSizeIfAny)); + } + + final List commandLogEntries = _Casts.uncheckedCast(repositoryService().allMatches(query)); + return needsTrimFix && commandLogEntries.size() > 1 + ? commandLogEntries.subList(0, 1) + : commandLogEntries; + } + + /** * intended for testing purposes only */ diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java index e24078a6f29..493b84b32fb 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java @@ -22,17 +22,13 @@ import java.sql.Timestamp; import java.time.Instant; -import java.time.chrono.ChronoZonedDateTime; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import org.apache.causeway.applib.ViewModel; -import org.apache.causeway.applib.annotation.Action; -import org.apache.causeway.applib.annotation.ActionLayout; import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.applib.annotation.DomainObject; @@ -40,45 +36,41 @@ import org.apache.causeway.applib.annotation.Introspection; import org.apache.causeway.applib.annotation.MemberSupport; import org.apache.causeway.applib.annotation.ObjectSupport; -import org.apache.causeway.applib.annotation.ParameterLayout; +import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.annotation.Property; import org.apache.causeway.applib.annotation.PropertyLayout; -import org.apache.causeway.applib.annotation.Publishing; -import org.apache.causeway.applib.annotation.RestrictTo; -import org.apache.causeway.applib.annotation.SemanticsOf; -import org.apache.causeway.applib.util.schema.CommandDtoUtils; -import org.apache.causeway.applib.value.Clob; -import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType; import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib; -import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry; import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository; -import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState; +import lombok.Data; import lombok.Getter; +import lombok.RequiredArgsConstructor; @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED) @DomainObjectLayout(cssClassFa = "solid share-from-square") @Named(CommandExportManager.LOGICAL_TYPE_NAME) -public final class CommandExportManager implements ViewModel { +public final class CommandExportManager implements ViewModel, HasBaseline { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager"; public static abstract class ActionDomainEvent extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { } - private ReplayContext replayContext; + ReplayContext replayContext; @Inject public CommandExportManager( final String memento, final ReplayContext replayContext) { - this(fromString(memento, replayContext.clockService().getClock().nowAsJavaSqlTimestamp()), replayContext); + this(State.parseMemento(memento, new State(replayContext.clockService().getClock().nowAsJavaSqlTimestamp(), 50, Mode.EXPORT)), replayContext); } public CommandExportManager( - final java.sql.Timestamp baseline, + final State state, final ReplayContext replayContext) { - this.baseline = baseline; + this.baseline = state.timestamp; + this.limit = state.limit; + this.mode = state.mode; this.replayContext = replayContext; } @@ -86,78 +78,60 @@ public CommandExportManager( return "Command Export Manager"; } + @RequiredArgsConstructor + public enum Mode { + EXPORT("Export commands (UNDEFINED -> EXPORTED)"), + UNEXPORT("Unexport commands (EXPORTED -> UNDEFINED)"); + + private final String title; + + @ObjectSupport + public String title() { + return title; + } + + + Mode toggle() { + return this == UNEXPORT ? EXPORT : UNEXPORT; + } + } + @Property - @PropertyLayout(describedAs = "Only commands after this timestamp are available for export") + @PropertyLayout(describedAs = "Only commands after this timestamp are available") @Getter private java.sql.Timestamp baseline; - @Action( - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = previousHour.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "1", - named = "Previous", - position = ActionLayout.Position.PANEL, - describedAs = "Move back one hour" - ) - public class previousHour { - public class DomainEvent extends ActionDomainEvent { } + @Property + @PropertyLayout(describedAs = "Number of commands per page") + @Getter + private int limit; - @MemberSupport public CommandExportManager act() { - return new CommandExportManager(addSeconds(baseline, -3600), replayContext); - } - } + @Property + @PropertyLayout(describedAs = "Whether to export or unexport commands") + @Getter + private Mode mode; - @Action( - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = nextHour.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "3", - named = "Next", - position = ActionLayout.Position.PANEL, - describedAs = "Move forward one hour" - ) - public class nextHour { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandExportManager act() { - return new CommandExportManager(addSeconds(baseline, +3600), replayContext); - } + + + @Override + @Programmatic + public CommandExportManager withBaseline(Timestamp baseline) { + return new CommandExportManager(new State(baseline, this.limit, this.mode), replayContext); } - @Action( - restrictTo = RestrictTo.PROTOTYPING, - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = changeBaseline.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "2", - named = "Change", - position = ActionLayout.Position.PANEL - ) - public class changeBaseline { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandExportManager act(final java.sql.Timestamp baseline) { - return new CommandExportManager(baseline, replayContext); - } - @MemberSupport public java.sql.Timestamp defaultBaseline() { - return CommandExportManager.this.baseline; - } + @Programmatic + public CommandExportManager withLimit(int limit) { + return new CommandExportManager(new State(this.baseline, limit, this.mode), replayContext); } - private static Timestamp addSeconds(Timestamp ts, int secondsToAdd) { - return Timestamp.from(ts.toInstant().plusSeconds(secondsToAdd)); + @Programmatic + public CommandExportManager withMode(Mode mode) { + return new CommandExportManager(new State(this.baseline, this.limit, mode), replayContext); } + // -- NOT YET EXPORTED @Collection @@ -165,154 +139,134 @@ private static Timestamp addSeconds(Timestamp ts, int secondsToAdd) { describedAs = "Commands that can be exported" ) public List getNotYetExported() { - return commandLogEntryRepository().findForegroundSinceTimestampAndCanBeExported(baseline).stream() + return commandLogEntryRepository().findForegroundSinceTimestampAndCanBeExported(baseline, limit).stream() .map(entry->new ReplayableCommand( entry.getInteractionId(), replayContext)) .collect(Collectors.toList()); } - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "notYetExported", - semantics = SemanticsOf.NON_IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = exportSelected.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "notYetExported", sequence = "1.1", - cssClassFa = "solid share-from-square", - cssClass = "btn-primary", - describedAs = "Exports selected Commands as zipped DTOs for import later. " - + "Refresh the page to see changed states." - ) - public class exportSelected { - public class DomainEvent extends ActionDomainEvent { } - - @MemberSupport public Clob act( - final List selected, - @ParameterLayout(describedAs = "File name for the exported file." ) - final String filenamePrefix, - @ParameterLayout(describedAs = "Whether to add a timestamp suffix to the exported file's name." ) - final boolean filenameTimestamp) { - - var selectedCommandLogEntries = selected.stream() - .map(ReplayableCommand::commandLogEntry) - .filter(Optional::isPresent) - .map(Optional::get) - .filter(entry->!ReplayState.isExported(entry.getReplayState())) // shouldn't be necessary unless a race condition - .sorted() - .collect(Collectors.toList()); - - var yaml = CommandDtoUtils.toYaml( - selectedCommandLogEntries.stream() - .map(CommandLogEntry::getCommandDto) - .collect(Collectors.toList())); - - final var replayableCommand = selected.get(0); // validate ensures there is at least one command - final var timestamp = filenameTimestamp - ? replayableCommand.getTimestampIfAny() - .map(ChronoZonedDateTime::toInstant) - .map(Instant::toString) - .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_")) // make safe within filename - .orElse("") - : ""; - final var filename = filenamePrefix + timestamp; - - var clob = Clob.of(filename, CommonMimeType.YAML, yaml); - - // do this last once we have successfully created the Clob - selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED)); - - return clob; - } - - @MemberSupport public String disableAct() { - return getNotYetExported().isEmpty() ? "No commands in collection" : null; - } - - @MemberSupport public String defaultFilenamePrefix() { - return "commands"; - } - - @MemberSupport public boolean defaultFilenameTimestamp() { - return true; - } - - @MemberSupport public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command to export" : null; - } - - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getNotYetExported(); - } + @MemberSupport + public boolean hideNotYetExported() { + return this.mode == Mode.UNEXPORT; + } + @Programmatic + public List getNotYetExportedPrevious() { + return commandLogEntryRepository().findForegroundBeforeTimestampAndCanBeExported(baseline, limit).stream() + .map(entry->new ReplayableCommand( + entry.getInteractionId(), + replayContext)) + .collect(Collectors.toList()); } - // -- EXPORTED + + // -- HAVE BEEN EXPORTED @Collection - @CollectionLayout(describedAs = "Commands that have been exported") + @CollectionLayout( + describedAs = "Commands that have been exported" + ) public List getExported() { - return commandLogEntryRepository().findForegroundSinceTimestampAndHasBeenExported(baseline).stream() + return commandLogEntryRepository().findForegroundSinceTimestampAndHasBeenExported(baseline, limit).stream() .map(entry->new ReplayableCommand( entry.getInteractionId(), replayContext)) .collect(Collectors.toList()); } + @MemberSupport + public boolean hideExported() { + return this.mode == Mode.EXPORT; + } + @Programmatic + private List getExportedPrevious() { + return commandLogEntryRepository().findForegroundBeforeTimestampAndHasBeenExported(baseline, limit).stream() + .map(entry->new ReplayableCommand( + entry.getInteractionId(), + replayContext)) + .collect(Collectors.toList()); + } - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "exported", - commandPublishing = Publishing.DISABLED, - semantics = SemanticsOf.IDEMPOTENT, - domainEvent = makeSelectedExportable.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "exported", sequence = "2.1", - describedAs = "Makes selected Commands exportable (again)" - ) - public class makeSelectedExportable { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport - public CommandExportManager act(final List selected) { - selected.forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility - return CommandExportManager.this; - } + public enum Direction { + NEXT, PREVIOUS + } - @MemberSupport - public String disableAct() { - return getExported().isEmpty() ? "No commands in collection" : null; + @Programmatic + public List commands(Direction direction) { + switch (mode) { + case EXPORT: + switch (direction) { + case NEXT: + return getNotYetExported(); + case PREVIOUS: + default: + return getNotYetExportedPrevious(); + } + case UNEXPORT: + default: + switch (direction) { + case NEXT: + return getExported(); + case PREVIOUS: + default: + return getExportedPrevious(); + } } + } - @MemberSupport - public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command" : null; - } - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getExported(); - } - } // -- VM STATE @Override public String viewModelMemento() { - return TimestampMarshallUtil.toString(this.baseline); + return new State(baseline, limit, mode).toMemento(); } // -- HELPER private CommandLogEntryRepository commandLogEntryRepository() { return replayContext.commandLogEntryRepository(); } + + @Data + public static class State { + private static final String DELIMITER = "--"; + + private final Timestamp timestamp; + private final int limit; + private final Mode mode; + + public static State parseMemento(String memento, State fallback) { + if(memento == null || memento.isEmpty()) { + return fallback; + } + try { + String[] parts = memento.split(DELIMITER, -1); + if(parts.length != 3) { + return fallback; + } + + final Timestamp fallbackTimestamp = fallback != null + ? fallback.timestamp + : Timestamp.from(Instant.now()); + final int fallbackLimit = fallback != null ? fallback.limit : 0; + final Mode fallbackMode = fallback != null ? fallback.mode : Mode.EXPORT; + + final Timestamp timestamp = fromString(parts[0], fallbackTimestamp); + final int limit = parts[1].isBlank() ? fallbackLimit : Integer.parseInt(parts[1]); + final Mode mode = parts[2].isBlank() ? fallbackMode : Mode.valueOf(parts[2]); + + return new State(timestamp, limit, mode); + } catch (Exception e) { + return fallback; + } + } + + public String toMemento() { + return TimestampMarshallUtil.toString(timestamp) + DELIMITER + limit + DELIMITER + mode; + } + } + } diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml index 272902d81be..fe52c33b98a 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml @@ -5,10 +5,18 @@ - - - - + + + + + + + + + + + + @@ -42,10 +50,6 @@ - - - - diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_changeLimit.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_changeLimit.java new file mode 100644 index 00000000000..284308278a2 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_changeLimit.java @@ -0,0 +1,60 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +import static org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.Direction.NEXT; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandExportManager_changeLimit.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "limit", sequence = "1", + position = ActionLayout.Position.PANEL, + describedAs = "Change number of commands in page" +) +@RequiredArgsConstructor +public class CommandExportManager_changeLimit { + + public static int MAX_LIMIT = 100; + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public CommandExportManager act(int newLimit) { + return commandExportManager.withLimit(newLimit); + } + @MemberSupport + public String validateNewLimit(int newLimit) { + if(newLimit < 0) { + return "Limit must be greater than or equal to 0."; + } + if(newLimit > MAX_LIMIT) { + return "Limit cannot be greater than " + MAX_LIMIT + "."; + } + return null; + } + + @MemberSupport + public String disableAct() { + final var commands = commandExportManager.commands(NEXT); + final var size = commands.size(); + if (size == 0) { + return "Empty"; + } + final var lastReplayable = commands.get(size - 1); + return commandExportManager(lastReplayable).commands(NEXT).isEmpty() ? "No more commands" : null; + } + + private CommandExportManager commandExportManager(ReplayableCommand lastReplayable) { + final var timestamp = lastReplayable.getTimestamp().toInstant(); + final var baselinePlus5Millis = HasBaseline.addMillis(timestamp, 5); + return commandExportManager.withBaseline(baselinePlus5Millis); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_exportSelected.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_exportSelected.java new file mode 100644 index 00000000000..923f353f212 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_exportSelected.java @@ -0,0 +1,102 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.time.chrono.ChronoZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.causeway.applib.annotation.*; +import org.apache.causeway.applib.util.schema.CommandDtoUtils; +import org.apache.causeway.applib.value.Clob; +import org.apache.causeway.applib.value.NamedWithMimeType; +import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry; +import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "notYetExported", + semantics = SemanticsOf.NON_IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandExportManager_exportSelected.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "notYetExported", sequence = "1.1", + cssClassFa = "solid share-from-square", + cssClass = "btn-primary", + describedAs = "Exports selected Commands as zipped DTOs for import later. " + + "Refresh the page to see changed states." +) +@RequiredArgsConstructor +public class CommandExportManager_exportSelected { + + public static class DomainEvent extends CommandExportManager.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public Clob act( + final List selected, + @ParameterLayout(describedAs = "File name for the exported file.") final String filenamePrefix, + @ParameterLayout(describedAs = "Whether to add a timestamp suffix to the exported file's name.") final boolean filenameTimestamp) { + + var selectedCommandLogEntries = selected.stream() + .map(ReplayableCommand::commandLogEntry) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(entry -> !ReplayState.isExported(entry.getReplayState())) // shouldn't be necessary unless a race condition + .sorted() + .collect(Collectors.toList()); + + var yaml = CommandDtoUtils.toYaml( + selectedCommandLogEntries.stream() + .map(CommandLogEntry::getCommandDto) + .collect(Collectors.toList())); + + final var replayableCommand = selected.get(0); // validate ensures there is at least one command + final var timestamp = filenameTimestamp + ? replayableCommand.getTimestampIfAny() + .map(ChronoZonedDateTime::toInstant) + .map(Instant::toString) + .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_")) // make safe within filename + .orElse("") + : ""; + final var filename = filenamePrefix + timestamp; + + var clob = Clob.of(filename, NamedWithMimeType.CommonMimeType.YAML, yaml); + + // do this last once we have successfully created the Clob + selectedCommandLogEntries.forEach(c -> c.setReplayState(ReplayState.EXPORTED)); + + return clob; + } + + @MemberSupport + public String disableAct() { + return commandExportManager.getNotYetExported().isEmpty() ? "No commands in collection" : null; + } + + @MemberSupport + public String defaultFilenamePrefix() { + return "commands"; + } + + @MemberSupport + public boolean defaultFilenameTimestamp() { + return true; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command to export" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandExportManager.getNotYetExported(); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_makeSelectedExportable.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_makeSelectedExportable.java new file mode 100644 index 00000000000..86710d605a6 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_makeSelectedExportable.java @@ -0,0 +1,49 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "exported", + commandPublishing = Publishing.DISABLED, + semantics = SemanticsOf.IDEMPOTENT, + domainEvent = CommandExportManager_makeSelectedExportable.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "exported", sequence = "2.1", + describedAs = "Makes selected Commands exportable (again)" +) +@RequiredArgsConstructor +public class CommandExportManager_makeSelectedExportable { + + public static class DomainEvent extends CommandExportManager.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public CommandExportManager act(final List selected) { + selected.forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility + return commandExportManager; + } + + @MemberSupport + public String disableAct() { + return commandExportManager.getExported().isEmpty() ? "No commands in collection" : null; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandExportManager.getExported(); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_nextPage.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_nextPage.java new file mode 100644 index 00000000000..84021cd3466 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_nextPage.java @@ -0,0 +1,55 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +import static org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.Direction.*; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandExportManager_nextPage.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "limit", sequence = "2", + named = "Next Page", + position = ActionLayout.Position.BELOW, + describedAs = "Move forward to next page of commands" +) +@RequiredArgsConstructor +public class CommandExportManager_nextPage { + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public CommandExportManager act() { + final var commands = commandExportManager.commands(NEXT); + final var size = commands.size(); + if (size == 0) { + return commandExportManager; + } + final var lastReplayable = commands.get(size - 1); + return commandExportManager(lastReplayable); + } + + @MemberSupport + public String disableAct() { + final var commands = commandExportManager.commands(NEXT); + final var size = commands.size(); + if (size == 0) { + return "Empty"; + } + final var lastReplayable = commands.get(size - 1); + return commandExportManager(lastReplayable).commands(NEXT).isEmpty() ? "No more commands" : null; + } + + private CommandExportManager commandExportManager(final ReplayableCommand replayableCommand) { + final var timestamp = replayableCommand.getTimestamp().toInstant(); + final var baselinePlus5Millis = HasBaseline.addMillis(timestamp, 5); + return commandExportManager.withBaseline(baselinePlus5Millis); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_previousPage.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_previousPage.java new file mode 100644 index 00000000000..4b4a59638b0 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_previousPage.java @@ -0,0 +1,56 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +import static org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.Direction.*; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandExportManager_previousPage.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "limit", sequence = "1", + named = "Previous", + position = ActionLayout.Position.BELOW, + describedAs = "Move backwards to previous page of commands" +) +@RequiredArgsConstructor +public class CommandExportManager_previousPage { + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public CommandExportManager act() { + final var commands = commandExportManager.commands(NEXT); + final var size = commands.size(); + if (size == 0) { + return commandExportManager; + } + final var firstReplayable = commands.get(0); + return commandExportManager(firstReplayable); + } + + @MemberSupport + public String disableAct() { + final var commands = commandExportManager.commands(NEXT); + final var size = commands.size(); + if (size == 0) { + return "No commands"; + } + final var firstReplayable = commands.get(0); + return commandExportManager(firstReplayable).commands(PREVIOUS).isEmpty() ? "No previous commands" : null; + } + + private CommandExportManager commandExportManager(final ReplayableCommand replayableCommand) { + final var timestamp = replayableCommand.getTimestamp().toInstant(); + final var baselineMinus5Millis = HasBaseline.addMillis(timestamp, -5); + return commandExportManager.withBaseline(baselineMinus5Millis); + } + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_toggleMode.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_toggleMode.java new file mode 100644 index 00000000000..83c931da0ff --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager_toggleMode.java @@ -0,0 +1,33 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +import static org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.Direction.NEXT; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandExportManager_toggleMode.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + sequence = "1.1", + named = "Toggle", + cssClass = "btn-secondary", + describedAs = "Toggle between exporting and un-exporting." +) +@RequiredArgsConstructor +public class CommandExportManager_toggleMode { + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { } + + private final CommandExportManager commandExportManager; + + @MemberSupport + public CommandExportManager act() { + return commandExportManager.withMode(commandExportManager.getMode().toggle()); + } + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java index 217318d7f14..b8bb8b066ec 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java @@ -21,38 +21,27 @@ import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString; import java.sql.Timestamp; -import java.time.Instant; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Named; -import javax.xml.datatype.XMLGregorianCalendar; import org.apache.causeway.applib.ViewModel; -import org.apache.causeway.applib.annotation.Action; -import org.apache.causeway.applib.annotation.ActionLayout; import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainObjectLayout; import org.apache.causeway.applib.annotation.Introspection; -import org.apache.causeway.applib.annotation.MemberSupport; import org.apache.causeway.applib.annotation.ObjectSupport; -import org.apache.causeway.applib.annotation.Parameter; -import org.apache.causeway.applib.annotation.ParameterLayout; +import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.annotation.Property; import org.apache.causeway.applib.annotation.PropertyLayout; -import org.apache.causeway.applib.annotation.Publishing; -import org.apache.causeway.applib.annotation.RestrictTo; -import org.apache.causeway.applib.annotation.SemanticsOf; -import org.apache.causeway.applib.util.schema.CommandDtoUtils; -import org.apache.causeway.applib.value.Blob; import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib; import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository; -import org.apache.causeway.schema.cmd.v2.CommandDto; +import org.eclipse.persistence.sessions.coordination.CommandManager; import org.jspecify.annotations.NonNull; import lombok.Getter; @@ -60,14 +49,14 @@ @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED) @DomainObjectLayout(cssClassFa = "solid circle-play") @Named(CommandReplayManager.LOGICAL_TYPE_NAME) -public final class CommandReplayManager implements ViewModel { +public final class CommandReplayManager implements ViewModel, HasBaseline { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager"; public static abstract class ActionDomainEvent extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { } - private ReplayContext replayContext; + ReplayContext replayContext; @Inject public CommandReplayManager( @@ -93,115 +82,10 @@ public CommandReplayManager( @Getter private java.sql.Timestamp baseline; - @Action( - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = previousHour.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "1", - named = "Previous", - position = ActionLayout.Position.PANEL, - describedAs = "Move back one hour" - ) - public class previousHour { - public class DomainEvent extends ActionDomainEvent { } - - @MemberSupport public CommandReplayManager act() { - return new CommandReplayManager(addSeconds(baseline, -3600), replayContext); - } - } - - @Action( - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = nextHour.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "3", - named = "Next", - position = ActionLayout.Position.PANEL, - describedAs = "Move forward one hour" - ) - public class nextHour { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandReplayManager act() { - return new CommandReplayManager(addSeconds(baseline, +3600), replayContext); - } - } - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - semantics = SemanticsOf.SAFE, - commandPublishing = Publishing.DISABLED, - domainEvent = changeBaseline.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "baseline", sequence = "2", - named = "Change", - position = ActionLayout.Position.PANEL - ) - public class changeBaseline { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandReplayManager act(final java.sql.Timestamp baseline) { - return new CommandReplayManager(baseline, replayContext); - } - @MemberSupport public java.sql.Timestamp defaultBaseline() { - return CommandReplayManager.this.baseline; - } - } - - private static Timestamp addSeconds(Timestamp ts, int secondsToAdd) { - return Timestamp.from(ts.toInstant().plusSeconds(secondsToAdd)); - } - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - semantics = SemanticsOf.IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = importCommands.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - sequence = "1.1", - cssClass = "btn-secondary", - describedAs = "Imports commands from yaml format, then persists them with a replayState of PENDING." - ) - public class importCommands { - public class DomainEvent extends ActionDomainEvent { } - public CommandReplayManager act( - @Parameter(fileAccept = ".yml,.yaml") - final Blob commandsYaml, - @ParameterLayout(describedAs = "Change the baseline to the timestamp of the oldest, so that they are listed at top") - final boolean moveBaselineToOldest) { - var yamlDs = commandsYaml.asDataSource(); - - final List commandDtos = CommandDtoUtils.fromYaml(yamlDs); - commandDtos.forEach(commandLogEntryRepository()::saveForReplay); - - return commandDtos.stream() - .filter(x -> moveBaselineToOldest) - .map(CommandDto::getTimestamp) - .map(CommandReplayManager::toJavaSqlTimestamp) - .sorted() - .findFirst() - .map(timestamp -> new CommandReplayManager(timestamp, replayContext)) - .orElse(CommandReplayManager.this); - } - - @MemberSupport public boolean defaultMoveBaselineToOldest() { - return true; - } - - } - - private static Timestamp toJavaSqlTimestamp(XMLGregorianCalendar xgc) { - if (xgc == null) return null; - Instant instant = xgc.toGregorianCalendar().toZonedDateTime().toInstant(); - return Timestamp.from(instant); + @Override + @Programmatic + public CommandReplayManager withBaseline(Timestamp baseline) { + return new CommandReplayManager(baseline, replayContext); } @@ -216,182 +100,18 @@ public List getPendingOrFailed() { .collect(Collectors.toList()); } - private @NonNull Stream streamPendingOrFailed() { + @NonNull Stream streamPendingOrFailed() { return commandLogEntryRepository().findForegroundSinceTimestampAndWithReplayPendingOrFailed(baseline).stream() .map(entry -> new ReplayableCommand( entry.getInteractionId(), replayContext)); } - private long sizePendingOrFailed() { + long sizePendingOrFailed() { return streamPendingOrFailed().count(); } - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "pendingOrFailed", - semantics = SemanticsOf.NON_IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = replayOrRetrySelected.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "pendingOrFailed", sequence = "1.2", - cssClass = "btn-secondary", - cssClassFa = "solid forward", - describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. " - + "If any of the given commands fails, " - + "its surrounding transaction is rolled back, but any successful commands so far are marked OK). " - + "The command, that caused the failure, gets marked FAILED.") - public class replayOrRetrySelected { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandReplayManager act(final List selected) { - var replayables = selected.stream() - .sorted() - .collect(Collectors.toList()); - for(var replayableCommand : replayables) { - var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility - if(tryReplayOrRetry.isFailure()) { - return CommandReplayManager.this; // stop further execution - } - } - return CommandReplayManager.this; - } - - - @MemberSupport - public String disableAct() { - return getPendingOrFailed().isEmpty() ? "No commands in collection" : null; - } - - @MemberSupport - public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command" : null; - } - - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getPendingOrFailed(); - } - } - - - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "pendingOrFailed", - semantics = SemanticsOf.NON_IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = replayOrRetrySelected.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "pendingOrFailed", sequence = "1.1", - cssClassFa = "solid circle-play", - cssClass = "btn-primary", - describedAs = "Executes the oldest command.") - public class replayOrRetryNext { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport public CommandReplayManager act() { - var nextIfAny = streamPendingOrFailed().findFirst(); - // should always be present, due to our guard - nextIfAny.ifPresent(ReplayableCommand::tryReplayOrRetry); - return CommandReplayManager.this; - } - - @MemberSupport - public String disableAct() { - return sizePendingOrFailed() == 0 ? "No commands in collection" : null; - } - } - - - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "pendingOrFailed", - semantics = SemanticsOf.NON_IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = excludeSelectedFromReplay.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "pendingOrFailed", sequence = "1.3", - cssClass = "btn-secondary", - describedAs = "Marks selected Commands to be EXCLUDED from replay" - ) - public class excludeSelectedFromReplay { - public class DomainEvent extends ActionDomainEvent { } - @MemberSupport - public CommandReplayManager act(final List selected) { - selected.stream() - .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility - return CommandReplayManager.this; - } - - @MemberSupport - public String disableAct() { - return sizePendingOrFailed() == 0 ? "No commands in collection" : null; - } - - @MemberSupport - public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command" : null; - } - - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getPendingOrFailed(); - } - - } - - - - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "pendingOrFailed", - semantics = SemanticsOf.NON_IDEMPOTENT, - commandPublishing = Publishing.DISABLED, - domainEvent = deleteSelectedPendingOrFailed.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "pendingOrFailed", sequence = "1.4", - cssClass = "btn-danger", - describedAs = "Deletes selected Commands (cannot be undone)" - ) - public class deleteSelectedPendingOrFailed { - public class DomainEvent extends ActionDomainEvent { } - public CommandReplayManager act(final List selected) { - selected.stream() - .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility - return CommandReplayManager.this; - } - - @MemberSupport - public String disableAct() { - return getPendingOrFailed().isEmpty() ? "No commands in collection" : null; - } - - @MemberSupport - public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command" : null; - } - - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getPendingOrFailed(); - } - - } - - - // -- OK OR EXCLUDE @Collection @@ -408,44 +128,6 @@ public List getSucceededOrExcluded() { } - @Action( - restrictTo = RestrictTo.PROTOTYPING, - choicesFrom = "succeededOrExcluded", - semantics = SemanticsOf.IDEMPOTENT, - domainEvent = deleteSelectedSucceededOrExcluded.DomainEvent.class, - executionPublishing = Publishing.DISABLED - ) - @ActionLayout( - associateWith = "succeededOrExcluded", - named = "Delete Selected", - describedAs = "Deletes selected Commands (cannot be undone)" - ) - public class deleteSelectedSucceededOrExcluded { - public class DomainEvent extends ActionDomainEvent { } - public CommandReplayManager act(final List selected) { - selected.stream() - .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility - return CommandReplayManager.this; - } - - @MemberSupport - public String disableAct() { - return getSucceededOrExcluded().isEmpty() ? "No commands in collection" : null; - } - - @MemberSupport - public String validateSelected(final List selected) { - return selected != null && selected.isEmpty() ? "Select at least one command" : null; - } - - // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet - @MemberSupport - public List choicesSelected() { - return getSucceededOrExcluded(); - } - } - - // -- VM STATE @Override diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedPendingOrFailed.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedPendingOrFailed.java new file mode 100644 index 00000000000..43d65d2b9d3 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedPendingOrFailed.java @@ -0,0 +1,51 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "pendingOrFailed", + semantics = SemanticsOf.NON_IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandReplayManager_deleteSelectedPendingOrFailed.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "pendingOrFailed", sequence = "1.4", + cssClass = "btn-danger", + describedAs = "Deletes selected Commands (cannot be undone)" +) +@RequiredArgsConstructor +public class CommandReplayManager_deleteSelectedPendingOrFailed { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { } + + private final CommandReplayManager commandReplayManager; + + public CommandReplayManager act(final List selected) { + selected.stream() + .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility + return commandReplayManager; + } + + @MemberSupport + public String disableAct() { + return commandReplayManager.getPendingOrFailed().isEmpty() ? "No commands in collection" : null; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandReplayManager.getPendingOrFailed(); + } + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedSucceededOrExcluded.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedSucceededOrExcluded.java new file mode 100644 index 00000000000..1c6e246b0a3 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_deleteSelectedSucceededOrExcluded.java @@ -0,0 +1,49 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "succeededOrExcluded", + semantics = SemanticsOf.IDEMPOTENT, + domainEvent = CommandReplayManager_deleteSelectedSucceededOrExcluded.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "succeededOrExcluded", + named = "Delete Selected", + describedAs = "Deletes selected Commands (cannot be undone)" +) +@RequiredArgsConstructor +public class CommandReplayManager_deleteSelectedSucceededOrExcluded { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { } + + private final CommandReplayManager commandReplayManager; + + public CommandReplayManager act(final List selected) { + selected.stream() + .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility + return commandReplayManager; + } + + @MemberSupport + public String disableAct() { + return commandReplayManager.getSucceededOrExcluded().isEmpty() ? "No commands in collection" : null; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandReplayManager.getSucceededOrExcluded(); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_excludeSelectedFromReplay.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_excludeSelectedFromReplay.java new file mode 100644 index 00000000000..ecd036ee658 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_excludeSelectedFromReplay.java @@ -0,0 +1,52 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "pendingOrFailed", + semantics = SemanticsOf.NON_IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandReplayManager_excludeSelectedFromReplay.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "pendingOrFailed", sequence = "1.3", + cssClass = "btn-secondary", + describedAs = "Marks selected Commands to be EXCLUDED from replay" +) +@RequiredArgsConstructor +public class CommandReplayManager_excludeSelectedFromReplay { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { } + + private final CommandReplayManager commandReplayManager; + + @MemberSupport + public CommandReplayManager act(final List selected) { + selected.stream() + .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility + return commandReplayManager; + } + + @MemberSupport + public String disableAct() { + return commandReplayManager.sizePendingOrFailed() == 0 ? "No commands in collection" : null; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandReplayManager.getPendingOrFailed(); + } + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_importCommands.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_importCommands.java new file mode 100644 index 00000000000..c8ce136ce38 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_importCommands.java @@ -0,0 +1,69 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; + +import javax.inject.Inject; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.apache.causeway.applib.annotation.*; +import org.apache.causeway.applib.util.schema.CommandDtoUtils; +import org.apache.causeway.applib.value.Blob; +import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository; +import org.apache.causeway.schema.cmd.v2.CommandDto; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + semantics = SemanticsOf.IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandReplayManager_importCommands.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + sequence = "1.1", + cssClass = "btn-secondary", + describedAs = "Imports commands from yaml format, then persists them with a replayState of PENDING." +) +@RequiredArgsConstructor +public class CommandReplayManager_importCommands { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { + } + + private final CommandReplayManager commandReplayManager; + + public CommandReplayManager act( + @Parameter(fileAccept = ".yml,.yaml") final Blob commandsYaml, + @ParameterLayout(describedAs = "Change the baseline to the timestamp of the oldest, so that they are listed at top") final boolean moveBaselineToOldest) { + var yamlDs = commandsYaml.asDataSource(); + + final List commandDtos = CommandDtoUtils.fromYaml(yamlDs); + commandDtos.forEach(commandLogEntryRepository::saveForReplay); + + return commandDtos.stream() + .filter(x -> moveBaselineToOldest) + .map(CommandDto::getTimestamp) + .map(CommandReplayManager_importCommands::toJavaSqlTimestamp) + .sorted() + .findFirst() + .map(timestamp -> new CommandReplayManager(timestamp, commandReplayManager.replayContext)) + .orElse(commandReplayManager); + } + + @MemberSupport + public boolean defaultMoveBaselineToOldest() { + return true; + } + + private static Timestamp toJavaSqlTimestamp(XMLGregorianCalendar xgc) { + if (xgc == null) return null; + Instant instant = xgc.toGregorianCalendar().toZonedDateTime().toInstant(); + return Timestamp.from(instant); + } + + @Inject CommandLogEntryRepository commandLogEntryRepository; + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetryNext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetryNext.java new file mode 100644 index 00000000000..8c7e8f0f934 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetryNext.java @@ -0,0 +1,39 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "pendingOrFailed", + semantics = SemanticsOf.NON_IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandReplayManager_replayOrRetrySelected.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "pendingOrFailed", sequence = "1.1", + cssClassFa = "solid circle-play", + cssClass = "btn-primary", + describedAs = "Executes the oldest command.") +@RequiredArgsConstructor +public class CommandReplayManager_replayOrRetryNext { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { } + + private final CommandReplayManager commandReplayManager; + + @MemberSupport + public CommandReplayManager act() { + var nextIfAny = commandReplayManager.streamPendingOrFailed().findFirst(); + // should always be present, due to our guard + nextIfAny.ifPresent(ReplayableCommand::tryReplayOrRetry); + return commandReplayManager; + } + + @MemberSupport + public String disableAct() { + return commandReplayManager.sizePendingOrFailed() == 0 ? "No commands in collection" : null; + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetrySelected.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetrySelected.java new file mode 100644 index 00000000000..3154e571778 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager_replayOrRetrySelected.java @@ -0,0 +1,63 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + choicesFrom = "pendingOrFailed", + semantics = SemanticsOf.NON_IDEMPOTENT, + commandPublishing = Publishing.DISABLED, + domainEvent = CommandReplayManager_replayOrRetrySelected.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "pendingOrFailed", sequence = "1.2", + cssClass = "btn-secondary", + cssClassFa = "solid forward", + describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. " + + "If any of the given commands fails, " + + "its surrounding transaction is rolled back, but any successful commands so far are marked OK). " + + "The command, that caused the failure, gets marked FAILED.") +@RequiredArgsConstructor +public class CommandReplayManager_replayOrRetrySelected { + + public static class DomainEvent extends CommandReplayManager.ActionDomainEvent { } + + private final CommandReplayManager commandReplayManager; + + @MemberSupport + public CommandReplayManager act(final List selected) { + var replayables = selected.stream() + .sorted() + .collect(Collectors.toList()); + for (var replayableCommand : replayables) { + var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility + if (tryReplayOrRetry.isFailure()) { + return commandReplayManager; // stop further execution + } + } + return commandReplayManager; + } + + + @MemberSupport + public String disableAct() { + return commandReplayManager.getPendingOrFailed().isEmpty() ? "No commands in collection" : null; + } + + @MemberSupport + public String validateSelected(final List selected) { + return selected != null && selected.isEmpty() ? "Select at least one command" : null; + } + + // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet + @MemberSupport + public List choicesSelected() { + return commandReplayManager.getPendingOrFailed(); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline.java new file mode 100644 index 00000000000..8d45d42474a --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline.java @@ -0,0 +1,32 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import java.sql.Timestamp; +import java.time.Instant; + +import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib; + +public interface HasBaseline { + + abstract class ActionDomainEvent + extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { } + + + java.sql.Timestamp getBaseline(); + + HasBaseline withBaseline(Timestamp baseline); + + + static Timestamp addSeconds(Timestamp ts, int secondsToAdd) { + Instant instant = ts.toInstant(); + return addSeconds(instant, secondsToAdd); + } + + static Timestamp addSeconds(Instant instant, int secondsToAdd) { + return Timestamp.from(instant.plusSeconds(secondsToAdd)); + } + + static Timestamp addMillis(Instant instant, int millisToAdd) { + return Timestamp.from(instant.plusMillis(millisToAdd)); + } + +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_changeBaseline.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_changeBaseline.java new file mode 100644 index 00000000000..9c42bb34bc4 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_changeBaseline.java @@ -0,0 +1,35 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +@Action( + restrictTo = RestrictTo.PROTOTYPING, + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = HasBaseline_changeBaseline.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "baseline", sequence = "2", + named = "Change", + position = ActionLayout.Position.PANEL +) +@RequiredArgsConstructor +public class HasBaseline_changeBaseline { + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { } + + private final HasBaseline hasBaseline; + + @MemberSupport + public HasBaseline act(final java.sql.Timestamp baseline) { + return hasBaseline.withBaseline(baseline); + } + + @MemberSupport + public java.sql.Timestamp defaultBaseline() { + return hasBaseline.getBaseline(); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_nextHour.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_nextHour.java new file mode 100644 index 00000000000..41f4d53586e --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_nextHour.java @@ -0,0 +1,32 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = HasBaseline_nextHour.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "baseline", sequence = "3", + named = "Next", + position = ActionLayout.Position.PANEL, + describedAs = "Move forward one hour" +) +@RequiredArgsConstructor +public class HasBaseline_nextHour { + + public static class DomainEvent extends HasBaseline.ActionDomainEvent { + } + + private final HasBaseline hasBaseline; + + @MemberSupport + public HasBaseline act() { + final var baseline = HasBaseline.addSeconds(hasBaseline.getBaseline(), +3600); + return hasBaseline.withBaseline(baseline); + } +} diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_previousHour.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_previousHour.java new file mode 100644 index 00000000000..74fcc899af8 --- /dev/null +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/HasBaseline_previousHour.java @@ -0,0 +1,31 @@ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import lombok.RequiredArgsConstructor; + +import org.apache.causeway.applib.annotation.*; + +@Action( + semantics = SemanticsOf.SAFE, + commandPublishing = Publishing.DISABLED, + domainEvent = HasBaseline_previousHour.DomainEvent.class, + executionPublishing = Publishing.DISABLED +) +@ActionLayout( + associateWith = "baseline", sequence = "1", + named = "Previous", + position = ActionLayout.Position.PANEL, + describedAs = "Move back one hour" +) +@RequiredArgsConstructor +public class HasBaseline_previousHour { + public static class DomainEvent extends HasBaseline.ActionDomainEvent { + } + + private final HasBaseline hasBaseline; + + @MemberSupport + public HasBaseline act() { + final var baseline = HasBaseline.addSeconds(hasBaseline.getBaseline(), -3600); + return hasBaseline.withBaseline(baseline); + } +} diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManagerStateTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManagerStateTest.java new file mode 100644 index 00000000000..8a1c5b1465d --- /dev/null +++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManagerStateTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.commandlog.applib.dom.replay; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Timestamp; +import java.time.Instant; + +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.Mode; +import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager.State; +import org.junit.jupiter.api.Test; + +class CommandExportManagerStateTest { + + @Test + void roundtrip_toMemento_and_parseMemento() { + final var timestamp = Timestamp.from(Instant.parse("2026-06-03T12:34:56.789Z")); + final var state = new State(timestamp, 25, Mode.UNEXPORT); + + final String memento = state.toMemento(); + final State parsed = State.parseMemento(memento, null); + + assertThat(memento).contains("--"); + assertThat(parsed).isNotNull(); + assertThat(parsed.getTimestamp()).isEqualTo(timestamp); + assertThat(parsed.getLimit()).isEqualTo(25); + assertThat(parsed.getMode()).isEqualTo(Mode.UNEXPORT); + } + + @Test + void parseMemento_null_returns_fallback() { + final State fallback = fallbackState(); + + final State parsed = State.parseMemento(null, fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_empty_returns_fallback() { + final State fallback = fallbackState(); + + final State parsed = State.parseMemento("", fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_invalid_part_count_returns_fallback() { + final State fallback = fallbackState(); + + final State parsed = State.parseMemento("only-timestamp", fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_blank_limit_and_mode_uses_fallback_values() { + final State fallback = fallbackState(); + final Timestamp timestamp = Timestamp.from(Instant.parse("2026-06-01T00:00:00.000Z")); + final String memento = TimestampMarshallUtil.toString(timestamp) + "----"; + + final State parsed = State.parseMemento(memento, fallback); + + assertThat(parsed).isNotNull(); + assertThat(parsed.getTimestamp()).isEqualTo(timestamp); + assertThat(parsed.getLimit()).isEqualTo(fallback.getLimit()); + assertThat(parsed.getMode()).isEqualTo(fallback.getMode()); + } + + @Test + void parseMemento_invalid_limit_returns_fallback() { + final State fallback = fallbackState(); + final String memento = TimestampMarshallUtil.toString(fallback.getTimestamp()) + "--abc--EXPORT"; + + final State parsed = State.parseMemento(memento, fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_invalid_mode_returns_fallback() { + final State fallback = fallbackState(); + final String memento = TimestampMarshallUtil.toString(fallback.getTimestamp()) + "--10--UNKNOWN"; + + final State parsed = State.parseMemento(memento, fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_invalid_timestamp_uses_fallback_timestamp() { + final State fallback = fallbackState(); + + final State parsed = State.parseMemento("not-a-timestamp--10--UNEXPORT", fallback); + + assertThat(parsed).isNotNull(); + assertThat(parsed.getTimestamp()).isEqualTo(fallback.getTimestamp()); + assertThat(parsed.getLimit()).isEqualTo(10); + assertThat(parsed.getMode()).isEqualTo(Mode.UNEXPORT); + } + + @Test + void parseMemento_legacy_delimiter_returns_fallback() { + final State fallback = fallbackState(); + final String memento = TimestampMarshallUtil.toString(fallback.getTimestamp()) + "|10|UNEXPORT"; + + final State parsed = State.parseMemento(memento, fallback); + + assertThat(parsed).isSameAs(fallback); + } + + @Test + void parseMemento_invalid_and_null_fallback_returns_null() { + final State parsed = State.parseMemento("not-three-parts", null); + + assertThat(parsed).isNull(); + } + + private static State fallbackState() { + return new State( + Timestamp.from(Instant.parse("2026-05-30T01:02:03.004Z")), + 77, + Mode.EXPORT); + } +} + + diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java index 49513f3859d..0ed194906ab 100644 --- a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java +++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java @@ -213,6 +213,14 @@ + " && timestamp >= :from " + " && replayState == :replayState " + " ORDER BY timestamp ASC"), + @Query( + name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_BEFORE_AND_REPLAY_STATE, + value = "SELECT " + + " FROM " + CommandLogEntry.FQCN + " " + + " WHERE executeIn == 'FOREGROUND' " + + " && timestamp < :to " + + " && replayState == :replayState " + + " ORDER BY timestamp DESC"), @Query( name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES, value = "SELECT " diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java index 8cc67652432..269e93f1771 100644 --- a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java +++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java @@ -216,6 +216,14 @@ + " AND cl.timestamp >= :from " + " AND cl.replayState = :replayState " + " ORDER BY cl.timestamp ASC"), + @NamedQuery( + name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_BEFORE_AND_REPLAY_STATE, + query = "SELECT cl " + + " FROM CommandLogEntry cl " + + " WHERE cl.executeIn = org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.FOREGROUND " + + " AND cl.timestamp < :to " + + " AND cl.replayState = :replayState " + + " ORDER BY cl.timestamp DESC"), @NamedQuery( name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES, query = "SELECT cl "