This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/main by this push:
new 350f629d651 CAUSEWAY-3883: re-implements async invocation
350f629d651 is described below
commit 350f629d6517fe67b527eb2f8fd9e4181ace2825
Author: Andi Huber <[email protected]>
AuthorDate: Thu Jun 26 11:43:48 2025 +0200
CAUSEWAY-3883: re-implements async invocation
---
api/applib/src/main/java/module-info.java | 1 -
.../applib/services/wrapper/WrapperFactory.java | 138 +++-----
.../services/wrapper/callable/AsyncCallable.java | 114 ------
.../services/wrapper/control/AsyncControl.java | 157 +++------
.../services/wrapper/control/SyncControl.java | 76 ++--
.../services/wrapper/events/ParseValueEvent.java | 62 ----
.../wrapper/control/AsyncControl_Test.java | 12 +-
.../services/wrapper/control/SyncControl_Test.java | 12 +-
.../_testing/WrapperFactory_forTesting.java | 10 +-
.../runtime/wrap/WrapperInvocationHandler.java | 9 +-
.../causeway/core/runtime/wrap/WrappingObject.java | 4 +-
.../wrapper/AsyncExecutorService.java | 134 +++++++
.../wrapper/AsyncProxyInternal.java | 51 +++
.../wrapper/WrapperFactoryDefault.java | 387 ++-------------------
.../wrapper/handlers/CommandRecord.java | 31 ++
.../wrapper/handlers/CommandRecordFactory.java | 52 +++
.../handlers/DomainObjectInvocationHandler.java | 80 +++--
.../wrapper/handlers/ProxyGenerator.java | 11 +-
.../wrapper/WrapperFactoryDefaultTest.java | 4 +-
.../ProxyCreatorTestUsingCodegenPlugin.java | 4 +-
.../applib/CausewayModuleExtCommandLogApplib.java | 4 +-
.../commandlog/applib/dom/BackgroundService.java | 189 +++-------
.../BackgroundService_IntegTestAbstract.java | 32 +-
.../integtest/CommandLog_IntegTestAbstract.java | 7 +-
.../applib/integtest/model/CounterRepository.java | 6 +-
.../jdo/publishing/PublishingTestFactoryJdo.java | 40 ++-
.../jpa/publishing/PublishingTestFactoryJpa.java | 47 +--
.../integtests/WrapperFactory_async_IntegTest.java | 24 +-
.../testdomain/interact/CommandArgumentTest.java | 14 +-
.../WrapperInteraction_Caching_IntegTest.java | 20 +-
30 files changed, 670 insertions(+), 1062 deletions(-)
diff --git a/api/applib/src/main/java/module-info.java
b/api/applib/src/main/java/module-info.java
index c6f6b2ea3f3..838b56ff3c1 100644
--- a/api/applib/src/main/java/module-info.java
+++ b/api/applib/src/main/java/module-info.java
@@ -109,7 +109,6 @@
exports org.apache.causeway.applib.services.userreg.events;
exports org.apache.causeway.applib.services.userreg;
exports org.apache.causeway.applib.services.userui;
- exports org.apache.causeway.applib.services.wrapper.callable;
exports org.apache.causeway.applib.services.wrapper.control;
exports org.apache.causeway.applib.services.wrapper.events;
exports org.apache.causeway.applib.services.wrapper.listeners;
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
index ef6403e74af..d64fd649f3d 100644
---
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
+++
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
@@ -19,89 +19,85 @@
package org.apache.causeway.applib.services.wrapper;
import java.util.List;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
import org.apache.causeway.applib.exceptions.recoverable.InteractionException;
import org.apache.causeway.applib.services.factory.FactoryService;
-import org.apache.causeway.applib.services.wrapper.callable.AsyncCallable;
import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.applib.services.wrapper.events.InteractionEvent;
import
org.apache.causeway.applib.services.wrapper.listeners.InteractionListener;
/**
- *
* Provides the ability to 'wrap' a domain object such that it can
* be interacted with while enforcing the hide/disable/validate rules implied
by
* the Apache Causeway programming model.
*
- * <p>
- * This capability goes beyond enforcing the (imperative) constraints within
+ * <p> This capability goes beyond enforcing the (imperative) constraints
within
* the `hideXxx()`, `disableXxx()` and `validateXxx()` supporting methods; it
* also enforces (declarative) constraints such as those represented by
* annotations, eg `@Parameter(maxLength=...)` or `@Property(mustSatisfy=...)`.
- * </p>
*
- * <p>
- * The wrapper can alternatively also be used to execute the action
+ * <p> The wrapper can alternatively also be used to execute the action
* asynchronously, through an {@link java.util.concurrent.ExecutorService}.
* Any business rules will be invoked synchronously beforehand, however.
- * </p>
*
- * <p>
- * The 'wrap' is a runtime-code-generated proxy that wraps the underlying
domain
+ * <p> The 'wrap' is a runtime-code-generated proxy that wraps the underlying
domain
* object. The wrapper can then be interacted with as follows:
* <ul>
- * <li>a <tt>get</tt> method for properties or collections</li>
- * <li>a <tt>set</tt> method for properties</li>
- * <li>any action</li>
+ * <li>a <tt>get</tt> method for properties or collections</li>
+ * <li>a <tt>set</tt> method for properties</li>
+ * <li>any action</li>
* </ul>
- * </p>
*
- * <p>
- * Calling any of the above methods may result in a (subclass of)
+ * <p> Calling any of the above methods may result in a (subclass of)
* {@link InteractionException} if the object disallows it. For example, if a
* property is annotated as hidden then a {@link HiddenException} will
* be thrown. Similarly if an action has a <tt>validate</tt> method and the
* supplied arguments are invalid then a {@link InvalidException} will be
* thrown.
- * </p>
*
- * <p>
- * In addition, the following methods may also be called:
+ * <p> In addition, the following methods may also be called:
* <ul>
- * <li>the <tt>title</tt> method</li>
- * <li>any <tt>defaultXxx</tt> or <tt>choicesXxx</tt> method</li>
+ * <li>the <tt>title</tt> method</li>
+ * <li>any <tt>defaultXxx</tt> or <tt>choicesXxx</tt> method</li>
* </ul>
- * </p>
*
- * <p>
- * If the object has (see {@link #isWrapper(Object)} already been wrapped),
+ * <p> If the object has (see {@link #isWrapper(Object)} already been wrapped),
* then should just return the object back unchanged.
- * </p>
*
- * @since 1.x {@index}
+ * @since 1.x revised for 3.4 {@index}
*/
public interface WrapperFactory {
+ /**
+ * @since 3.4 {@index}
+ * @see CompletableFuture
+ */
+ interface AsyncProxy<T> {
+ AsyncProxy<Void> thenAcceptAsync(Consumer<? super T> action);
+ <U> AsyncProxy<U> thenApplyAsync(Function<? super T, ? extends U> fn);
+ AsyncProxy<T> orTimeout(long timeout, TimeUnit unit);
+ T join();
+ }
+
/**
* Provides the "wrapper" of a domain object against which to
invoke the action.
*
- * <p>
- * The provided {@link SyncControl} determines whether business rules
are checked first, and conversely
- * whether the action is executed. There are therefore three typical
cases:
- * <ul>
- * <li>check rules, execute action</li>
- * <li>skip rules, execute action</li>
- * <li>check rules, skip action</li>
- * </ul>
- * <p>
- * The last logical option (skip rules, skip action) is valid but
doesn't make sense, as it's basically a no-op.
- * </p>
- * </p>
+ * <p>The provided {@link SyncControl} determines whether business rules
are checked first, and conversely
+ * whether the action is executed. There are therefore three typical
cases:
+ * <ul>
+ * <li>check rules, execute action</li>
+ * <li>skip rules, execute action</li>
+ * <li>check rules, skip action</li>
+ * </ul>
*
- * <p>
- * Otherwise, will do all the validations (raise exceptions as required
+ * <p>The last logical option (skip rules, skip action) is valid but
doesn't make sense, as it's basically a no-op.
+ *
+ * <p>Otherwise, will do all the validations (raise exceptions as required
* etc.), but doesn't modify the model.
*/
<T> T wrap(T domainObject,
@@ -177,50 +173,32 @@ default <T extends Mixin<MIXEE>, MIXEE> T
wrapMixinT(Class<T> mixinClass, MIXEE
//
/**
- * Returns a proxy object for the provided {@code domainObject},
- * through which can execute the action asynchronously (in another thread).
+ * Returns a {@link CompletableFuture} holding a proxy object for the
provided {@code domainObject},
+ * through which one can execute the action asynchronously (in another
thread).
*
* @param <T> - the type of the domain object
- * @param <R> - the type of the return of the action
* @param domainObject
* @param asyncControl
*
- * @since 2.0
+ * @since 3.4
*/
- <T,R> T asyncWrap(T domainObject,
- AsyncControl<R> asyncControl);
+ <T> AsyncProxy<T> asyncWrap(T domainObject, AsyncControl asyncControl);
/**
- * Returns a proxy object for the provided {@code mixinClass},
- * through which can execute the action asynchronously (in another thread).
+ * Returns a {@link CompletableFuture} holding a proxy object for the
provided {@code mixinClass},
+ * through which one can execute the action asynchronously (in another
thread).
*
- * @param <T>
+ * @param <T> - the type of the mixin
* @param mixinClass
* @param mixee
* @param asyncControl
*
- * @since 2.0
+ * @since 3.4
*/
- <T,R> T asyncWrapMixin(
- Class<T> mixinClass, Object mixee,
- AsyncControl<R> asyncControl);
-
- /**
- * Returns a proxy object for the provided {@code mixinClass},
- * through which can execute the action asynchronously (in another thread).
- *
- * @param <T>
- * @param mixinClass
- * @param mixee
- * @param asyncControl
- *
- * @since 2.0
- */
- default <T extends MIXEE,MIXEE, R> T asyncWrapMixinT(
- Class<T> mixinClass, MIXEE mixee,
- AsyncControl<R> asyncControl) {
- return asyncWrapMixin(mixinClass, mixee, asyncControl);
- }
+ <T> AsyncProxy<T> asyncWrapMixin(
+ Class<T> mixinClass,
+ Object mixee,
+ AsyncControl asyncControl);
//
// -- INTERACTION EVENT HANDLING
@@ -230,15 +208,13 @@ default <T extends MIXEE,MIXEE, R> T asyncWrapMixinT(
* All {@link InteractionListener}s that have been registered using
* {@link #addInteractionListener(InteractionListener)}.
*/
- // ...
List<InteractionListener> getListeners();
/**
* Registers an {@link InteractionListener}, to be notified of interactions
* on all wrappers.
*
- * <p>
- * This is retrospective: the listener will be notified of interactions
even
+ * <p> This is retrospective: the listener will be notified of
interactions even
* on wrappers created before the listener was installed. (From an
* implementation perspective this is because the wrappers delegate back to
* the container to fire the events).
@@ -251,8 +227,7 @@ default <T extends MIXEE,MIXEE, R> T asyncWrapMixinT(
* Remove an {@link InteractionListener}, to no longer be notified of
* interactions on wrappers.
*
- * <p>
- * This is retrospective: the listener will no longer be notified of any
+ * <p>This is retrospective: the listener will no longer be notified of any
* interactions created on any wrappers, not just on those wrappers created
* subsequently. (From an implementation perspective this is because the
* wrappers delegate back to the container to fire the events).
@@ -263,15 +238,4 @@ boolean removeInteractionListener(
InteractionListener listener);
void notifyListeners(InteractionEvent ev);
-
- //
- // -- SPI for ExecutorServices
- //
-
- /**
- * Provides a mechanism for custom implementations of {@link
java.util.concurrent.ExecutorService}, as installed
- * using {@link AsyncControl#with(ExecutorService)}, to actually execute
the {@link AsyncCallable} that they
- * are passed initially during {@link WrapperFactory#asyncWrap(Object,
AsyncControl)} and its brethren.
- */
- <R> R execute(AsyncCallable<R> asyncCallable);
}
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/callable/AsyncCallable.java
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/callable/AsyncCallable.java
deleted file mode 100644
index a332fcebe01..00000000000
---
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/callable/AsyncCallable.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.applib.services.wrapper.callable;
-
-import java.io.Serializable;
-import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-
-import org.springframework.transaction.annotation.Propagation;
-
-import org.apache.causeway.applib.services.command.Command;
-import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
-import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
-import org.apache.causeway.schema.cmd.v2.CommandDto;
-
-/**
- * Provides access to the details of the asynchronous callable (representing a
child command to be executed
- * asynchronously) when using
- * {@link
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object,
AsyncControl)} and its brethren.
- *
- * <p>
- * To explain in a little more depth; we can execute commands (actions
etc) asynchronously using
- * {@link
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object,
AsyncControl)} or similar.
- * The {@link AsyncControl} parameter allows various aspects of this to be
controlled, one such being the
- * implementation of the {@link java.util.concurrent.ExecutorService}
(using
- * {@link AsyncControl#with(ExecutorService)}).
- * </p>
- *
- * <p>
- * The default {@link ExecutorService} is just {@link
java.util.concurrent.ForkJoinPool}, and this and similar
- * implementations will hold the provided callable in memory and execute
it in due course. For these out-of-the-box
- * implementations, the {@link java.util.concurrent.Callable} is a black
box and they have no need to look inside
- * it. So long as the implementation of the Callable is not serialized
then deserialized (ie is only ever held in
- * memory), then all will work fine.
- * </p>
- *
- * <p>
- * This interface, though, is intended to expose the details of the passed
{@link java.util.concurrent.Callable},
- * most notably the {@link CommandDto} to be executed. The main use case
this supports is to allow a custom
- * implementation of {@link ExecutorService} to be provided that could do
more sophisticated things, for example
- * persisting the callable somewhere, either exploiting the fact that the
object is serializable, or perhaps by
- * unpacking the parts and persisting (for example, as a
<code>CommandLogEntry</code> courtesy of the
- * commandlog extension).
- * </p>
- *
- * <p>
- * These custom implementations of {@link ExecutorService} must however
reinitialize the state of the callable,
- * either by injecting in services using {@link
org.apache.causeway.applib.services.inject.ServiceInjector} and then
- * just <code>call()</code>ing it, or alternatively and more
straightforwardly simply executing it using
- * {@link
org.apache.causeway.applib.services.wrapper.WrapperFactory#execute(AsyncCallable)}.
- * </p>
- *
- * @since 2.0 {@index}
- */
-public interface AsyncCallable<R> extends Serializable, Callable<R> {
-
- /**
- * The requested {@link InteractionContext} to execute the command, as
inferred from the {@link AsyncControl}
- * that was used to call
- * {@link
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object,
AsyncControl)} and its ilk.
- */
- InteractionContext getInteractionContext();
-
- /**
- * The transaction propagation to use when creating a new {@link
org.apache.causeway.applib.services.iactn.Interaction}
- * in which to execute the child command.
- */
- Propagation getPropagation();
-
- /**
- * Details of the actual child command (action or property edit) to be
performed.
- *
- * <p>
- * (Ultimately this is handed onto the {@link
org.apache.causeway.applib.services.command.CommandExecutorService}).
- * </p>
- */
- CommandDto getCommandDto();
-
- /**
- * The type of the object returned by the child command once finally
executed.
- */
- Class<R> getReturnType();
-
- /**
- * The unique {@link Command#getInteractionId() interactionId} of the
parent {@link Command}, which is to say the
- * {@link Command} that was active in the original interaction where
- * {@link
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object,
AsyncControl)} (or its brethren)
- * was called.
- *
- * <p>
- * This can be useful for custom implementations of {@link
ExecutorService} that use the commandlog
- * extension's <code>CommandLogEntry</code>, to link parent and child
commands together.
- * </p>
- */
- UUID getParentInteractionId();
-
-}
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl.java
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl.java
index e59fdeef199..9086c945f2d 100644
---
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl.java
+++
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl.java
@@ -20,23 +20,18 @@
import java.time.ZoneId;
import java.util.Locale;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.apache.causeway.applib.clock.VirtualClock;
+import org.apache.causeway.applib.locale.UserLocale;
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
import org.apache.causeway.applib.services.user.UserMemento;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
-import org.apache.causeway.commons.internal.assertions._Assert;
-import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
/**
@@ -55,11 +50,9 @@
* @since 2.0 {@index}
*/
@Log4j2
-public record AsyncControl<R>(
- Class<R> returnType,
+public record AsyncControl (
SyncControl syncControl,
@Nullable ExecutorService executorService,
-
/**
* Defaults to the system clock, if not overridden
*/
@@ -78,67 +71,37 @@ public record AsyncControl<R>(
*
* <p>If not specified, then the user of the current foreground
session is used.
*/
- @Nullable UserMemento user,
- /**
- * Contains the result of the invocation.
- *
- * <p> If an entity is returned, then the object is automatically
detached
- * because the persistence session within which it was obtained will
have
- * been closed already.
- */
- AtomicReference<Future<R>> futureRef) {
-
- /**
- * Factory method to instantiate a control instance for a void action
- * or a property edit (where there is no need or intention to provide a
- * return value through the `Future`).
- */
- public static AsyncControl<Void> returningVoid() {
- return new AsyncControl<>(Void.class);
- }
-
- /**
- * Factory method to instantiate for a control instance for an action
- * returning a value of `<R>` (where this value will be returned through
- * the `Future`).
- */
- public static <X> AsyncControl<X> returning(final Class<X> cls) {
- return new AsyncControl<X>(cls);
- }
+ @Nullable UserMemento user
+ ) {
- // non canonical constructor
- private AsyncControl(final Class<R> returnType) {
- this(returnType,
- SyncControl.control(),
- /*executorService*/null, /*clock*/null, /*locale*/null,
/*timeZone*/null, /*user*/null,
- new AtomicReference<>());
+ public static AsyncControl defaults() {
+ return new AsyncControl(SyncControl.defaults(),
+ /*executorService*/null,
+ /*clock*/null, /*locale*/null, /*timeZone*/null, /*user*/null);
}
/**
* Explicitly set the action to be executed.
*/
- public AsyncControl<R> withExecute() {
- return new AsyncControl<>(returnType, syncControl.withExecute(),
executorService, clock, locale, timeZone, user, futureRef);
+ public AsyncControl withExecute() {
+ return new AsyncControl(syncControl.withExecute(), executorService,
clock, locale, timeZone, user);
}
-
/**
- * Explicitly set the action to <i>not</i >be executed, in other words a
- * "dry run".
+ * Explicitly set the action to <i>not</i >be executed, in other words a
'dry run'.
*/
- public AsyncControl<R> withNoExecute() {
- return new AsyncControl<>(returnType, syncControl.withExecute(),
executorService, clock, locale, timeZone, user, futureRef);
+ public AsyncControl withNoExecute() {
+ return new AsyncControl(syncControl.withNoExecute(), executorService,
clock, locale, timeZone, user);
}
/**
* Skip checking business rules (hide/disable/validate) before
* executing the underlying property or action
*/
- public AsyncControl<R> withSkipRules() {
- return new AsyncControl<>(returnType, syncControl.withSkipRules(),
executorService, clock, locale, timeZone, user, futureRef);
+ public AsyncControl withSkipRules() {
+ return new AsyncControl(syncControl.withSkipRules(), executorService,
clock, locale, timeZone, user);
}
-
- public AsyncControl<R> withCheckRules() {
- return new AsyncControl<>(returnType, syncControl.withCheckRules(),
executorService, clock, locale, timeZone, user, futureRef);
+ public AsyncControl withCheckRules() {
+ return new AsyncControl(syncControl.withCheckRules(), executorService,
clock, locale, timeZone, user);
}
/**
@@ -148,7 +111,7 @@ public AsyncControl<R> withCheckRules() {
*
* <p>Changes are made in place, returning the same instance.
*/
- public AsyncControl<R> setExceptionHandler(final @NonNull ExceptionHandler
exceptionHandler) {
+ public AsyncControl setExceptionHandler(final @NonNull ExceptionHandler
exceptionHandler) {
syncControl.setExceptionHandler(exceptionHandler);
return this;
}
@@ -162,29 +125,33 @@ public AsyncControl<R> setExceptionHandler(final @NonNull
ExceptionHandler excep
*
* @param executorService - null-able
*/
- public AsyncControl<R> with(final ExecutorService executorService) {
- return new AsyncControl<>(returnType, syncControl, executorService,
clock, locale, timeZone, user, futureRef);
+ public AsyncControl with(final ExecutorService executorService) {
+ return new AsyncControl(syncControl, executorService, clock, locale,
timeZone, user);
+ }
+
+ public AsyncControl listen(final SyncControl.@NonNull CommandListener
commandListener) {
+ return new AsyncControl(syncControl.listen(commandListener),
executorService, clock, locale, timeZone, user);
}
/**
* Defaults to the system clock, if not overridden
*/
- public AsyncControl<R> withClock(final @NonNull VirtualClock clock) {
- return new AsyncControl<>(returnType, syncControl, executorService,
clock, locale, timeZone, user, futureRef);
+ public AsyncControl withClock(final @NonNull VirtualClock clock) {
+ return new AsyncControl(syncControl, executorService, clock, locale,
timeZone, user);
}
/**
* Defaults to the system locale, if not overridden
*/
- public AsyncControl<R> withLocale(final @NonNull Locale locale) {
- return new AsyncControl<>(returnType, syncControl, executorService,
clock, locale, timeZone, user, futureRef);
+ public AsyncControl withLocale(final @NonNull Locale locale) {
+ return new AsyncControl(syncControl, executorService, clock, locale,
timeZone, user);
}
/**
* Defaults to the system time zone, if not overridden
*/
- public AsyncControl<R> withTimeZone(final @NonNull ZoneId timeZone) {
- return new AsyncControl<>(returnType, syncControl, executorService,
clock, locale, timeZone, user, futureRef);
+ public AsyncControl withTimeZone(final @NonNull ZoneId timeZone) {
+ return new AsyncControl(syncControl, executorService, clock, locale,
timeZone, user);
}
/**
@@ -193,58 +160,18 @@ public AsyncControl<R> withTimeZone(final @NonNull ZoneId
timeZone) {
*
* <p>If not specified, then the user of the current foreground session is
used.
*/
- public AsyncControl<R> withUser(final @NonNull UserMemento user) {
- return new AsyncControl<>(returnType, syncControl, executorService,
clock, locale, timeZone, user, futureRef);
- }
-
- public Future<R> future() {
- return futureRef.get();
+ public AsyncControl withUser(final @NonNull UserMemento user) {
+ return new AsyncControl(syncControl, executorService, clock, locale,
timeZone, user);
}
- /**
- * Waits on the callers thread, for a maximum amount of time,
- * for the result of the invocation to become available.
- * @param timeout the maximum time to wait
- * @param unit the time unit of the {@code timeout} argument
- * @return the invocation result
- * @throws CancellationException if the computation was cancelled
- * @throws ExecutionException if the computation threw an exception
- * @throws InterruptedException if the current thread was interrupted
while waiting
- * @throws TimeoutException if the wait timed out
- */
- @SuppressWarnings("javadoc")
- @SneakyThrows
- public R waitForResult(final long timeout, final TimeUnit unit) {
- _Assert.assertNotNull(future(),
- ()->"detected call to waitForResult(..) before future was
set");
- return future().get(timeout, unit);
+ public InteractionContext override(
+ final InteractionContext interactionContext) {
+ return InteractionContext.builder()
+
.clock(Optional.ofNullable(clock()).orElseGet(interactionContext::getClock))
+
.locale(Optional.ofNullable(locale()).map(UserLocale::valueOf).orElse(null)) //
if not set in asyncControl use defaults (set override to null)
+
.timeZone(Optional.ofNullable(timeZone()).orElseGet(interactionContext::getTimeZone))
+
.user(Optional.ofNullable(user()).orElseGet(interactionContext::getUser))
+ .build();
}
- // -- DEPRECATIONS
-
- @Deprecated public Class<R> getReturnType() { return returnType(); }
- @Deprecated public ExecutorService getExecutorService() { return
executorService(); }
-
- /**
- * Defaults to the system clock, if not overridden
- */
- @Deprecated public VirtualClock getClock() { return clock(); }
- /**
- * Defaults to the system locale, if not overridden
- */
- @Deprecated public Locale getLocale() { return locale(); }
- /**
- * Defaults to the system time zone, if not overridden
- */
- @Deprecated public ZoneId getTimeZone() { return timeZone(); }
- /**
- * Specifies the user for the session used to execute the command
- * asynchronously, in the background.
- *
- * <p>If not specified, then the user of the current foreground session is
used.
- */
- @Deprecated public UserMemento getUser() { return user(); }
-
- @Deprecated public Future<R> getFuture() { return future(); }
-
}
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
index 38d3f235a10..ac826d8a126 100644
---
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
+++
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
@@ -18,10 +18,15 @@
*/
package org.apache.causeway.applib.services.wrapper.control;
+import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.jspecify.annotations.Nullable;
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
+
import lombok.NonNull;
/**
@@ -30,63 +35,80 @@
* @since 2.0 revised for 3.4 {@index}
*/
public record SyncControl(
+ /**
+ * Skip checking business rules (hide/disable/validate) before
+ * executing the underlying property or action
+ */
+ boolean isSkipRules,
+ boolean isSkipExecute,
+ /**
+ * Get notified on action invocation or property change.
+ */
+ Can<CommandListener> commandListeners,
/**
* How to handle exceptions if they occur, using the provided
* {@link ExceptionHandler}.
*
* <p>The default behaviour is to rethrow the exception.
*/
- AtomicReference<ExceptionHandler> exceptionHandlerRef,
- boolean isSkipExecute,
- /**
- * Skip checking business rules (hide/disable/validate) before
- * executing the underlying property or action
- */
- boolean isSkipRules) {
+ AtomicReference<ExceptionHandler> exceptionHandlerRef) {
- public static SyncControl control() {
- return new SyncControl(null, false, false);
+ @FunctionalInterface
+ public interface CommandListener {
+ public void onCommand(
+ InteractionContext interactionContext,
+ CommandDto commandDto,
+ UUID parentInteractionId);
+ }
+
+ public static SyncControl defaults() {
+ return new SyncControl(false, false, null, null);
}
public SyncControl(
- @Nullable AtomicReference<ExceptionHandler> exceptionHandlerRef,
+ boolean isSkipRules,
boolean isSkipExecute,
- boolean isSkipRules) {
+ @Nullable Can<CommandListener> commandListeners,
+ @Nullable AtomicReference<ExceptionHandler> exceptionHandlerRef) {
+ this.isSkipRules = isSkipRules;
+ this.isSkipExecute = isSkipExecute;
+ this.commandListeners = commandListeners!=null
+ ? commandListeners
+ : Can.empty();
this.exceptionHandlerRef = exceptionHandlerRef!=null
? exceptionHandlerRef
: new AtomicReference<>();
- this.isSkipExecute = isSkipExecute;
- this.isSkipRules = isSkipRules;
if(this.exceptionHandlerRef.get()==null) {
this.exceptionHandlerRef.set(exception -> { throw exception; });
}
}
/**
- * Explicitly set the action to be executed.
+ * Skip checking business rules (hide/disable/validate) before
+ * executing the underlying property or action
*/
- public SyncControl withExecute() {
- return new SyncControl(exceptionHandlerRef, false, isSkipRules);
+ public SyncControl withSkipRules() {
+ return new SyncControl(true, isSkipExecute, commandListeners,
exceptionHandlerRef);
+ }
+ public SyncControl withCheckRules() {
+ return new SyncControl(false, isSkipExecute, commandListeners,
exceptionHandlerRef);
}
/**
- * Explicitly set the action to <i>not</i >be executed, in other words a
- * "dry run".
+ * Explicitly set the action to be executed.
*/
- public SyncControl withNoExecute() {
- return new SyncControl(exceptionHandlerRef, true, isSkipRules);
+ public SyncControl withExecute() {
+ return new SyncControl(isSkipRules, false, commandListeners,
exceptionHandlerRef);
}
-
/**
- * Skip checking business rules (hide/disable/validate) before
- * executing the underlying property or action
+ * Explicitly set the action to <i>not</i> be executed, in other words a
'dry run'.
*/
- public SyncControl withSkipRules() {
- return new SyncControl(exceptionHandlerRef, isSkipExecute, true);
+ public SyncControl withNoExecute() {
+ return new SyncControl(isSkipRules, true, commandListeners,
exceptionHandlerRef);
}
- public SyncControl withCheckRules() {
- return new SyncControl(exceptionHandlerRef, isSkipExecute, false);
+ public SyncControl listen(@NonNull CommandListener commandListener) {
+ return new SyncControl(isSkipRules, isSkipExecute,
commandListeners.add(commandListener), exceptionHandlerRef);
}
/**
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/events/ParseValueEvent.java
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/events/ParseValueEvent.java
deleted file mode 100644
index 25b6007432b..00000000000
---
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/events/ParseValueEvent.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.applib.services.wrapper.events;
-
-import org.apache.causeway.applib.Identifier;
-import org.apache.causeway.applib.services.wrapper.WrapperFactory;
-
-/**
- * Supported only by {@link WrapperFactory},
- * represents a check as to whether the proposed values of the value type is
valid.
- * <p>
- * If {@link #getReason()} is not <tt>null</tt> then provides the reason why
the
- * proposed value is invalid, otherwise the new value is acceptable.
- *
- * @since 1.x {@index}
- */
-@Deprecated // not used
-public class ParseValueEvent extends ValidityEvent {
-
- private static Object coalesce(final Object source, final String proposed)
{
- return source != null ? source : proposed;
- }
-
- private final String proposed;
-
- public ParseValueEvent(final Object source, final Identifier
classIdentifier, final String proposed) {
- super(coalesce(source, proposed), classIdentifier);
- this.proposed = proposed;
- }
-
- /**
- * Will be the source provided in the
- * {@link #ParseValueEvent(Object, Identifier, String) constructor} if not
- * null, otherwise will fallback to the proposed value.
- */
- @Override
- public Object getSource() {
- return super.getSource();
- }
-
- @Override
- public String getProposed() {
- return proposed;
- }
-
-}
diff --git
a/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl_Test.java
b/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl_Test.java
index 6b6d5635864..a5f9a75b633 100644
---
a/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl_Test.java
+++
b/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/AsyncControl_Test.java
@@ -34,7 +34,7 @@ class AsyncControl_Test {
public void defaults() throws Exception {
// given
- var control = AsyncControl.returningVoid();
+ var control = AsyncControl.defaults();
// then
Assertions.assertThat(control.syncControl().isSkipExecute()).isEqualTo(false);
@@ -44,7 +44,7 @@ public void defaults() throws Exception {
@Test
public void check_rules() throws Exception {
// given
- var control = AsyncControl.returningVoid();
+ var control = AsyncControl.defaults();
// when
control = control.withCheckRules();
@@ -58,7 +58,7 @@ public void check_rules() throws Exception {
public void skip_rules() throws Exception {
// given
- var control = AsyncControl.returningVoid();
+ var control = AsyncControl.defaults();
// when
control = control.withSkipRules();
@@ -72,7 +72,7 @@ public void skip_rules() throws Exception {
public void user() throws Exception {
// given
- var control = AsyncControl.returningVoid();
+ var control = AsyncControl.defaults();
// when
control = control.withUser(UserMemento.ofName("fred"));
@@ -85,7 +85,7 @@ public void user() throws Exception {
public void roles() throws Exception {
// given
- var control = AsyncControl.returningVoid();
+ var control = AsyncControl.defaults();
// when
control = control.withUser(UserMemento.ofNameAndRoleNames("fred",
"role-1", "role-2"));
@@ -101,7 +101,7 @@ public void chaining() throws Exception {
var executorService = new ExecutorServiceAdapter(new
TaskExecutorAdapter(command -> {}));
var exceptionHandler = (ExceptionHandler) ex -> null;
- var control = AsyncControl.returning(String.class)
+ var control = AsyncControl.defaults()
.withSkipRules()
.withUser(UserMemento.ofNameAndRoleNames("fred", "role-1",
"role-2"))
.with(executorService)
diff --git
a/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/SyncControl_Test.java
b/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/SyncControl_Test.java
index fc98b02e18f..051b7727d02 100644
---
a/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/SyncControl_Test.java
+++
b/api/applib/src/test/java/org/apache/causeway/applib/services/wrapper/control/SyncControl_Test.java
@@ -29,7 +29,7 @@ class SyncControl_Test {
public void defaults() throws Exception {
// given
- var control = SyncControl.control();
+ var control = SyncControl.defaults();
// then
assertFalse(control.isSkipExecute());
@@ -39,7 +39,7 @@ public void defaults() throws Exception {
@Test
public void check_rules() throws Exception {
// given
- var control = SyncControl.control();
+ var control = SyncControl.defaults();
// when
control = control.withCheckRules();
@@ -53,7 +53,7 @@ public void check_rules() throws Exception {
public void skip_rules() throws Exception {
// given
- var control = SyncControl.control();
+ var control = SyncControl.defaults();
// when
control = control.withSkipRules();
@@ -67,7 +67,7 @@ public void skip_rules() throws Exception {
public void execute() throws Exception {
// given
- var control = SyncControl.control();
+ var control = SyncControl.defaults();
// when
control = control.withExecute();
@@ -81,7 +81,7 @@ public void execute() throws Exception {
public void no_execute() throws Exception {
// given
- var control = SyncControl.control();
+ var control = SyncControl.defaults();
// when
control = control.withNoExecute();
@@ -96,7 +96,7 @@ public void chaining() throws Exception {
ExceptionHandler exceptionHandler = ex -> null;
- var control = SyncControl.control()
+ var control = SyncControl.defaults()
.withNoExecute()
.withSkipRules()
.setExceptionHandler(exceptionHandler);
diff --git
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/_testing/WrapperFactory_forTesting.java
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/_testing/WrapperFactory_forTesting.java
index 83f56c2862b..cd5642d074f 100644
---
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/_testing/WrapperFactory_forTesting.java
+++
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/_testing/WrapperFactory_forTesting.java
@@ -21,7 +21,6 @@
import java.util.List;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
-import org.apache.causeway.applib.services.wrapper.callable.AsyncCallable;
import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.applib.services.wrapper.events.InteractionEvent;
@@ -61,12 +60,12 @@ public <T> boolean isWrapper(T possibleWrappedDomainObject)
{
}
@Override
- public <T, R> T asyncWrap(T domainObject, AsyncControl<R> asyncControl) {
+ public <T> AsyncProxy<T> asyncWrap(T domainObject, AsyncControl
asyncControl) {
return null;
}
@Override
- public <T, R> T asyncWrapMixin(Class<T> mixinClass, Object mixedIn,
AsyncControl<R> asyncControl) {
+ public <T> AsyncProxy<T> asyncWrapMixin(Class<T> mixinClass, Object mixee,
AsyncControl asyncControl) {
return null;
}
@@ -89,9 +88,4 @@ public boolean removeInteractionListener(InteractionListener
listener) {
public void notifyListeners(InteractionEvent ev) {
}
- @Override
- public <R> R execute(AsyncCallable<R> asyncCallable) {
- return null;
- }
-
}
diff --git
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
index 90fd46f407a..63c08567b29 100644
---
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
+++
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
@@ -24,6 +24,7 @@
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
+import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.commons.internal._Constants;
import org.apache.causeway.commons.internal.base._Lazy;
import org.apache.causeway.commons.internal.proxy.CachableInvocationHandler;
@@ -117,12 +118,8 @@ static WrapperInvocation of(Object target, Method method,
Object[] args) {
return new WrapperInvocation(origin, method, args!=null ? args :
_Constants.emptyObjects);
}
- public boolean shouldEnforceRules() {
- return !origin().syncControl().isSkipRules();
- }
-
- public boolean shouldExecute() {
- return !origin().syncControl().isSkipExecute();
+ public SyncControl syncControl() {
+ return origin().syncControl();
}
}
diff --git
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
index dec7ce86e0b..aa251e4d809 100644
---
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
+++
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
@@ -63,13 +63,13 @@ record Origin(
* fallback, used for non-proxied target, with no execute (no verify
no rule checking).
*/
public static Origin fallback(Object target) {
- return new Origin(target, null,
SyncControl.control().withNoExecute(), true);
+ return new Origin(target, null,
SyncControl.defaults().withNoExecute(), true);
}
/**
* fallback, used for non-proxied target as mixin, with no execute (no
verify no rule checking)
*/
public static Origin fallbackMixin(Object target, ManagedObject
managedMixee) {
- return new Origin(target, managedMixee,
SyncControl.control().withNoExecute(), true);
+ return new Origin(target, managedMixee,
SyncControl.defaults().withNoExecute(), true);
}
public Origin(Object pojo, SyncControl syncControl) {
this(pojo, null, syncControl, false);
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
new file mode 100644
index 00000000000..886e61edafe
--- /dev/null
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
@@ -0,0 +1,134 @@
+/*
+ * 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.core.runtimeservices.wrapper;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.springframework.transaction.annotation.Propagation;
+
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.apache.causeway.commons.functional.ThrowingRunnable;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+
+record AsyncExecutorService(
+ InteractionService interactionService,
+ TransactionService transactionService,
+ InteractionContext interactionContext,
+ /**
+ * If empty then executes non-transactionally, similar to {@link
Propagation#NEVER},
+ * but does NOT throw any exceptions if a transaction exists.
+ */
+ Optional<Propagation> propagation,
+ ExecutorService delegate) implements ExecutorService {
+
+ @Override
+ public <T> Future<T> submit(final Callable<T> task) {
+ return delegate.submit(()->call(task));
+ }
+
+ @Override
+ public <T> Future<T> submit(final Runnable task, final T result) {
+ return delegate.submit(()->run(task::run), result);
+ }
+
+ @Override
+ public Future<?> submit(final Runnable task) {
+ return delegate.submit(()->run(task::run));
+ }
+
+ @Override
+ public void execute(final Runnable command) {
+ delegate.execute(()->run(command::run));
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks) throws InterruptedException {
+ throw _Exceptions.unsupportedOperation(); //return
delegate.invokeAll(tasks);
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks, final long timeout, final TimeUnit unit) throws
InterruptedException {
+ throw _Exceptions.unsupportedOperation(); //return
delegate.invokeAll(tasks, timeout, unit);
+ }
+
+ @Override
+ public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
+ throw _Exceptions.unsupportedOperation(); //return
delegate.invokeAny(tasks);
+ }
+
+ @Override
+ public <T> T invokeAny(final Collection<? extends Callable<T>> tasks,
final long timeout, final TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
+ throw _Exceptions.unsupportedOperation(); //return
delegate.invokeAny(tasks, timeout, unit);
+ }
+
+ @Override
+ public void shutdown() {
+ delegate.shutdown();
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ return delegate.shutdownNow();
+ }
+
+ @Override
+ public boolean awaitTermination(final long timeout, final TimeUnit unit)
throws InterruptedException {
+ return delegate.awaitTermination(timeout, unit);
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return delegate.isShutdown();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return delegate.isTerminated();
+ }
+
+ // -- HELPER
+
+ private void run(ThrowingRunnable runnable) {
+ if(propagation.isEmpty())
+ interactionService.run(interactionContext, runnable);
+ else
+ interactionService.run(interactionContext, ()->transactionService
+ .runTransactional(propagation().get(), runnable)
+ .ifFailureFail());
+ }
+
+ private <T> T call(Callable<T> callable) {
+ return propagation.isEmpty()
+ ? interactionService.call(interactionContext, callable)
+ : interactionService.call(interactionContext,
()->transactionService
+ .callTransactional(propagation().get(), callable)
+ .valueAsNullableElseFail());
+ }
+
+}
\ No newline at end of file
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
new file mode 100644
index 00000000000..a2f3a8a67aa
--- /dev/null
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
@@ -0,0 +1,51 @@
+/*
+ * 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.core.runtimeservices.wrapper;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
+
+//TODO this is just a proof of concept; chaining makes non sense once future
is no longer a proxy
+record AsyncProxyInternal<T>(CompletableFuture<T> future, AsyncExecutorService
executor) implements AsyncProxy<T> {
+ @Override public AsyncProxy<Void> thenAcceptAsync(Consumer<? super T>
action) {
+ return map(in->in.thenAcceptAsync(action, executor));
+ }
+
+ @Override public <U> AsyncProxy<U> thenApplyAsync(Function<? super T, ?
extends U> fn) {
+ return map(in->in.thenApplyAsync(fn, executor));
+ }
+
+ @Override public AsyncProxy<T> orTimeout(long timeout, TimeUnit unit) {
+ return map(in->in.orTimeout(timeout, unit));
+ }
+
+ @Override public T join() {
+ return future.join();
+ }
+
+ // -- HELPER
+
+ private <U> AsyncProxy<U> map(Function<CompletableFuture<T>,
CompletableFuture<U>> fn) {
+ return new AsyncProxyInternal<>(fn.apply(future), executor);
+ }
+}
\ No newline at end of file
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index b155151ec7c..359e28aeba5 100644
---
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -18,15 +18,13 @@
*/
package org.apache.causeway.core.runtimeservices.wrapper;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiConsumer;
@@ -46,21 +44,11 @@
import org.springframework.transaction.annotation.Propagation;
import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.locale.UserLocale;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.applib.services.bookmark.BookmarkService;
-import org.apache.causeway.applib.services.command.CommandExecutorService;
import org.apache.causeway.applib.services.factory.FactoryService;
-import org.apache.causeway.applib.services.iactn.InteractionProvider;
import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
-import org.apache.causeway.applib.services.iactnlayer.InteractionLayer;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
-import org.apache.causeway.applib.services.inject.ServiceInjector;
-import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
-import org.apache.causeway.applib.services.wrapper.callable.AsyncCallable;
import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
-import org.apache.causeway.applib.services.wrapper.control.AsyncLogger;
import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.applib.services.wrapper.events.ActionArgumentEvent;
import
org.apache.causeway.applib.services.wrapper.events.ActionInvocationEvent;
@@ -80,40 +68,24 @@
import
org.apache.causeway.applib.services.wrapper.listeners.InteractionListener;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
-import org.apache.causeway.commons.internal.reflection._GenericResolver;
-import
org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
-import
org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MixinConstructor;
import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
-import org.apache.causeway.core.metamodel.interactions.InteractionHead;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory;
-import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
-import org.apache.causeway.core.metamodel.spec.feature.MixedInMember;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
-import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
-import
org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler.WrapperInvocation;
import org.apache.causeway.core.runtime.wrap.WrappingObject;
import
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
import
org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcher;
import
org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcherTypeSafe;
import
org.apache.causeway.core.runtimeservices.wrapper.handlers.ProxyGenerator;
-import org.apache.causeway.schema.cmd.v2.CommandDto;
-
-import static
org.apache.causeway.applib.services.wrapper.control.SyncControl.control;
import lombok.Getter;
-import lombok.RequiredArgsConstructor;
/**
* Default implementation of {@link WrapperFactory}.
- *
- * @since 2.0 {@index}
*/
@Service
@Named(WrapperFactoryDefault.LOGICAL_TYPE_NAME)
@@ -131,10 +103,6 @@ public class WrapperFactoryDefault
@Inject private Provider<InteractionService> interactionServiceProvider;
@Inject private Provider<TransactionService> transactionServiceProvider;
- @Inject private Provider<CommandExecutorService>
commandExecutorServiceProvider;
- @Inject private Provider<InteractionProvider> interactionProviderProvider;
- @Inject private Provider<BookmarkService> bookmarkServiceProvider;
- @Inject private Provider<RepositoryService> repositoryServiceProvider;
@Inject private InteractionIdGenerator interactionIdGenerator;
private final List<InteractionListener> listeners = new ArrayList<>();
@@ -149,7 +117,7 @@ public void init() {
this.commonExecutorService = newCommonExecutorService();
- this.proxyGenerator = new ProxyGenerator(proxyFactoryService);
+ this.proxyGenerator = new ProxyGenerator(proxyFactoryService,
interactionIdGenerator);
putDispatcher(ObjectTitleEvent.class,
InteractionListener::objectTitleRead);
putDispatcher(PropertyVisibilityEvent.class,
InteractionListener::propertyVisible);
@@ -180,7 +148,7 @@ public void close() {
@Override
public <T> T wrap(
final @NonNull T domainObject) {
- return wrap(domainObject, control());
+ return wrap(domainObject, SyncControl.defaults());
}
@Override
@@ -210,7 +178,7 @@ public <T> T wrap(
public <T> T wrapMixin(
final @NonNull Class<T> mixinClass,
final @NonNull Object mixee) {
- return wrapMixin(mixinClass, mixee, control());
+ return wrapMixin(mixinClass, mixee, SyncControl.defaults());
}
@Override
@@ -267,242 +235,33 @@ public <T> T unwrap(final T possibleWrappedDomainObject)
{
// -- ASYNC WRAPPING
- @Override
- public <T, R> T asyncWrap(
- final @NonNull T domainObject,
- final AsyncControl<R> asyncControl) {
-
- var targetAdapter =
adaptAndGuardAgainstWrappingNotSupported(domainObject);
- if(targetAdapter.objSpec().isMixin()) {
- throw _Exceptions.illegalArgument("cannot wrap a mixin instance
directly, "
- + "use WrapperFactory.asyncWrapMixin(...) instead");
- }
-
- var handler = (InvocationHandler) (proxy, method, args) -> {
- var resolvedMethod = _GenericResolver.resolveMethod(method,
domainObject.getClass())
- .orElseThrow(); // fail early on attempt to invoke method
that is not part of the meta-model
-
- if (isInheritedFromJavaLangObject(method)) {
- return method.invoke(domainObject, args);
- }
-
- if (!asyncControl.syncControl().isSkipRules()) {
- var doih = proxyGenerator.handler(targetAdapter.objSpec());
- var origin = WrappingObject.Origin.fallback(domainObject);
- doih.invoke(new WrapperInvocation(origin, method, args));
- }
-
- var memberAndTarget = MemberAndTarget.forRegular(resolvedMethod,
targetAdapter);
- if(!memberAndTarget.isMemberFound()) {
- return method.invoke(domainObject, args);
- }
-
- submitAsync(memberAndTarget, args, asyncControl);
- return null;
- };
+ AsyncExecutorService asyncExecutorService(AsyncControl asyncControl) {
+ return new AsyncExecutorService(
+ interactionServiceProvider.get(),
+ transactionServiceProvider.get(),
+ asyncControl.override(InteractionContext.builder().build()),
+ Optional.of(Propagation.REQUIRES_NEW),
+ Optional.ofNullable(asyncControl.executorService())
+ .orElse(commonExecutorService));
+ }
- @SuppressWarnings("unchecked")
- var proxyClass = proxyFactoryService
- .proxyClass(handler,
- (Class<T>)domainObject.getClass(),
WrappingObject.class, WrappingObject.ADDITIONAL_FIELDS);
- var proxyFactory = proxyFactoryService.factory(proxyClass);
- return proxyFactory.createInstance(false);
+ @Override
+ public <T> AsyncProxy<T> asyncWrap(T domainObject, AsyncControl
asyncControl) {
+ var proxy = wrap(domainObject, asyncControl.syncControl());
+ return new AsyncProxyInternal<>(
+ CompletableFuture.completedFuture(proxy),
+ asyncExecutorService(asyncControl));
}
@Override
- public <T, R> T asyncWrapMixin(
+ public <T> AsyncProxy<T> asyncWrapMixin(
final @NonNull Class<T> mixinClass,
final @NonNull Object mixee,
- final @NonNull AsyncControl<R> asyncControl) {
-
- T mixin = factoryService.mixin(mixinClass, mixee);
-
- var managedMixee = adaptAndGuardAgainstWrappingNotSupported(mixee);
- var managedMixin = adaptAndGuardAgainstWrappingNotSupported(mixin);
-
- var mixinConstructor =
MixinConstructor.PUBLIC_SINGLE_ARG_RECEIVING_MIXEE
- .getConstructorElseFail(mixinClass, mixee.getClass());
-
- var handler = (InvocationHandler) (proxy, method, args) -> {
- var resolvedMethod = _GenericResolver.resolveMethod(method,
mixinClass)
- .orElseThrow(); // fail early on attempt to invoke method
that is not part of the meta-model
-
- if (isInheritedFromJavaLangObject(method)) {
- return method.invoke(mixin, args);
- }
-
- if (!asyncControl.syncControl().isSkipRules()) {
- var doih = proxyGenerator.handler(managedMixin.objSpec());
- var origin = WrappingObject.Origin.fallbackMixin(mixin,
managedMixee);
- doih.invoke(new WrapperInvocation(origin, method, args));
- }
-
- var actionAndTarget = MemberAndTarget.forMixin(resolvedMethod,
mixee, managedMixin);
- if (!actionAndTarget.isMemberFound()) {
- return method.invoke(mixin, args);
- }
-
- submitAsync(actionAndTarget, args, asyncControl);
- return null;
- };
-
- var proxyClass = proxyFactoryService
- .proxyClass(handler, mixinClass, new
Class[]{WrappingObject.class}, WrappingObject.ADDITIONAL_FIELDS);
-
- var proxyFactory = proxyFactoryService
- .factory(proxyClass, mixinConstructor.getParameterTypes());
-
- return proxyFactory.createInstance(new Object[]{ mixee });
- }
-
- private boolean isInheritedFromJavaLangObject(final Method method) {
- return method.getDeclaringClass().equals(Object.class);
- }
-
- private <R> void submitAsync(
- final MemberAndTarget memberAndTarget,
- final Object[] args,
- final AsyncControl<R> asyncControl) {
-
- var interactionLayer = currentInteractionLayer();
- var interactionContext = interactionLayer.interactionContext();
- var asyncInteractionContext = interactionContextFrom(asyncControl,
interactionContext);
-
- var parentCommand =
getInteractionService().currentInteractionElseFail().getCommand();
- var parentInteractionId = parentCommand.getInteractionId();
-
- var targetAdapter = memberAndTarget.target();
- var method = memberAndTarget.method();
-
- var head = InteractionHead.regular(targetAdapter);
-
- var childInteractionId = interactionIdGenerator.interactionId();
- CommandDto childCommandDto;
- switch (memberAndTarget.type()) {
- case ACTION:
- var action = memberAndTarget.action();
- var argAdapters =
ManagedObject.adaptParameters(action.getParameters(), _Lists.ofArray(args));
- childCommandDto = commandDtoFactory
- .asCommandDto(childInteractionId, head, action,
argAdapters);
- break;
- case PROPERTY:
- var property = memberAndTarget.property();
- var propertyValueAdapter =
ManagedObject.adaptProperty(property, args[0]);
- childCommandDto = commandDtoFactory
- .asCommandDto(childInteractionId, head, property,
propertyValueAdapter);
- break;
- default:
- // shouldn't happen, already catered for this case previously
- return;
- }
- var oidDto = childCommandDto.getTargets().getOid().get(0);
-
- var rootExceptionHandler =
asyncControl.syncControl().exceptionHandler();
- asyncControl.setExceptionHandler(new AsyncLogger(rootExceptionHandler,
method, Bookmark.forOidDto(oidDto)));
-
- var executorService =
Optional.ofNullable(asyncControl.executorService())
- .orElse(commonExecutorService);
- var asyncTask = getServiceInjector().injectServicesInto(new
AsyncTask<R>(
- asyncInteractionContext,
- Propagation.REQUIRES_NEW,
- childCommandDto,
- asyncControl.returnType(),
- parentInteractionId)); // this command becomes the parent of child
command
-
- var future = executorService.submit(asyncTask);
- asyncControl.futureRef().set(future);
- }
-
- private static <R> InteractionContext interactionContextFrom(
- final AsyncControl<R> asyncControl,
- final InteractionContext interactionContext) {
-
- return InteractionContext.builder()
-
.clock(Optional.ofNullable(asyncControl.clock()).orElseGet(interactionContext::getClock))
-
.locale(Optional.ofNullable(asyncControl.locale()).map(UserLocale::valueOf).orElse(null))
// if not set in asyncControl use defaults (set override to null)
-
.timeZone(Optional.ofNullable(asyncControl.timeZone()).orElseGet(interactionContext::getTimeZone))
-
.user(Optional.ofNullable(asyncControl.user()).orElseGet(interactionContext::getUser))
- .build();
- }
-
- record MemberAndTarget(
- Type type,
- /**
- * Populated if and only if {@link #type} is {@link Type#ACTION}.
- */
- ObjectAction action,
- /**
- * Populated if and only if {@link #type} is {@link Type#PROPERTY}.
- */
- OneToOneAssociation property,
- ManagedObject target,
- Method method) {
- static MemberAndTarget notFound() {
- return new MemberAndTarget(Type.NONE, null, null, null, null);
- }
- static MemberAndTarget foundAction(final ObjectAction action, final
ManagedObject target, final Method method) {
- return new MemberAndTarget(Type.ACTION, action, null, target,
method);
- }
- static MemberAndTarget foundProperty(final OneToOneAssociation
property, final ManagedObject target, final Method method) {
- return new MemberAndTarget(Type.PROPERTY, null, property, target,
method);
- }
- static MemberAndTarget forRegular(
- final ResolvedMethod method,
- final ManagedObject targetAdapter) {
-
- var objectMember =
targetAdapter.objSpec().getMember(method).orElse(null);
- if(objectMember == null) {
- return MemberAndTarget.notFound();
- }
- if (objectMember instanceof OneToOneAssociation) {
- return MemberAndTarget.foundProperty((OneToOneAssociation)
objectMember, targetAdapter, method.method());
- }
- if (objectMember instanceof ObjectAction) {
- return MemberAndTarget.foundAction((ObjectAction)
objectMember, targetAdapter, method.method());
- }
-
- throw new UnsupportedOperationException(
- "Only properties and actions can be executed in the
background "
- + "(method " + method.name() + " represents a " +
objectMember.getFeatureType().name() + "')");
- }
- static <T> MemberAndTarget forMixin(
- final ResolvedMethod method,
- final T mixee,
- final ManagedObject mixinAdapter) {
-
- var mixinMember =
mixinAdapter.objSpec().getMember(method).orElse(null);
- if (mixinMember == null) {
- return MemberAndTarget.notFound();
- }
-
- var mmc = mixinAdapter.getMetaModelContext();
-
- // find corresponding action of the mixee (this is the 'real'
target, the target usable for invocation).
- var mixeeClass = mixee.getClass();
-
- // don't care about anything other than actions
- // (contributed properties and collections are read-only).
- final ObjectAction targetAction =
mmc.getSpecificationLoader().specForType(mixeeClass)
- .flatMap(mixeeSpec->mixeeSpec.streamAnyActions(MixedIn.ONLY)
- .filter(act ->
((MixedInMember)act).hasMixinAction((ObjectAction) mixinMember))
- .findFirst()
- )
- .orElseThrow(()->new UnsupportedOperationException(String.format(
- "Could not locate objectAction delegating to mixinAction
id='%s' on mixee class '%s'",
- mixinMember.getId(), mixeeClass.getName())));
-
- return MemberAndTarget.foundAction(targetAction,
mmc.getObjectManager().adapt(mixee), method.method());
- }
-
- public boolean isMemberFound() {
- return type != Type.NONE;
- }
-
- enum Type {
- ACTION,
- PROPERTY,
- NONE
- }
+ final @NonNull AsyncControl asyncControl) {
+ var proxy = wrapMixin(mixinClass, mixee, asyncControl.syncControl());
+ return new AsyncProxyInternal<>(
+ CompletableFuture.completedFuture(proxy),
+ asyncExecutorService(asyncControl));
}
// -- LISTENERS
@@ -565,102 +324,6 @@ public void dispatchTypeSafe(final T interactionEvent) {
dispatchersByEventClass.put(type, dispatcher);
}
- private InteractionLayer currentInteractionLayer() {
- return getInteractionService().currentInteractionLayerElseFail();
- }
-
- @RequiredArgsConstructor
- private static class AsyncTask<R> implements AsyncCallable<R> {
-
- private static final long serialVersionUID = 1L;
-
- @Getter private final InteractionContext interactionContext;
- @Getter private final Propagation propagation;
- @Getter private final CommandDto commandDto;
- @Getter private final Class<R> returnType;
- @Getter private final UUID parentInteractionId;
-
- /**
- * Note this is a <code>transient</code> field, in order that
- * {@link
org.apache.causeway.applib.services.wrapper.callable.AsyncCallable} can be
declared as
- * {@link java.io.Serializable}.
- *
- * <p>
- * Because this field needs to be populated, the {@link
java.util.concurrent.ExecutorService} that ultimately
- * executes the task will need to be a custom implementation because
it must reinitialize this field first,
- * using the {@link ServiceInjector} service. Alternatively, it
could call
- * {@link WrapperFactory#execute(AsyncCallable)} directly, which
achieves the same thing.
- * </p>
- */
- @Inject transient WrapperFactory wrapperFactory;
-
- /**
- * If the {@link java.util.concurrent.ExecutorService} used to execute
this task (as defined by
- * {@link AsyncControl#with(ExecutorService)} is not custom, then it
can simply invoke this method, but it is
- * important that it has not serialized/deserialized the object since
important transient state would be lost.
- *
- * <p>
- * On the other hand, a custom implementation of {@link
ExecutorService} is free to serialize this object, and
- * deserialize it later. When deserializing it can either
reinitialize the necessary state using the
- * {@link ServiceInjector} service, then call this method, or it can
instead call
- * {@link WrapperFactory#execute(AsyncCallable)} directly, which
achieves the same thing.
- * </p>
- */
- @Override
- public R call() {
- if (wrapperFactory == null) {
- throw new IllegalStateException(
- "The transient wrapperFactory is null; suggests that
this async task been serialized and " +
- "then deserialized, but is now being executed by an
ExecutorService that has not re-injected necessary services.");
- }
- return wrapperFactory.execute(this);
- }
- }
-
- @Override
- public <R> R execute(final AsyncCallable<R> asyncCallable) {
- getServiceInjector().injectServicesInto(this);
- final R result = interactionServiceProvider.get()
- .call(asyncCallable.getInteractionContext(),
- () ->
updateDomainObjectHonoringTransactionalPropagation(asyncCallable));
- return result;
- }
-
- private <R> R updateDomainObjectHonoringTransactionalPropagation(final
AsyncCallable<R> asyncCallable) {
- return transactionServiceProvider.get()
- .callTransactional(asyncCallable.getPropagation(),
- () -> updateDomainObject(asyncCallable))
- .ifFailureFail()
- .getValue().orElse(null);
- }
-
- private <R> R updateDomainObject(final AsyncCallable<R> asyncCallable) {
-
- // obtain the Command that is implicitly created (initially mainly
empty) whenever an Interaction is started.
- var childCommand =
interactionProviderProvider.get().currentInteractionElseFail().getCommand();
-
- // we will "take over" this Command, updating it with the
parentInteractionId of the command for the action
- // that called WrapperFactory#asyncMixin in the first place.
-
childCommand.updater().setParentInteractionId(asyncCallable.getParentInteractionId());
-
- var tryBookmark =
commandExecutorServiceProvider.get().executeCommand(asyncCallable.getCommandDto());
-
- return tryBookmark.fold(
- throwable -> null, // failure
- bookmarkIfAny -> bookmarkIfAny.map( // success
- bookmark -> {
- var spec =
getSpecificationLoader().specForBookmark(bookmark).orElse(null);
- if(spec==null) {
- return null;
- }
- R domainObject =
bookmarkServiceProvider.get().lookup(bookmark,
asyncCallable.getReturnType()).orElse(null);
- if(spec.isEntity()) {
- domainObject =
repositoryServiceProvider.get().detach(domainObject);
- }
- return domainObject;
- }).orElse(null));
- }
-
private final static int MIN_POOL_SIZE = 2; // at least 2
private final static int MAX_POOL_SIZE = 4; // max 4
private ExecutorService newCommonExecutorService() {
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
new file mode 100644
index 00000000000..ee5235fef70
--- /dev/null
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
@@ -0,0 +1,31 @@
+/*
+ * 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.core.runtimeservices.wrapper.handlers;
+
+import java.util.UUID;
+
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
+
+record CommandRecord(
+ InteractionContext interactionContext,
+ CommandDto commandDto,
+ UUID parentInteractionId) {
+
+}
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
new file mode 100644
index 00000000000..6b19783527a
--- /dev/null
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.core.runtimeservices.wrapper.handlers;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.interactions.InteractionHead;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
+
+record CommandRecordFactory(
+ InteractionIdGenerator interactionIdGenerator) {
+
+ public CommandRecord forAction(InteractionHead head, ObjectAction act,
Can<ManagedObject> args) {
+ return new CommandRecord(
+
act.getInteractionService().currentInteractionContextElseFail(),
+ act.getCommandDtoFactory()
+ .asCommandDto(interactionIdGenerator.interactionId(),
head, act, args),
+ act.getInteractionService().currentInteractionElseFail()
+ .getCommand()
+ .getInteractionId());
+ }
+
+ public CommandRecord forProperty(InteractionHead head, OneToOneAssociation
prop, ManagedObject arg) {
+ return new CommandRecord(
+
prop.getInteractionService().currentInteractionContextElseFail(),
+ prop.getCommandDtoFactory()
+ .asCommandDto(interactionIdGenerator.interactionId(),
head, prop, arg),
+ prop.getInteractionService().currentInteractionElseFail()
+ .getCommand()
+ .getInteractionId());
+ }
+
+
+}
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
index 3c39d7c97a1..4e2587eabbf 100644
---
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
@@ -24,6 +24,8 @@
import java.util.function.Supplier;
import java.util.stream.Stream;
+import org.jspecify.annotations.Nullable;
+
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.exceptions.recoverable.InteractionException;
import org.apache.causeway.applib.services.wrapper.DisabledException;
@@ -45,6 +47,7 @@
import org.apache.causeway.commons.internal.reflection._GenericResolver;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.causeway.core.metamodel.consent.InteractionResult;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.facets.ImperativeFacet;
import org.apache.causeway.core.metamodel.facets.ImperativeFacet.Intent;
import org.apache.causeway.core.metamodel.interactions.InteractionHead;
@@ -76,16 +79,19 @@ final class DomainObjectInvocationHandler
@Getter(onMethod_ = {@Override}) @Accessors(fluent=true)
private final String key;
- private final ProxyGenerator proxyGenerator;
private final ObjectSpecification targetSpec;
+ private final ProxyGenerator proxyGenerator;
+ private final CommandRecordFactory commandRecordFactory;
DomainObjectInvocationHandler(
final ObjectSpecification targetSpec,
- final ProxyGenerator proxyGenerator) {
+ final ProxyGenerator proxyGenerator,
+ final CommandRecordFactory commandRecordFactory) {
this.targetSpec = targetSpec;
this.classMetaData =
WrapperInvocationHandler.ClassMetaData.of(targetSpec.getCorrespondingClass());
this.proxyGenerator = proxyGenerator;
this.key = targetSpec.getCorrespondingClass().getName();
+ this.commandRecordFactory = commandRecordFactory;
}
@Override
@@ -100,7 +106,7 @@ public Object invoke(WrapperInvocation wrapperInvocation)
throws Throwable {
return method.invoke(target, wrapperInvocation.args());
}
- final ManagedObject targetAdapter =
targetSpec.getObjectManager().adapt(target);
+ final ManagedObject targetAdapter =
mmc().getObjectManager().adapt(target);
if(!targetAdapter.specialization().isMixin()) {
MmAssertionUtils.assertIsBookmarkSupported(targetAdapter);
@@ -161,14 +167,12 @@ public Object invoke(WrapperInvocation wrapperInvocation)
throws Throwable {
}
}
- if (objectMember instanceof ObjectAction) {
+ if (objectMember instanceof ObjectAction objectAction) {
if (intent == Intent.CHECK_IF_VALID) {
throw new UnsupportedOperationException(String.format("Cannot
invoke supporting method '%s'; use only the 'invoke' method",
objectMember.getId()));
}
- var objectAction = (ObjectAction) objectMember;
-
if(targetAdapter.objSpec().isMixin()) {
if (managedMixee == null) {
throw _Exceptions.illegalState(
@@ -227,9 +231,9 @@ private static ObjectMember determineMixinMember(
}
private InteractionInitiatedBy getInteractionInitiatedBy(final
WrapperInvocation wrapperInvocation) {
- return wrapperInvocation.shouldEnforceRules()
- ? InteractionInitiatedBy.USER
- : InteractionInitiatedBy.FRAMEWORK;
+ return wrapperInvocation.syncControl().isSkipRules()
+ ? InteractionInitiatedBy.FRAMEWORK
+ : InteractionInitiatedBy.USER;
}
private boolean isEnhancedEntityMethod(final Method method) {
@@ -246,7 +250,7 @@ private Object handleTitleMethod(
var titleContext = targetNoSpec
.createTitleInteractionContext(targetAdapter,
InteractionInitiatedBy.FRAMEWORK);
var titleEvent = titleContext.createInteractionEvent();
- targetSpec.getWrapperFactory().notifyListeners(titleEvent);
+ mmc().getWrapperFactory().notifyListeners(titleEvent);
return titleEvent.getTitle();
}
@@ -261,6 +265,8 @@ private Object handleSaveMethod(
notifyListenersAndVetoIfRequired(interactionResult);
});
+ handleCommandListeners(wrapperInvocation, ()->null); //FIXME
+
var spec = targetAdapter.objSpec();
if(spec.isEntity()) {
return runExecutionTask(wrapperInvocation, ()->{
@@ -283,6 +289,8 @@ private Object handleGetterMethodOnProperty(
checkVisibility(wrapperInvocation, targetAdapter, property);
});
+ handleCommandListeners(wrapperInvocation, ()->null); //FIXME
+
return runExecutionTask(wrapperInvocation, ()->{
var interactionInitiatedBy =
getInteractionInitiatedBy(wrapperInvocation);
@@ -290,7 +298,7 @@ private Object handleGetterMethodOnProperty(
var currentReferencedObj =
MmUnwrapUtils.single(currentReferencedAdapter);
- targetSpec.getWrapperFactory().notifyListeners(new
PropertyAccessEvent(
+ mmc().getWrapperFactory().notifyListeners(new PropertyAccessEvent(
targetAdapter.getPojo(),
property.getFeatureIdentifier(),
currentReferencedObj));
@@ -321,6 +329,9 @@ targetAdapter, argumentAdapter,
getInteractionInitiatedBy(wrapperInvocation))
notifyListenersAndVetoIfRequired(interactionResult);
});
+ handleCommandListeners(wrapperInvocation, ()->commandRecordFactory
+ .forProperty(InteractionHead.regular(targetAdapter), property,
argumentAdapter));
+
return runExecutionTask(wrapperInvocation, ()->{
property.set(targetAdapter, argumentAdapter,
getInteractionInitiatedBy(wrapperInvocation));
return null;
@@ -340,6 +351,8 @@ private Object handleGetterMethodOnCollection(
checkVisibility(wrapperInvocation, targetAdapter, collection);
});
+ handleCommandListeners(wrapperInvocation, ()->null); //FIXME
+
return runExecutionTask(wrapperInvocation, ()->{
var interactionInitiatedBy =
getInteractionInitiatedBy(wrapperInvocation);
@@ -353,13 +366,13 @@ private Object handleGetterMethodOnCollection(
var collectionViewObject = wrapCollection(
(Collection<?>) currentReferencedObj,
collection);
-
targetSpec.getWrapperFactory().notifyListeners(collectionAccessEvent);
+
mmc().getWrapperFactory().notifyListeners(collectionAccessEvent);
return collectionViewObject;
} else if (currentReferencedObj instanceof Map) {
var mapViewObject = wrapMap(
(Map<?, ?>) currentReferencedObj,
collection);
-
targetSpec.getWrapperFactory().notifyListeners(collectionAccessEvent);
+
mmc().getWrapperFactory().notifyListeners(collectionAccessEvent);
return mapViewObject;
}
@@ -412,6 +425,9 @@ private Object handleActionMethod(
checkValidity(wrapperInvocation, head, objectAction, argAdapters);
});
+ handleCommandListeners(wrapperInvocation, ()->commandRecordFactory
+ .forAction(head, objectAction, argAdapters));
+
return runExecutionTask(wrapperInvocation, ()->{
var interactionInitiatedBy =
getInteractionInitiatedBy(wrapperInvocation);
@@ -419,9 +435,7 @@ private Object handleActionMethod(
head, argAdapters,
interactionInitiatedBy);
return MmUnwrapUtils.single(returnedAdapter);
-
});
-
}
private void checkValidity(
@@ -478,7 +492,7 @@ private void checkUsability(
private void notifyListenersAndVetoIfRequired(final InteractionResult
interactionResult) {
var interactionEvent = interactionResult.getInteractionEvent();
- targetSpec.getWrapperFactory().notifyListeners(interactionEvent);
+ mmc().getWrapperFactory().notifyListeners(interactionEvent);
if (interactionEvent.isVeto()) {
throw toException(interactionEvent);
}
@@ -510,10 +524,12 @@ private InteractionException toException(final
InteractionEvent interactionEvent
// -- HELPER
+ private MetaModelContext mmc() {
+ return targetSpec.getMetaModelContext();
+ }
+
private void runValidationTask(final WrapperInvocation wrapperInvocation,
final Runnable task) {
- if(!wrapperInvocation.shouldEnforceRules()) {
- return;
- }
+ if(wrapperInvocation.syncControl().isSkipRules()) return;
try {
task.run();
} catch(Exception ex) {
@@ -521,10 +537,30 @@ private void runValidationTask(final WrapperInvocation
wrapperInvocation, final
}
}
- private <X> X runExecutionTask(final WrapperInvocation wrapperInvocation,
final Supplier<X> task) {
- if(!wrapperInvocation.shouldExecute()) {
- return null;
+ /**
+ * regardless of to be executed or not,
+ * we inform any listeners of the execution intent (e.g.
BackgroundExecutionService)
+ */
+ private void handleCommandListeners(
+ final WrapperInvocation wrapperInvocation,
+ final @Nullable Supplier<CommandRecord> commandRecordSupplier) {
+ if(commandRecordSupplier!=null
+ &&
wrapperInvocation.syncControl().commandListeners().isNotEmpty()) {
+ var commandRecord = commandRecordSupplier.get();
+ if(commandRecord==null) return;
+ wrapperInvocation.syncControl().commandListeners()
+ .forEach(listener->listener
+ .onCommand(
+ commandRecord.interactionContext(),
+ commandRecord.commandDto(),
+ commandRecord.parentInteractionId()));
}
+ }
+
+ private <X> X runExecutionTask(
+ final WrapperInvocation wrapperInvocation,
+ final Supplier<X> task) {
+ if(wrapperInvocation.syncControl().isSkipExecute()) return null;
try {
return task.get();
} catch(Exception ex) {
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
index 85a1e9a4466..b5e2b424adf 100644
---
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
@@ -34,8 +34,15 @@
import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;
import org.apache.causeway.core.runtime.wrap.WrappingObject;
+import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
-public record ProxyGenerator(@NonNull ProxyFactoryService proxyFactoryService)
{
+public record ProxyGenerator(
+ @NonNull ProxyFactoryService proxyFactoryService,
+ @NonNull CommandRecordFactory commandRecordFactory) {
+
+ public ProxyGenerator(ProxyFactoryService proxyFactoryService,
InteractionIdGenerator interactionIdGenerator) {
+ this(proxyFactoryService, new
CommandRecordFactory(interactionIdGenerator));
+ }
@SuppressWarnings("unchecked")
public <T> T objectProxy(
@@ -113,7 +120,7 @@ private <T, P> P instantiatePluralProxy(final Class<T>
base, final PluralInvocat
public WrapperInvocationHandler handler(ObjectSpecification targetSpec) {
return new DomainObjectInvocationHandler(
targetSpec,
- this);
+ this, commandRecordFactory);
}
}
diff --git
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
index a7489e78312..292a4e9711c 100644
---
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
+++
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
@@ -50,7 +50,7 @@ public void __causeway_save() {
@Override
public WrappingObject.Origin __causeway_origin() {
- return new WrappingObject.Origin(wrappedObject,
SyncControl.control());
+ return new WrappingObject.Origin(wrappedObject,
SyncControl.defaults());
}
}
@@ -117,7 +117,7 @@ public void
wrap_ofWrapped_differentMode_delegates_to_createProxy() throws Excep
final DomainObject domainObject = new
WrappingDomainObject(wrappedObject);
// when
- final DomainObject wrappingObject = wrapperFactory.wrap(domainObject,
SyncControl.control().withSkipRules());
+ final DomainObject wrappingObject = wrapperFactory.wrap(domainObject,
SyncControl.defaults().withSkipRules());
// then
assertThat(wrappingObject, is(not(domainObject)));
diff --git
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
index ea62989b938..d5f7cd91abb 100644
---
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
+++
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
@@ -34,7 +34,7 @@
class ProxyCreatorTestUsingCodegenPlugin extends RuntimeServicesTestAbstract {
- private ProxyGenerator proxyGenerator = new ProxyGenerator(new
ProxyFactoryServiceByteBuddy());
+ private ProxyGenerator proxyGenerator = new ProxyGenerator(new
ProxyFactoryServiceByteBuddy(), new CommandRecordFactory(null));
@DomainObject(nature = Nature.VIEW_MODEL)
public static class Employee {
@@ -53,7 +53,7 @@ void proxyShouldDelegateCalls() {
final Employee employee = new Employee();
var employeeSpec =
getMetaModelContext().getSpecificationLoader().loadSpecification(Employee.class);
- var proxy = proxyGenerator.objectProxy(employee, employeeSpec,
SyncControl.control());
+ var proxy = proxyGenerator.objectProxy(employee, employeeSpec,
SyncControl.defaults());
assertNotNull(proxy);
assertTrue(proxy instanceof WrappingObject);
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 dc025903595..64303bf8c12 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
@@ -18,8 +18,6 @@
*/
package org.apache.causeway.extensions.commandlog.applib;
-import
org.apache.causeway.extensions.commandlog.applib.spi.RunBackgroundCommandsJobListener;
-
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -36,6 +34,7 @@
import
org.apache.causeway.extensions.commandlog.applib.fakescheduler.FakeScheduler;
import
org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
import
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
+import
org.apache.causeway.extensions.commandlog.applib.spi.RunBackgroundCommandsJobListener;
import
org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscriberForCommandLog;
@Configuration
@@ -62,7 +61,6 @@
BackgroundCommandsJobControl.class,
BackgroundService.class,
- BackgroundService.PersistCommandExecutorService.class,
FakeScheduler.class,
})
diff --git
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
index 9023f4e604f..ecc931eb0c2 100644
---
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
+++
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
@@ -19,15 +19,7 @@
package org.apache.causeway.extensions.commandlog.applib.dom;
import java.sql.Timestamp;
-import java.util.Collection;
-import java.util.List;
import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import jakarta.inject.Inject;
@@ -36,9 +28,11 @@
import org.apache.causeway.applib.jaxb.JavaSqlJaxbAdapters;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
-import org.apache.causeway.applib.services.wrapper.callable.AsyncCallable;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
+import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.schema.cmd.v2.CommandDto;
import org.apache.causeway.schema.common.v2.PeriodDto;
@@ -46,21 +40,19 @@
* Allows the execution of action invocations or property edits to be deferred
so that they can be executed later in
* another thread of execution.
*
- * <p>
- * Typically this other thread of execution would be scheduled from quartz
or similar. The
- * {@link
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob}
provides a ready-made
- * implementation to do this for quartz.
- * </p>
+ * <p>Typically this other thread of execution would be scheduled from quartz
or similar. The
+ * {@link
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob}
provides a ready-made
+ * implementation to do this for quartz.
*
* @see WrapperFactory
* @see
org.apache.causeway.extensions.commandlog.applib.fakescheduler.FakeScheduler
- * @since 2.0 {@index}
+ * @since 2.0 revised for 3.4 {@index}
*/
@Service
public class BackgroundService {
@Inject WrapperFactory wrapperFactory;
- @Inject PersistCommandExecutorService persistCommandExecutorService;
+ @Inject CommandLogEntryRepository commandLogEntryRepository;
/**
* Wraps the domain object in a proxy whereby any actions invoked through
the proxy will instead be persisted as a
@@ -68,10 +60,11 @@ public class BackgroundService {
*
* @see #executeMixin(Class, Object) - to invoke actions that are
implemented as mixins
*/
- public <T> T execute(final T object) {
- return wrapperFactory.asyncWrap(object,
AsyncControl.returningVoid().withCheckRules()
- .with(persistCommandExecutorService)
- );
+ public <T> AsyncProxy<T> execute(final T object) {
+ return wrapperFactory.asyncWrap(object, AsyncControl.defaults()
+ .withNoExecute()
+ .withCheckRules()
+ .listen(new CommandPersistor(commandLogEntryRepository)));
}
/**
* Wraps the domain object in a proxy whereby any actions invoked through
the proxy will instead be persisted as a
@@ -79,10 +72,11 @@ public <T> T execute(final T object) {
*
* @see #executeMixin(Class, Object) - to invoke actions that are
implemented as mixins
*/
- public <T> T executeSkipRules(final T object) {
- return wrapperFactory.asyncWrap(object,
AsyncControl.returningVoid().withSkipRules()
- .with(persistCommandExecutorService)
- );
+ public <T> AsyncProxy<T> executeSkipRules(final T object) {
+ return wrapperFactory.asyncWrap(object, AsyncControl.defaults()
+ .withNoExecute()
+ .withSkipRules()
+ .listen(new CommandPersistor(commandLogEntryRepository)));
}
/**
@@ -91,10 +85,11 @@ public <T> T executeSkipRules(final T object) {
*
* @see #execute(Object) - to invoke actions that are implemented directly
within the object
*/
- public <T> T executeMixin(final Class<T> mixinClass, final Object mixedIn)
{
- return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn,
AsyncControl.returningVoid().withCheckRules()
- .with(persistCommandExecutorService)
- );
+ public <T> AsyncProxy<T> executeMixin(final Class<T> mixinClass, final
Object mixedIn) {
+ return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn,
AsyncControl.defaults()
+ .withNoExecute()
+ .withCheckRules()
+ .listen(new CommandPersistor(commandLogEntryRepository)));
}
/**
@@ -103,39 +98,28 @@ public <T> T executeMixin(final Class<T> mixinClass, final
Object mixedIn) {
*
* @see #execute(Object) - to invoke actions that are implemented directly
within the object
*/
- public <T> T executeMixinSkipRules(final Class<T> mixinClass, final Object
mixedIn) {
- return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn,
AsyncControl.returningVoid().withSkipRules()
- .with(persistCommandExecutorService)
- );
+ public <T> AsyncProxy<T> executeMixinSkipRules(final Class<T> mixinClass,
final Object mixedIn) {
+ return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn,
AsyncControl.defaults()
+ .withNoExecute()
+ .withSkipRules()
+ .listen(new CommandPersistor(commandLogEntryRepository)));
}
- /**
- * @since 2.0 {@index}
- */
- @Service
- public static class PersistCommandExecutorService implements
ExecutorService {
-
- @Inject CommandLogEntryRepository commandLogEntryRepository;
-
- private final static
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter
gregorianCalendarAdapter = new
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();;
+ record CommandPersistor(CommandLogEntryRepository
commandLogEntryRepository) implements SyncControl.CommandListener {
@Override
- public <T> Future<T> submit(final Callable<T> task) {
- var callable = (AsyncCallable<T>) task;
- var commandDto = callable.getCommandDto();
+ public void onCommand(
+ final InteractionContext interactionContext,
+ final CommandDto commandDto,
+ final UUID parentInteractionId) {
// we'll mutate the commandDto in line with the callable, then
// create the CommandLogEntry from that commandDto
- var childInteractionId = UUID.randomUUID();
- commandDto.setInteractionId(childInteractionId.toString());
+ commandDto.setInteractionId(UUID.randomUUID().toString());
// copy details from requested interaction context into the
commandDto
- var interactionContext = callable.getInteractionContext();
- var timestamp =
interactionContext.getClock().nowAsJavaSqlTimestamp();
-
commandDto.setTimestamp(gregorianCalendarAdapter.marshal(timestamp));
-
- var username = interactionContext.getUser().name();
- commandDto.setUsername(username);
+
commandDto.setTimestamp(GREGORIAN_CALENDAR_ADAPTER.marshal(interactionContext.getClock().nowAsJavaSqlTimestamp()));
+ commandDto.setUsername(interactionContext.getUser().name());
var periodDto = new PeriodDto();
periodDto.setStartedAt(null);
@@ -144,110 +128,27 @@ public <T> Future<T> submit(final Callable<T> task) {
var childCommand = newCommand(commandDto);
- commandLogEntryRepository.createEntryAndPersist(childCommand,
callable.getParentInteractionId(), ExecuteIn.BACKGROUND);
-
- // a more sophisticated implementation could perhaps return a
Future that supports these methods by
- // querying the CommandLogEntryRepository
- return new Future<T>() {
- @Override
- public boolean cancel(final boolean mayInterruptIfRunning) {
- throw new IllegalStateException("Not implemented");
- }
- @Override
- public boolean isCancelled() {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public boolean isDone() {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public T get() {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public T get(final long timeout, final TimeUnit unit) {
- throw new IllegalStateException("Not implemented");
- }
- };
+ commandLogEntryRepository.createEntryAndPersist(childCommand,
parentInteractionId, ExecuteIn.BACKGROUND);
}
+ // -- HELPER
+
+ private final static
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter
GREGORIAN_CALENDAR_ADAPTER
+ = new JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();
+
private static Command newCommand(final CommandDto commandDto) {
return new Command(UUID.fromString(commandDto.getInteractionId()))
{
@Override public String getUsername() {return
commandDto.getUsername();}
- @Override public Timestamp getTimestamp() {return
gregorianCalendarAdapter.unmarshal(commandDto.getTimestamp());}
+ @Override public Timestamp getTimestamp() {return
GREGORIAN_CALENDAR_ADAPTER.unmarshal(commandDto.getTimestamp());}
@Override public CommandDto getCommandDto() {return
commandDto;}
@Override public String getLogicalMemberIdentifier() {return
commandDto.getMember().getLogicalMemberIdentifier();}
@Override public Bookmark getTarget() {return
Bookmark.forOidDto(commandDto.getTargets().getOid().get(0));}
- @Override public Timestamp getStartedAt() {return
gregorianCalendarAdapter.unmarshal(commandDto.getTimings().getStartedAt());}
- @Override public Timestamp getCompletedAt() {return
gregorianCalendarAdapter.unmarshal(commandDto.getTimings().getCompletedAt());}
+ @Override public Timestamp getStartedAt() {return
GREGORIAN_CALENDAR_ADAPTER.unmarshal(commandDto.getTimings().getStartedAt());}
+ @Override public Timestamp getCompletedAt() {return
GREGORIAN_CALENDAR_ADAPTER.unmarshal(commandDto.getTimings().getCompletedAt());}
@Override public Bookmark getResult() {return null;}
@Override public Throwable getException() {return null;}
};
}
- @Override
- public <T> Future<T> submit(final Runnable task, final T result) {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public Future<?> submit(final Runnable task) {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public void execute(final Runnable command) {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks) {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks, final long timeout, final TimeUnit unit) throws
InterruptedException {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public <T> T invokeAny(final Collection<? extends Callable<T>> tasks,
final long timeout, final TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public void shutdown() {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public List<Runnable> shutdownNow() {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public boolean awaitTermination(final long timeout, final TimeUnit
unit) throws InterruptedException {
- throw new IllegalStateException("Not implemented");
- }
-
- @Override
- public boolean isShutdown() {
- return false;
- }
-
- @Override
- public boolean isTerminated() {
- return false;
- }
-
}
}
diff --git
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
index 6ec96cd451a..5a3e3dce8e4 100644
---
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
+++
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
@@ -19,6 +19,7 @@
package org.apache.causeway.extensions.commandlog.applib.integtest;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import jakarta.inject.Inject;
@@ -62,7 +63,7 @@ public abstract class BackgroundService_IntegTestAbstract
extends CausewayIntegr
Bookmark bookmark;
- protected abstract Counter newCounter(String name);
+ protected abstract <T extends Counter> T newCounter(String name);
private static boolean prototypingOrig;
@@ -84,7 +85,7 @@ void setup_counter() {
counterRepository.removeAll();
counterRepository.persist(newCounter("fred"));
- List<Counter> counters = counterRepository.find();
+ List<? extends Counter> counters = counterRepository.find();
assertThat(counters).hasSize(1);
bookmark = bookmarkService.bookmarkForElseFail(counters.get(0));
@@ -104,11 +105,12 @@ void async_using_default_executor_service() {
transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
var counter = bookmarkService.lookup(bookmark,
Counter.class).orElseThrow();
- var control = AsyncControl.returning(Counter.class);
- wrapperFactory.asyncWrap(counter,
control).bumpUsingDeclaredAction();
+ var control = AsyncControl.defaults();
+ wrapperFactory.asyncWrap(counter, control)
+ .thenApplyAsync(Counter::bumpUsingDeclaredAction)
+ .orTimeout(5, TimeUnit.SECONDS)
+ .join(); // wait till done
- // wait till done
- control.future().get();
}).ifFailureFail();
// then
@@ -123,13 +125,12 @@ void async_using_default_executor_service() {
assertThat(counter.getNum()).isEqualTo(1L);
// when
- var control = AsyncControl.returning(Counter.class);
- wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
counter, control).act();
+ var control = AsyncControl.defaults();
+ wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
counter, control)
+ .thenApplyAsync(Counter_bumpUsingMixin::act)
+ .join(); // wait till done
- // wait til done
- control.future().get();
}).ifFailureFail();
-
// then
transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
var counter = bookmarkService.lookup(bookmark,
Counter.class).orElseThrow();
@@ -151,9 +152,11 @@ void using_background_service() {
assertThat(counter.getNum()).isNull();
// when
- backgroundService.execute(counter).bumpUsingDeclaredAction();
+ backgroundService.execute(counter)
+ .thenAcceptAsync(Counter::bumpUsingDeclaredAction)
+ .orTimeout(1, TimeUnit.SECONDS)
+ .join(); // wait for completion
- Thread.sleep(1_000);// horrid, but let's just wait 1 sec before
testing
}).ifFailureFail();
// then no change to the counter
@@ -219,12 +222,11 @@ private void removeAllCommandLogEntriesAndCounters() {
@Inject InteractionService interactionService;
@Inject BackgroundService backgroundService;
- @Inject BackgroundService.PersistCommandExecutorService
persistCommandExecutorService;
@Inject WrapperFactory wrapperFactory;
@Inject CommandLogEntryRepository commandLogEntryRepository;
@Inject TransactionService transactionService;
@Inject RunBackgroundCommandsJob runBackgroundCommandsJob;
@Inject BookmarkService bookmarkService;
- @Inject CounterRepository counterRepository;
+ @Inject CounterRepository<? extends Counter> counterRepository;
}
diff --git
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index 06fd7efbf5b..2a5d2951337 100644
---
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -84,7 +84,7 @@ void beforeEach() {
assertThat(mostRecentCompleted).isEmpty();
}
- protected abstract Counter newCounter(String name);
+ protected abstract <T extends Counter> T newCounter(String name);
@Test
void invoke_mixin() {
@@ -243,7 +243,7 @@ void roundtrip_CLE_bookmarks() {
// then
assertThat(cleBookmarkIfAny).isPresent();
Bookmark cleBookmark = cleBookmarkIfAny.get();
- String identifier = cleBookmark.getIdentifier();
+ String identifier = cleBookmark.identifier();
if (causewayBeanTypeRegistry.persistenceStack().isJdo()) {
assertThat(identifier).startsWith("u_");
UUID.fromString(identifier.substring("u_".length())); // should
not fail, ie check the format is as we expect
@@ -351,6 +351,7 @@ void test_all_the_repository_methods() {
// given
commandTarget1User1 = commandTarget1User1ById.get();
commandTarget1User2 = commandTarget1User2ById.get();
+ @SuppressWarnings("unused")
var commandTarget1User1Yesterday =
commandTarget1User1YesterdayById.get();
commandTarget2User1 = commandTarget2User1ById.get();
@@ -468,7 +469,7 @@ void test_all_the_repository_methods() {
@Inject ClockService clockService;
@Inject InteractionService interactionService;
@Inject InteractionLayerTracker interactionLayerTracker;
- @Inject CounterRepository counterRepository;
+ @Inject CounterRepository<? extends Counter> counterRepository;
@Inject WrapperFactory wrapperFactory;
@Inject BookmarkService bookmarkService;
@Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry;
diff --git
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/CounterRepository.java
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/CounterRepository.java
index e37a77b45e1..5ee1286cd84 100644
---
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/CounterRepository.java
+++
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/CounterRepository.java
@@ -31,7 +31,7 @@ public abstract class CounterRepository<X extends Counter> {
private final Class<X> counterClass;
- public CounterRepository(Class<X> counterClass) {
+ public CounterRepository(final Class<X> counterClass) {
this.counterClass = counterClass;
}
@@ -39,7 +39,7 @@ public List<X> find() {
return repositoryService.allInstances(counterClass);
}
- public X persist(X counter) {
+ public X persist(final X counter) {
return repositoryService.persistAndFlush(counter);
}
@@ -49,7 +49,7 @@ public void removeAll() {
@Inject RepositoryService repositoryService;
- public Counter findByName(String name) {
+ public X findByName(final String name) {
List<X> xes = find();
return xes.stream().filter(x -> Objects.equals(x.getName(),
name)).findFirst().orElseThrow();
}
diff --git
a/regressiontests/base-jdo/src/main/java/org/apache/causeway/testdomain/jdo/publishing/PublishingTestFactoryJdo.java
b/regressiontests/base-jdo/src/main/java/org/apache/causeway/testdomain/jdo/publishing/PublishingTestFactoryJdo.java
index bce2af5a117..e48f9aa88ca 100644
---
a/regressiontests/base-jdo/src/main/java/org/apache/causeway/testdomain/jdo/publishing/PublishingTestFactoryJdo.java
+++
b/regressiontests/base-jdo/src/main/java/org/apache/causeway/testdomain/jdo/publishing/PublishingTestFactoryJdo.java
@@ -31,12 +31,14 @@
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
+import org.springframework.util.function.ThrowingFunction;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.commons.collections.Can;
@@ -53,8 +55,6 @@
import
org.apache.causeway.testdomain.publishing.PublishingTestFactoryAbstract.CommitListener;
import org.apache.causeway.testdomain.util.dto.BookDto;
-import static
org.apache.causeway.applib.services.wrapper.control.AsyncControl.returningVoid;
-
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -278,7 +278,7 @@ protected void wrapperSyncExecutionNoRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withSkipRules(); //
don't enforce rules
+ var syncControl = SyncControl.defaults().withSkipRules(); //
don't enforce rules
context.changeProperty(()->wrapper.wrap(book,
syncControl).setName("Book #2"));
});
@@ -291,7 +291,7 @@ protected void wrapperSyncExecutionNoRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withSkipRules(); //
don't enforce rules
+ var syncControl = SyncControl.defaults().withSkipRules(); //
don't enforce rules
context.executeAction(()->wrapper.wrap(book,
syncControl).doubleThePrice());
});
@@ -318,7 +318,7 @@ protected void wrapperSyncExecutionWithRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withCheckRules(); //
enforce rules
+ var syncControl = SyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
wrapper.wrap(book, syncControl).setName("Book #2"); //
should fail with DisabledException
@@ -334,7 +334,7 @@ protected void wrapperSyncExecutionWithRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withCheckRules(); //
enforce rules
+ var syncControl = SyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
wrapper.wrap(book, syncControl).doubleThePrice(); //
should fail with DisabledException
@@ -358,20 +358,19 @@ protected void wrapperAsyncExecutionNoRules(
context.bind(commitListener);
// given
- var asyncControl = returningVoid().withSkipRules(); // don't enforce
rules
-
- withBookDo(book->{
+ var asyncControl = AsyncControl.defaults().withSkipRules(); // don't
enforce rules
+ var future = withBookCall(book->{
context.runGiven();
// when - running asynchronous
- wrapper.asyncWrap(book, asyncControl)
- .setName("Book #2");
-
+ return wrapper.asyncWrap(book, asyncControl)
+ .thenAcceptAsync(bk->bk.setName("Book #2"));
});
- asyncControl.getFuture().get(10, TimeUnit.SECONDS);
-
+ future
+ .orTimeout(10, TimeUnit.SECONDS)
+ .join(); // wait till done
}
@Override
@@ -385,11 +384,12 @@ protected void wrapperAsyncExecutionWithRules(
context.runGiven();
// when - running synchronous
- var asyncControl = returningVoid().withCheckRules(); // enforce
rules
+ var asyncControl = AsyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
// should fail with DisabledException (synchronous) within the
calling Thread
- wrapper.asyncWrap(book, asyncControl).setName("Book #2");
+ wrapper.asyncWrap(book, asyncControl)
+ .thenAcceptAsync(bk->bk.setName("Book #2"));
//});
@@ -397,7 +397,7 @@ protected void wrapperAsyncExecutionWithRules(
}
- // -- TEST SETUP
+ // -- HELPER
@SneakyThrows
private void withBookDo(final CheckedConsumer<JdoBook>
transactionalBookConsumer) {
@@ -405,4 +405,10 @@ private void withBookDo(final CheckedConsumer<JdoBook>
transactionalBookConsumer
transactionalBookConsumer.accept(book);
}
+ @SneakyThrows
+ private <T> T withBookCall(final ThrowingFunction<JdoBook, T>
transactionalBookFunction) {
+ var book =
repository.allInstances(JdoBook.class).listIterator().next();
+ return transactionalBookFunction.apply(book);
+ }
+
}
diff --git
a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/publishing/PublishingTestFactoryJpa.java
b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/publishing/PublishingTestFactoryJpa.java
index f3091aaa6ac..dfd7a5212c4 100644
---
a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/publishing/PublishingTestFactoryJpa.java
+++
b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/publishing/PublishingTestFactoryJpa.java
@@ -29,12 +29,14 @@
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
+import org.springframework.util.function.ThrowingFunction;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
import org.apache.causeway.applib.services.wrapper.control.SyncControl;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.commons.collections.Can;
@@ -55,8 +57,6 @@
import org.apache.causeway.testdomain.util.dto.BookDto;
import
org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts;
-import static
org.apache.causeway.applib.services.wrapper.control.AsyncControl.returningVoid;
-
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -273,7 +273,7 @@ protected void wrapperSyncExecutionNoRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withSkipRules(); //
don't enforce rules
+ var syncControl = SyncControl.defaults().withSkipRules(); //
don't enforce rules
context.changeProperty(()->wrapper.wrap(book,
syncControl).setName("Book #2"));
});
@@ -286,7 +286,7 @@ protected void wrapperSyncExecutionNoRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withSkipRules(); //
don't enforce rules
+ var syncControl = SyncControl.defaults().withSkipRules(); //
don't enforce rules
context.executeAction(()->wrapper.wrap(book,
syncControl).doubleThePrice());
});
@@ -313,7 +313,7 @@ protected void wrapperSyncExecutionWithRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withCheckRules(); //
enforce rules
+ var syncControl = SyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
wrapper.wrap(book, syncControl).setName("Book #2"); //
should throw DisabledException
@@ -329,7 +329,7 @@ protected void wrapperSyncExecutionWithRules(
context.runGiven();
// when - running synchronous
- var syncControl = SyncControl.control().withCheckRules(); //
enforce rules
+ var syncControl = SyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
wrapper.wrap(book, syncControl).doubleThePrice(); //
should throw DisabledException
@@ -353,21 +353,19 @@ protected void wrapperAsyncExecutionNoRules(
context.bind(commitListener);
// given
- var asyncControl = returningVoid().withSkipRules(); // don't enforce
rules
-
- // when
-
- withBookDo(book->{
+ var asyncControl = AsyncControl.defaults().withSkipRules(); // don't
enforce rules
+ var future = withBookCall(book->{
context.runGiven();
// when - running asynchronous
- wrapper.asyncWrap(book, asyncControl)
- .setName("Book #2");
-
+ return wrapper.asyncWrap(book, asyncControl)
+ .thenAcceptAsync(bk->bk.setName("Book #2"));
});
- asyncControl.getFuture().get(10, TimeUnit.SECONDS);
+ future
+ .orTimeout(10, TimeUnit.SECONDS)
+ .join(); // wait till done
}
@@ -381,11 +379,12 @@ protected void wrapperAsyncExecutionWithRules(
withBookDo(book->{
// when - running synchronous
- var asyncControl = returningVoid().withCheckRules(); // enforce
rules
+ var asyncControl = AsyncControl.defaults().withCheckRules(); //
enforce rules
//assertThrows(DisabledException.class, ()->{
// should fail with DisabledException (synchronous) within the
calling Thread
- wrapper.asyncWrap(book, asyncControl).setName("Book #2");
+ wrapper.asyncWrap(book, asyncControl)
+ .thenAcceptAsync(bk->bk.setName("Book #2"));
//});
@@ -425,16 +424,20 @@ private void setupBookForJpa() {
em.flush();
}
- @SneakyThrows
- private void withBookDo(final CheckedConsumer<JpaBook> bookConsumer) {
+ // -- HELPER
+ @SneakyThrows
+ private void withBookDo(final CheckedConsumer<JpaBook>
transactionalBookConsumer) {
//var em = jpaSupport.getEntityManagerElseFail(JpaBook.class);
-
var book =
repository.allInstances(JpaBook.class).listIterator().next();
- bookConsumer.accept(book);
-
+ transactionalBookConsumer.accept(book);
//em.flush(); // in effect makes changes visible during PRE_COMMIT
+ }
+ @SneakyThrows
+ private <T> T withBookCall(final ThrowingFunction<JpaBook, T>
transactionalBookFunction) {
+ var book =
repository.allInstances(JpaBook.class).listIterator().next();
+ return transactionalBookFunction.apply(book);
}
}
diff --git
a/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
b/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
index 005ec326d51..2721f98a817 100644
---
a/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
+++
b/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
@@ -33,7 +33,6 @@
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
@@ -83,14 +82,13 @@ void async_using_default_executor_service(final String
displayName, final Execut
runWithNewTransaction(() -> {
var counter = bookmarkService.lookup(bookmark,
Counter.class).orElseThrow();
- var asyncControl = AsyncControl.returning(Counter.class)
+ var asyncControl = AsyncControl.defaults()
.with(executorService);
- wrapperFactory.asyncWrap(counter, asyncControl).increment();
-
- // let's wait max 5 sec to allow executor to complete before
continuing
- asyncControl.waitForResult(5_000, TimeUnit.MILLISECONDS);
- assertTrue(asyncControl.getFuture().isDone()); // verify execution
finished
+ wrapperFactory.asyncWrap(counter, asyncControl)
+ .thenApplyAsync(Counter::increment)
+ .orTimeout(5_000, TimeUnit.MILLISECONDS)
+ .join(); // let's wait max 5 sec to allow executor to complete
before continuing
});
// then
@@ -104,15 +102,17 @@ void async_using_default_executor_service(final String
displayName, final Execut
var counter = bookmarkService.lookup(bookmark,
Counter.class).orElseThrow();
assertThat(counter.getNum()).isEqualTo(1L);
- var asyncControl = AsyncControl.returning(Counter.class)
+ var asyncControl = AsyncControl.defaults()
.with(executorService);
// when
- wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
counter, asyncControl).act();
+ wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
counter, asyncControl)
+ .thenApplyAsync(Counter_bumpUsingMixin::act)
+ // let's wait max 5 sec to allow executor to complete before
continuing
+ .orTimeout(5_000, TimeUnit.MILLISECONDS)
+ .join(); // wait till done
- // let's wait max 5 sec to allow executor to complete before
continuing
- asyncControl.waitForResult(5_000, TimeUnit.MILLISECONDS);
- assertTrue(asyncControl.getFuture().isDone()); // verify execution
finished
+ assertThat(counter.getNum()).isEqualTo(2L); // verify execution
succeeded
});
// then
diff --git
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/CommandArgumentTest.java
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/CommandArgumentTest.java
index 3887ffacd97..516def8849b 100644
---
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/CommandArgumentTest.java
+++
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/CommandArgumentTest.java
@@ -18,7 +18,6 @@
*/
package org.apache.causeway.testdomain.interact;
-import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -105,7 +104,7 @@ void listParam_shouldAllowInvocation() {
var pendingArgs = actionInteraction.startParameterNegotiation().get();
- pendingArgs.setParamValue(0, objectManager.adapt(Arrays.asList(1L, 2L,
3L)));
+ pendingArgs.setParamValue(0, objectManager.adapt(List.of(1L, 2L, 3L)));
var resultOrVeto = actionInteraction.invokeWith(pendingArgs);
assertTrue(resultOrVeto.isSuccess());
@@ -123,12 +122,13 @@ void listParam_shouldAllowAsyncInvocation() throws
InterruptedException, Executi
var commandArgDemo = new CommandArgDemo();
- var control = AsyncControl.returning(CommandResult.class);
+ var control = AsyncControl.defaults();
- wrapperFactory.asyncWrap(commandArgDemo, control)
- .list(Arrays.asList(1L, 2L, 3L));
-
- var stringified = control.future().get(3L,
TimeUnit.DAYS).getResultAsString();
+ var stringified = wrapperFactory.asyncWrap(commandArgDemo, control)
+ .thenApplyAsync(commandResult->commandResult.list(List.of(1L, 2L,
3L)))
+ .orTimeout(3L, TimeUnit.DAYS)
+ .join() // wait till done
+ .getResultAsString();
assertEquals("[1, 2, 3]", stringified);
}
diff --git
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
index c8763437802..53d1141fdc9 100644
---
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
+++
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
@@ -19,7 +19,6 @@
package org.apache.causeway.testdomain.interact;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -118,22 +117,19 @@ void sync_mixin() throws ExecutionException,
InterruptedException, TimeoutExcept
void async_wrapped() throws ExecutionException, InterruptedException,
TimeoutException {
// when
- AsyncControl<Integer> asyncControlForCalculator1 =
AsyncControl.returning(Integer.class);
- StatefulCalculator asyncCalculator1 =
wrapperFactory.asyncWrap(calculator1, asyncControlForCalculator1);
+ var asyncControlForCalculator1 = AsyncControl.defaults();
+ var asyncCalculator1 = wrapperFactory.asyncWrap(calculator1,
asyncControlForCalculator1)
+ .thenApplyAsync(calc->calc.inc(12));
- AsyncControl<Integer> asyncControlForCalculator2 =
AsyncControl.returning(Integer.class);
- StatefulCalculator asyncCalculator2 =
wrapperFactory.asyncWrap(calculator2, asyncControlForCalculator2);
-
- asyncCalculator1.inc(12);
- asyncCalculator2.inc(24);
+ var asyncControlForCalculator2 = AsyncControl.defaults();
+ var asyncCalculator2 = wrapperFactory.asyncWrap(calculator2,
asyncControlForCalculator2)
+ .thenApplyAsync(calc->calc.inc(24));
// then
- Future<Integer> future = asyncControlForCalculator1.getFuture();
- Integer i = future.get(10, TimeUnit.SECONDS);
- Assertions.assertThat(i.intValue()).isEqualTo(12);
+ Assertions.assertThat(asyncCalculator1.orTimeout(10,
TimeUnit.SECONDS).join().intValue()).isEqualTo(12);
Assertions.assertThat(calculator1.getTotal()).isEqualTo(12);
- Assertions.assertThat(asyncControlForCalculator2.getFuture().get(10,
TimeUnit.SECONDS).intValue()).isEqualTo(24);
+ Assertions.assertThat(asyncCalculator2.orTimeout(10,
TimeUnit.SECONDS).join().intValue()).isEqualTo(24);
Assertions.assertThat(calculator2.getTotal()).isEqualTo(24);
}