From d71ecb5fa7d10ff20de9d90bdcb07acbe9cdf329 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 20 Mar 2026 08:16:00 +0100 Subject: [PATCH 01/31] CAUSEWAY-3975: Open Telemetry Integration initial commit Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-3975 --- bom/pom.xml | 12 +++ commons/src/main/java/module-info.java | 2 + .../CausewayObservationInternal.java | 76 +++++++++++++++++++ .../CausewayModuleCoreMetamodel.java | 14 +++- .../runtime/CausewayModuleCoreRuntime.java | 4 +- ...rvice.java => XrayInitializerService.java} | 34 ++------- .../CausewayModuleCoreRuntimeServices.java | 11 +++ .../session/InteractionServiceDefault.java | 50 ++++++++---- .../CausewayModuleViewerWicketModel.java | 14 +++- 9 files changed, 170 insertions(+), 47 deletions(-) create mode 100644 commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java rename core/runtime/src/main/java/org/apache/causeway/core/runtime/events/{MetamodelEventService.java => XrayInitializerService.java} (58%) diff --git a/bom/pom.xml b/bom/pom.xml index bc4aff57104..085eb241c30 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1803,6 +1803,18 @@ identified ${jdom.version} + + org.springframework.boot + spring-boot-starter-opentelemetry + ${spring-boot.version} + + + org.jetbrains + annotations + + + + org.springframework.boot spring-boot-starter-quartz diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java index 398b9432e33..a5d7963e405 100644 --- a/commons/src/main/java/module-info.java +++ b/commons/src/main/java/module-info.java @@ -50,6 +50,7 @@ exports org.apache.causeway.commons.internal.html; exports org.apache.causeway.commons.internal.image; exports org.apache.causeway.commons.internal.ioc; + exports org.apache.causeway.commons.internal.observation; exports org.apache.causeway.commons.internal.os; exports org.apache.causeway.commons.internal.primitives; exports org.apache.causeway.commons.internal.proxy; @@ -67,6 +68,7 @@ requires transitive tools.jackson.core; requires transitive tools.jackson.databind; requires transitive tools.jackson.module.jakarta.xmlbind; + requires transitive micrometer.observation; requires transitive org.jdom2; requires transitive org.jspecify; requires transitive org.jsoup; diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java new file mode 100644 index 00000000000..f4387ed04d9 --- /dev/null +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java @@ -0,0 +1,76 @@ +/* + * 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.commons.internal.observation; + +import java.util.Optional; + +import org.springframework.util.StringUtils; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +/** + * Holder of {@link ObservationRegistry} which comes as a dependency of spring-context. + * + * @apiNote each Causeway module can have its own, using qualifiers and bean factory methods, e.g.: + *
+ * @Bean("causeway-metamodel")
+ * public CausewayObservationInternal causewayObservationInternal(
+ *   Optional observationRegistryOpt) {
+ *   return new CausewayObservationInternal(observationRegistryOpt, "causeway-metamodel");
+ * }
+ *  
+ */ +public record CausewayObservationInternal( + ObservationRegistry observationRegistry, + String module) { + + public CausewayObservationInternal( + final Optional observationRegistryOpt, + final String module) { + this(observationRegistryOpt.orElse(ObservationRegistry.NOOP), module); + } + + public CausewayObservationInternal { + observationRegistry = observationRegistry!=null + ? observationRegistry + : ObservationRegistry.NOOP; + module = StringUtils.hasText(module) ? module : "unknown_module"; + } + + public boolean isNoop() { + return observationRegistry.isNoop(); + } + + public Observation createNotStarted(final Class bean, final String name) { + return Observation.createNotStarted(name, observationRegistry) + .lowCardinalityKeyValue("module", module) + .highCardinalityKeyValue("bean", bean.getSimpleName()); + } + + @FunctionalInterface + public interface ObservationProvider { + Observation get(String name); + } + + public ObservationProvider provider(final Class bean) { + return name->createNotStarted(bean, name); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index f0b1049c17e..4f6b1a6b210 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import jakarta.inject.Provider; @@ -42,6 +43,7 @@ import org.apache.causeway.commons.functional.Either; import org.apache.causeway.commons.functional.Railway; import org.apache.causeway.commons.functional.Try; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.commons.semantics.CollectionSemantics; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.CausewayModuleCoreConfig; @@ -116,7 +118,9 @@ import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault; import org.apache.causeway.core.security.CausewayModuleCoreSecurity; -@Configuration +import io.micrometer.observation.ObservationRegistry; + +@Configuration(proxyBeanMethods = false) @Import({ // Modules CausewayModuleApplib.class, @@ -200,7 +204,7 @@ // standalone validators LogicalTypeMalformedValidator.class, - + // menubar contributions MetamodelInspectMenu.class }) @@ -261,4 +265,10 @@ public ValueCodec valueCodec( return new ValueCodec(bookmarkService, valueSemanticsResolverProvider); } + @Bean("causeway-metamodel") + public CausewayObservationInternal causewayObservationInternal( + final Optional observationRegistryOpt) { + return new CausewayObservationInternal(observationRegistryOpt, "causeway-metamodel"); + } + } diff --git a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java index 442e0008e44..220ff51c8c3 100644 --- a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java +++ b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java @@ -23,7 +23,7 @@ import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; -import org.apache.causeway.core.runtime.events.MetamodelEventService; +import org.apache.causeway.core.runtime.events.XrayInitializerService; import org.apache.causeway.core.transaction.CausewayModuleCoreTransaction; @Configuration @@ -34,7 +34,7 @@ CausewayModuleCoreTransaction.class, // @Service's - MetamodelEventService.class, + XrayInitializerService.class, }) public class CausewayModuleCoreRuntime { diff --git a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java similarity index 58% rename from core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java rename to core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java index d02a988f772..4320e218ece 100644 --- a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/MetamodelEventService.java +++ b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/XrayInitializerService.java @@ -18,49 +18,31 @@ */ package org.apache.causeway.core.runtime.events; -import jakarta.annotation.Priority; -import jakarta.inject.Inject; import jakarta.inject.Named; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; -import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.applib.events.metamodel.MetamodelEvent; +import org.apache.causeway.applib.events.metamodel.MetamodelListener; import org.apache.causeway.applib.services.confview.ConfigurationViewService; -import org.apache.causeway.applib.services.eventbus.EventBusService; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; -/** - * - * @since 2.0 - * @implNote Listeners to runtime events can only reliably receive these after the - * post-construct phase has finished and before the pre-destroy phase has begun. - */ @Service -@Named(CausewayModuleCoreRuntime.NAMESPACE + ".MetamodelEventService") -@Priority(PriorityPrecedence.MIDPOINT) -@Qualifier("Default") -public class MetamodelEventService { - - @Inject - private EventBusService eventBusService; +@Named(CausewayModuleCoreRuntime.NAMESPACE + ".XrayInitializerService") +public class XrayInitializerService implements MetamodelListener { @Autowired(required = false) private ConfigurationViewService configurationService; - public void fireBeforeMetamodelLoading() { - + @Override + public void onMetamodelAboutToBeLoaded() { if(configurationService!=null) { _Xray.addConfiguration(configurationService); } - - eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING); } - public void fireAfterMetamodelLoaded() { - eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED); + @Override + public void onMetamodelLoaded() { + // no-op } - } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java index bee6e2db2ce..ae003667556 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java @@ -18,6 +18,8 @@ */ package org.apache.causeway.core.runtimeservices; +import java.util.Optional; + import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -29,6 +31,7 @@ import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.bookmark.HmacAuthority; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault; @@ -73,6 +76,8 @@ import org.apache.causeway.core.runtimeservices.xml.XmlServiceDefault; import org.apache.causeway.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefault; +import io.micrometer.observation.ObservationRegistry; + @Configuration(proxyBeanMethods = false) @Import({ // Modules @@ -151,4 +156,10 @@ public HmacAuthority fallbackHmacAuthority() { } } + @Bean("causeway-runtimeservices") + public CausewayObservationInternal causewayObservationInternal( + final Optional observationRegistryOpt) { + return new CausewayObservationInternal(observationRegistryOpt, "causeway-runtimeservices"); + } + } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 810a262b682..10c70fe6869 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -30,6 +30,8 @@ import jakarta.inject.Named; import jakarta.inject.Provider; +import org.jspecify.annotations.NonNull; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.event.ContextRefreshedEvent; @@ -38,8 +40,10 @@ import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.annotation.Programmatic; +import org.apache.causeway.applib.events.metamodel.MetamodelEvent; import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.command.Command; +import org.apache.causeway.applib.services.eventbus.EventBusService; import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; @@ -57,17 +61,17 @@ import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; import org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor; import org.apache.causeway.core.interaction.scope.InteractionScopeLifecycleHandler; import org.apache.causeway.core.interaction.session.CausewayInteraction; import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; -import org.apache.causeway.core.runtime.events.MetamodelEventService; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring; import org.apache.causeway.core.security.authentication.InteractionContextFactory; -import org.jspecify.annotations.NonNull; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -93,7 +97,8 @@ public class InteractionServiceDefault // ThreadLocal would be considered bad practice and instead should be managed using the TransactionSynchronization mechanism. final ThreadLocal> interactionLayerStack = ThreadLocal.withInitial(Stack::new); - final MetamodelEventService runtimeEventService; + final EventBusService eventBusService; + final ObservationProvider observationProvider; final Provider specificationLoaderProvider; final ServiceInjector serviceInjector; @@ -108,7 +113,9 @@ public class InteractionServiceDefault @Inject public InteractionServiceDefault( - final MetamodelEventService runtimeEventService, + final EventBusService eventBusService, + @Qualifier("causeway-runtimeservices") + final CausewayObservationInternal observation, final Provider specificationLoaderProvider, final ServiceInjector serviceInjector, final TransactionServiceSpring transactionServiceSpring, @@ -116,7 +123,8 @@ public InteractionServiceDefault( final Provider commandPublisherProvider, final ConfigurableBeanFactory beanFactory, final InteractionIdGenerator interactionIdGenerator) { - this.runtimeEventService = runtimeEventService; + this.eventBusService = eventBusService; + this.observationProvider = observation.provider(getClass()); this.specificationLoaderProvider = specificationLoaderProvider; this.serviceInjector = serviceInjector; this.transactionServiceSpring = transactionServiceSpring; @@ -124,19 +132,34 @@ public InteractionServiceDefault( this.commandPublisherProvider = commandPublisherProvider; this.beanFactory = beanFactory; this.interactionIdGenerator = interactionIdGenerator; - this.interactionScopeLifecycleHandler = InteractionScopeBeanFactoryPostProcessor.lookupScope(beanFactory); } @EventListener public void init(final ContextRefreshedEvent event) { - log.info("Initialising Causeway System"); log.info("working directory: {}", new File(".").getAbsolutePath()); - runtimeEventService.fireBeforeMetamodelLoading(); + observationProvider.get("Initialising Causeway System") + .observe(()->{ + observationProvider.get("Notify BEFORE_METAMODEL_LOADING Listeners") + .observe(()->{ + eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING); + }); + + observationProvider.get("Initialising Causeway Metamodel") + .observe(()->{ + initMetamodel(specificationLoaderProvider.get()); + }); + + observationProvider.get("Notify AFTER_METAMODEL_LOADED Listeners") + .observe(()->{ + eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED); + }); + }); + } - var specificationLoader = specificationLoaderProvider.get(); + private void initMetamodel(final SpecificationLoader specificationLoader) { var taskList = _ConcurrentTaskList.named("CausewayInteractionFactoryDefault Init") .addRunnable("SpecificationLoader::createMetaModel", specificationLoader::createMetaModel) @@ -161,9 +184,6 @@ public void init(final ContextRefreshedEvent event) { //throw _Exceptions.unrecoverable("Validation FAILED"); } } - - runtimeEventService.fireAfterMetamodelLoaded(); - } @Override @@ -191,10 +211,9 @@ public InteractionLayer openInteraction( .map(currentInteractionContext -> Objects.equals(currentInteractionContext, interactionContextToUse)) .orElse(false); - if(reuseCurrentLayer) { + if(reuseCurrentLayer) // we are done, just return the stack's top return interactionLayerStack.get().peek(); - } var interactionLayer = new InteractionLayer(causewayInteraction, interactionContextToUse); @@ -465,9 +484,8 @@ private void closeInteractionLayerStackDownToStackSize(final int downToStackSize private CausewayInteraction getInternalInteractionElseFail() { var interaction = currentInteractionElseFail(); - if(interaction instanceof CausewayInteraction) { + if(interaction instanceof CausewayInteraction) return (CausewayInteraction) interaction; - } throw _Exceptions.unrecoverable("the framework does not recognize " + "this implementation of an Interaction: %s", interaction.getClass().getName()); } diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java index 6db06646573..7e3bca0fbd0 100644 --- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java +++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java @@ -18,18 +18,30 @@ */ package org.apache.causeway.viewer.wicket.model; +import java.util.Optional; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.core.webapp.CausewayModuleCoreWebapp; +import io.micrometer.observation.ObservationRegistry; + /** * @since 1.x {@index} */ -@Configuration +@Configuration(proxyBeanMethods = false) @Import({ // Modules CausewayModuleCoreWebapp.class, }) public class CausewayModuleViewerWicketModel { + + @Bean("causeway-wicketviewer") + public CausewayObservationInternal causewayObservationInternal( + final Optional observationRegistryOpt) { + return new CausewayObservationInternal(observationRegistryOpt, "causeway-wicketviewer"); + } } From 743a259e5960026097735ed7f68e8de9745fe87f Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 20 Mar 2026 22:51:24 +0100 Subject: [PATCH 02/31] CAUSEWAY-3975: work on simplified InteractionService hope to better integrate with micrometer API --- .../services/iactnlayer/InteractionLayer.java | 40 +- .../iactnlayer/InteractionLayerStack.java | 93 +++ .../iactnlayer/InteractionService.java | 125 ++-- .../session/InteractionServiceDefault.java | 134 ++-- .../core/runtimeservices/session/_Xray.java | 22 +- .../InteractionService_forTesting.java | 43 +- .../BackgroundService_IntegTestAbstract.java | 47 +- .../CommandLog_IntegTestAbstract.java | 598 +++++++++--------- .../ExecutionLog_IntegTestAbstract.java | 235 +++---- .../ExecutionOutbox_IntegTestAbstract.java | 21 +- .../AuditTrail_IntegTestAbstract.java | 246 +++---- .../conf/Configuration_usingWicket.java | 13 +- ...CmdExecAuditSessLog_IntegTestAbstract.java | 208 +++--- .../integtest/Layout_Counter_IntegTest.java | 14 +- .../applib/CausewayInteractionHandler.java | 8 +- .../applib/NoPermissionChecks.java | 2 +- .../applib/UserMementoRefiners.java | 3 +- .../AuthenticatedWebSessionForCauseway.java | 152 ++--- .../viewer/integration/RequestCycle2.java | 84 +++ .../integration/SessionAuthenticator.java | 61 ++ .../WebRequestCycleForCauseway.java | 68 +- ...CausewayWicketAjaxRequestListenerUtil.java | 7 +- .../wicketapp/CausewayWicketApplication.java | 8 +- 23 files changed, 1254 insertions(+), 978 deletions(-) create mode 100644 api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java index 170931239a0..13da3e007fb 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java @@ -18,36 +18,54 @@ */ package org.apache.causeway.applib.services.iactnlayer; +import org.jspecify.annotations.Nullable; + import org.apache.causeway.applib.services.iactn.Interaction; /** * Binds an {@link Interaction} ("what" is being executed) with * an {@link InteractionContext} ("who" is executing, "when" and "where"). * - *

- * {@link InteractionLayer}s are so called because they may be nested (held in a stack). For example the + *

{@link InteractionLayer}s are so called because they may be nested (held in a stack). For example the * {@link org.apache.causeway.applib.services.sudo.SudoService} creates a new temporary layer with a different * {@link InteractionContext#getUser() user}, while fixtures that mock the clock switch out the * {@link InteractionContext#getClock() clock}. - *

* - *

- * The stack of layers is per-thread, managed by {@link InteractionService} as a thread-local). - *

+ *

The stack of layers is per-thread, managed by {@link InteractionService} as a thread-local). * * @since 2.0 {@index} */ public record InteractionLayer( + @Nullable InteractionLayer parent, /** - * Current thread's {@link Interaction} : "what" is being executed + * Current thread's {@link Interaction} : WHAT is being executed */ Interaction interaction, /** - * "who" is performing this {@link #getInteraction()}, also - * "when" and "where". + * WHO is performing this {@link #getInteraction()}, also + * WHEN and WHERE. */ - InteractionContext interactionContext - ) { + InteractionContext interactionContext) { + + public boolean isRoot() { + return parent==null; + } + + public int parentCount() { + return parent!=null + ? 1 + parent.parentCount() + : 0; + } + + public int totalLayerCount() { + return 1 + parentCount(); + } + + public InteractionLayer rootLayer() { + return parent!=null + ? parent.rootLayer() + : this; + } } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java new file mode 100644 index 00000000000..3791d22d494 --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java @@ -0,0 +1,93 @@ +/* + * 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.iactnlayer; + +import java.util.Optional; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.apache.causeway.applib.services.iactn.Interaction; + +public final class InteractionLayerStack { + + // TODO: reading the javadoc for TransactionSynchronizationManager and looking at the implementations + // of TransactionSynchronization (in particular SpringSessionSynchronization), I suspect that this + // ThreadLocal would be considered bad practice and instead should be managed using the TransactionSynchronization mechanism. + private final ThreadLocal threadLocalLayer = new ThreadLocal<>(); + + public Optional currentLayer() { + return Optional.ofNullable(threadLocalLayer.get()); + } + + public InteractionLayer push( + final Interaction interaction, + final InteractionContext interactionContext) { + var parent = currentLayer().orElse(null); + var newLayer = new InteractionLayer(parent, interaction, interactionContext); + threadLocalLayer.set(newLayer); + return newLayer; + } + + public void clear() { + threadLocalLayer.remove(); + } + + public boolean isEmpty() { + return threadLocalLayer.get()==null; + } + + public int size() { + return currentLayer() + .map(InteractionLayer::totalLayerCount) + .orElse(0); + } + + @Nullable + public InteractionLayer peek() { + return threadLocalLayer.get(); + } + + @Nullable + public InteractionLayer pop() { + var current = threadLocalLayer.get(); + return set(current != null + ? current.parent() + : null); + } + + public void popWhile(final Predicate condition) { + while(!isEmpty()) { + if(!condition.test(peek())) return; + pop(); + } + } + + // -- HELPER + + private InteractionLayer set(@Nullable final InteractionLayer layer) { + if(layer != null) { + threadLocalLayer.set(layer); + } else { + threadLocalLayer.remove(); + } + return layer; + } + +} diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java index d133392bb42..60845096603 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java @@ -19,12 +19,13 @@ package org.apache.causeway.applib.services.iactnlayer; import java.util.concurrent.Callable; +import java.util.function.Consumer; + +import org.jspecify.annotations.NonNull; import org.apache.causeway.commons.functional.ThrowingRunnable; import org.apache.causeway.commons.functional.Try; -import org.jspecify.annotations.NonNull; - /** * A low-level service to programmatically create a short-lived interaction or session. * @@ -55,42 +56,6 @@ */ public interface InteractionService extends InteractionLayerTracker { - /** - * If present, reuses the current top level {@link InteractionLayer}, otherwise creates a new - * anonymous one. - * - * @see #openInteraction(InteractionContext) - */ - InteractionLayer openInteraction(); - - /** - * Returns a new or reused {@link InteractionLayer} that is a holder of the {@link InteractionContext} - * on top of the current thread's interaction layer stack. - * - *

- * If available reuses an existing {@link InteractionContext}, otherwise creates a new one. - *

- * - *

- * The {@link InteractionLayer} represents a user's span of activities interacting with - * the application. These can be stacked (usually temporarily), for example for a sudo - * session or to mock the clock. The stack is later closed using {@link #closeInteractionLayers()}. - *

- * - * @param interactionContext - * - * @apiNote if the current {@link InteractionLayer} (if any) has an {@link InteractionContext} that - * equals that of the given one, as an optimization, no new layer is pushed onto the stack; - * instead the current one is returned - */ - InteractionLayer openInteraction( - @NonNull InteractionContext interactionContext); - - /** - * closes all open {@link InteractionLayer}(s) as stacked on the current thread - */ - void closeInteractionLayers(); - /** * @return whether the calling thread is within the context of an open {@link InteractionLayer} */ @@ -205,33 +170,69 @@ default Try runAnonymousAndCatch( } /** - * Primarily for testing, closes the current interaction and opens a new one. - * - *

- * In tests, this is a good way to simulate multiple interactions within a scenario. If you use the popular - * given/when/then structure, consider using at the end of each "given" or "when" block. - *

- * - * @see #closeInteractionLayers() - * @see #openInteraction() - * @see #nextInteraction(InteractionContext) + * closes all open {@link InteractionLayer}(s) as stacked on the current thread */ - default InteractionLayer nextInteraction() { - closeInteractionLayers(); - return openInteraction(); + void closeInteractionLayers(); + + public interface TestSupport { + T model(); + /** + * Primarily for testing, closes the current interaction and opens a new one. + * + *

+ * In tests, this is a good way to simulate multiple interactions within a scenario. If you use the popular + * given/when/then structure, consider using at the end of each "given" or "when" block. + *

+ * + * @see #closeInteractionLayers() + * @see #openInteraction() + * @see #nextInteraction(InteractionContext) + */ + void nextInteraction(final Consumer callback); + /** + * Primarily for testing, closes the current interaction and opens a new one with the specified + * {@link InteractionContext}. + * + * @see #closeInteractionLayers() + * @see #openInteraction(InteractionContext) + * @see #nextInteraction() + */ + void nextInteraction(final InteractionContext interactionContext, final Consumer callback); + /** + * If present, reuses the current top level {@link InteractionLayer}, otherwise creates a new + * anonymous one. + * + * @see #openInteraction(InteractionContext) + */ + InteractionLayer openInteraction(); + /** + * Returns a new or reused {@link InteractionLayer} that is a holder of the {@link InteractionContext} + * on top of the current thread's interaction layer stack. + * + *

+ * If available reuses an existing {@link InteractionContext}, otherwise creates a new one. + *

+ * + *

+ * The {@link InteractionLayer} represents a user's span of activities interacting with + * the application. These can be stacked (usually temporarily), for example for a sudo + * session or to mock the clock. The stack is later closed using {@link #closeInteractionLayers()}. + *

+ * + * @param interactionContext + * + * @apiNote if the current {@link InteractionLayer} (if any) has an {@link InteractionContext} that + * equals that of the given one, as an optimization, no new layer is pushed onto the stack; + * instead the current one is returned + */ + InteractionLayer openInteraction( + @NonNull InteractionContext interactionContext); } - /** - * Primarily for testing, closes the current interaction and opens a new one with the specified - * {@link InteractionContext}. - * - * @see #closeInteractionLayers() - * @see #openInteraction(InteractionContext) - * @see #nextInteraction() - */ - default InteractionLayer nextInteraction(final InteractionContext interactionContext) { - closeInteractionLayers(); - return openInteraction(interactionContext); + TestSupport testSupport(T model); + + default TestSupport testSupport() { + return testSupport(null); } } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 10c70fe6869..7f2f854fa20 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -21,9 +21,9 @@ import java.io.File; import java.util.Objects; import java.util.Optional; -import java.util.Stack; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.function.Consumer; import jakarta.annotation.Priority; import jakarta.inject.Inject; @@ -47,6 +47,7 @@ import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; +import org.apache.causeway.applib.services.iactnlayer.InteractionLayerStack; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.inject.ServiceInjector; @@ -55,7 +56,6 @@ import org.apache.causeway.applib.util.schema.InteractionDtoUtils; import org.apache.causeway.applib.util.schema.InteractionsDtoUtils; import org.apache.causeway.commons.functional.ThrowingRunnable; -import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; import org.apache.causeway.commons.internal.debug._Probe; @@ -72,7 +72,10 @@ import org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring; import org.apache.causeway.core.security.authentication.InteractionContextFactory; +import lombok.Getter; import lombok.SneakyThrows; +import lombok.Value; +import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; /** @@ -92,10 +95,7 @@ public class InteractionServiceDefault InteractionService, InteractionLayerTracker { - // TODO: reading the javadoc for TransactionSynchronizationManager and looking at the implementations - // of TransactionSynchronization (in particular SpringSessionSynchronization), I suspect that this - // ThreadLocal would be considered bad practice and instead should be managed using the TransactionSynchronization mechanism. - final ThreadLocal> interactionLayerStack = ThreadLocal.withInitial(Stack::new); + final InteractionLayerStack layerStack = new InteractionLayerStack(); final EventBusService eventBusService; final ObservationProvider observationProvider; @@ -159,6 +159,7 @@ public void init(final ContextRefreshedEvent event) { }); } + //TODO this is a metamodel concern, why is it in runtime services? private void initMetamodel(final SpecificationLoader specificationLoader) { var taskList = _ConcurrentTaskList.named("CausewayInteractionFactoryDefault Init") @@ -188,69 +189,63 @@ private void initMetamodel(final SpecificationLoader specificationLoader) { @Override public int getInteractionLayerCount() { - return interactionLayerStack.get().size(); + return layerStack.size(); } - @Override - public InteractionLayer openInteraction() { + private InteractionLayer openInteraction() { return currentInteractionLayer() // or else create an anonymous authentication layer .orElseGet(()->openInteraction(InteractionContextFactory.anonymous())); } - @Override - public InteractionLayer openInteraction( - final @NonNull InteractionContext interactionContextToUse) { - - var causewayInteraction = getOrCreateCausewayInteraction(); + private InteractionLayer openInteraction(final @NonNull InteractionContext interactionContextToUse) { // check whether we should reuse any current interactionLayer, // that is, if current authentication and authToUse are equal - var reuseCurrentLayer = currentInteractionContext() .map(currentInteractionContext -> Objects.equals(currentInteractionContext, interactionContextToUse)) .orElse(false); - if(reuseCurrentLayer) // we are done, just return the stack's top - return interactionLayerStack.get().peek(); + return currentInteractionLayerElseFail(); - var interactionLayer = new InteractionLayer(causewayInteraction, interactionContextToUse); + var newInteractionLayer = observationProvider.get("New Interaction Layer") + .highCardinalityKeyValue("stackSize", ""+getInteractionLayerCount()) + .observe(()->{ - interactionLayerStack.get().push(interactionLayer); + var causewayInteraction = currentInteractionLayer() + .map(InteractionLayer::interaction) + .map(it->(CausewayInteraction)it) + .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId())); + var interactionLayer = layerStack.push(causewayInteraction, interactionContextToUse); - if(isAtTopLevel()) { - transactionServiceSpring.onOpen(causewayInteraction); - interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); - } + if(isAtTopLevel()) { + transactionServiceSpring.onOpen(causewayInteraction); + interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); + } + + return interactionLayer; + }); if(log.isDebugEnabled()) { log.debug("new interaction layer created (interactionId={}, total-layers-on-stack={}, {})", currentInteraction().map(Interaction::getInteractionId).orElse(null), - interactionLayerStack.get().size(), + getInteractionLayerCount(), _Probe.currentThreadId()); } if(XrayUi.isXrayEnabled()) { - _Xray.newInteractionLayer(interactionLayerStack.get()); + _Xray.newInteractionLayer(newInteractionLayer); } - return interactionLayer; - } - - private CausewayInteraction getOrCreateCausewayInteraction() { - - final Stack interactionLayers = interactionLayerStack.get(); - return interactionLayers.isEmpty() - ? new CausewayInteraction(interactionIdGenerator.interactionId()) - : _Casts.uncheckedCast(interactionLayers.firstElement().interaction()); + return newInteractionLayer; } @Override public void closeInteractionLayers() { log.debug("about to close the interaction stack (interactionId={}, total-layers-on-stack={}, {})", currentInteraction().map(Interaction::getInteractionId).orElse(null), - interactionLayerStack.get().size(), + layerStack.size(), _Probe.currentThreadId()); // @@ -262,15 +257,12 @@ public void closeInteractionLayers() { @Override public Optional currentInteractionLayer() { - var stack = interactionLayerStack.get(); - return stack.isEmpty() - ? Optional.empty() - : Optional.of(stack.lastElement()); + return layerStack.currentLayer(); } @Override public boolean isInInteraction() { - return !interactionLayerStack.get().isEmpty(); + return !layerStack.isEmpty(); } // -- AUTHENTICATED EXECUTION @@ -281,7 +273,7 @@ public R call( final @NonNull InteractionContext interactionContext, final @NonNull Callable callable) { - final int stackSizeWhenEntering = interactionLayerStack.get().size(); + final int stackSizeWhenEntering = layerStack.size(); openInteraction(interactionContext); try { return callInternal(callable); @@ -302,7 +294,7 @@ public void run( final @NonNull InteractionContext interactionContext, final @NonNull ThrowingRunnable runnable) { - final int stackSizeWhenEntering = interactionLayerStack.get().size(); + final int stackSizeWhenEntering = layerStack.size(); openInteraction(interactionContext); try { runInternal(runnable); @@ -374,8 +366,7 @@ private void runInternal(final @NonNull ThrowingRunnable runnable) { } private void requestRollback(final Throwable cause) { - var stack = interactionLayerStack.get(); - if(stack.isEmpty()) { + if(layerStack.isEmpty()) { // seeing this code-path, when the corresponding runnable/callable // by itself causes the interaction stack to be closed log.warn("unexpected state: missing interaction (layer) on interaction rollback; " @@ -384,12 +375,12 @@ private void requestRollback(final Throwable cause) { cause.getMessage()); return; } - var interaction = _Casts.uncheckedCast(stack.get(0).interaction()); + var interaction = (CausewayInteraction) layerStack.peek().rootLayer().interaction(); transactionServiceSpring.requestRollback(interaction); } private boolean isAtTopLevel() { - return interactionLayerStack.get().size()==1; + return layerStack.size()==1; } @SneakyThrows @@ -454,30 +445,31 @@ private void preInteractionClosed(final CausewayInteraction interaction) { } private void closeInteractionLayerStackDownToStackSize(final int downToStackSize) { + if(layerStack.isEmpty()) return; + if(downToStackSize<0) throw new IllegalArgumentException("required non-negative"); log.debug("about to close interaction stack down to size {} (interactionId={}, total-layers-on-stack={}, {})", downToStackSize, currentInteraction().map(Interaction::getInteractionId).orElse(null), - interactionLayerStack.get().size(), + layerStack.size(), _Probe.currentThreadId()); - var stack = interactionLayerStack.get(); try { - while(stack.size()>downToStackSize) { + layerStack.popWhile(currentLayer->{ + if(!(layerStack.size()>downToStackSize)) return false; if(isAtTopLevel()) { // keep the stack unmodified yet, to allow for callbacks to properly operate - - preInteractionClosed(_Casts.uncheckedCast(stack.peek().interaction())); + preInteractionClosed((CausewayInteraction)currentLayer.interaction()); } - _Xray.closeInteractionLayer(stack); - stack.pop(); - } + _Xray.closeInteractionLayer(currentLayer); + return true; + }); } finally { // preInteractionClosed above could conceivably throw an exception, so we'll tidy up our threadlocal // here to ensure everything is cleaned up if(downToStackSize == 0) { // cleanup thread-local - interactionLayerStack.remove(); + layerStack.clear(); } } } @@ -524,4 +516,36 @@ public void completeAndPublishCurrentCommand() { interaction.clear(); } + @Value + static final class TestSupportImpl implements TestSupport { + @Getter @Accessors(fluent = true) final T model; + final InteractionServiceDefault interactionService; + + @Override + public void nextInteraction(final Consumer callback) { + interactionService.closeInteractionLayers(); + interactionService.openInteraction(); + callback.accept(model); + } + @Override + public void nextInteraction(final InteractionContext interactionContext, final Consumer callback) { + interactionService.closeInteractionLayers(); + interactionService.openInteraction(interactionContext); + callback.accept(model); + } + @Override + public InteractionLayer openInteraction() { + return interactionService.openInteraction(); + } + @Override + public InteractionLayer openInteraction(@NonNull final InteractionContext interactionContext) { + return interactionService.openInteraction(interactionContext); + } + } + + @Override + public TestSupport testSupport(final T model) { + return new TestSupportImpl<>(model, this); + } + } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Xray.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Xray.java index d040d4c3db8..7a884c5dcc7 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Xray.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Xray.java @@ -18,8 +18,6 @@ */ package org.apache.causeway.core.runtimeservices.session; -import java.util.Stack; - import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; import org.apache.causeway.commons.internal.debug._XrayEvent; import org.apache.causeway.commons.internal.debug.xray.XrayDataModel; @@ -30,16 +28,15 @@ //@Slf4j final class _Xray { - static void newInteractionLayer(final Stack afterEnter) { + static void newInteractionLayer(final InteractionLayer afterEnter) { - if(!XrayUi.isXrayEnabled()) { + if(!XrayUi.isXrayEnabled()) return; - } // make defensive copies, so can use in another thread - final int authStackSize = afterEnter.size(); - var interactionId = afterEnter.peek().interaction().getInteractionId(); - var executionContext = afterEnter.peek().interactionContext(); + final int authStackSize = afterEnter.totalLayerCount(); + var interactionId = afterEnter.interaction().getInteractionId(); + var executionContext = afterEnter.interactionContext(); _XrayEvent.interactionOpen("open interaction %s", interactionId); @@ -86,14 +83,13 @@ static void newInteractionLayer(final Stack afterEnter) { } - public static void closeInteractionLayer(final Stack beforeClose) { + public static void closeInteractionLayer(final InteractionLayer beforeClose) { - if(!XrayUi.isXrayEnabled()) { + if(!XrayUi.isXrayEnabled()) return; - } - final int authStackSize = beforeClose.size(); - var interactionId = beforeClose.peek().interaction().getInteractionId(); + final int authStackSize = beforeClose.totalLayerCount(); + var interactionId = beforeClose.interaction().getInteractionId(); var sequenceId = XrayUtil.sequenceId(interactionId); _XrayEvent.interactionClose("close interaction %s", interactionId); diff --git a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java index 3694824855a..95f18fcb0f2 100644 --- a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java +++ b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java @@ -19,21 +19,22 @@ package org.apache.causeway.core.security._testing; import java.util.Optional; -import java.util.Stack; import java.util.UUID; import java.util.concurrent.Callable; import java.util.function.Function; +import org.jspecify.annotations.NonNull; + import org.apache.causeway.applib.services.command.Command; import org.apache.causeway.applib.services.iactn.Execution; import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; +import org.apache.causeway.applib.services.iactnlayer.InteractionLayerStack; import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.commons.functional.ThrowingRunnable; -import org.jspecify.annotations.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; @@ -44,29 +45,28 @@ public class InteractionService_forTesting implements InteractionService { - private Stack interactionLayers = new Stack<>(); + final InteractionLayerStack layerStack = new InteractionLayerStack(); - @Override - public InteractionLayer openInteraction() { +// @Override + private InteractionLayer openInteraction() { final UserMemento userMemento = UserMemento.system(); return openInteraction(InteractionContext.ofUserWithSystemDefaults(userMemento)); } - @Override - public InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) { +// @Override + private InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) { final Interaction interaction = new Interaction_forTesting(); - return interactionLayers.push( - new InteractionLayer(interaction, interactionContext)); + return layerStack.push(interaction, interactionContext); } @Override public void closeInteractionLayers() { - interactionLayers.clear(); + layerStack.clear(); } @Override public boolean isInInteraction() { - return interactionLayers.size()>0; + return !layerStack.isEmpty(); } @Override public Optional getInteractionId() { @@ -76,13 +76,13 @@ public boolean isInInteraction() { } @Override public Optional currentInteractionLayer() { - return interactionLayers.isEmpty() + return layerStack.isEmpty() ? Optional.empty() - : Optional.of(interactionLayers.peek()); + : Optional.of(layerStack.peek()); } @Override public int getInteractionLayerCount() { - return interactionLayers.size(); + return layerStack.size(); } @Override @SneakyThrows @@ -91,7 +91,7 @@ public R call(final @NonNull InteractionContext interactionContext, final @N openInteraction(interactionContext); return callable.call(); } finally { - interactionLayers.pop(); + layerStack.pop(); } } @@ -101,7 +101,7 @@ public void run(final @NonNull InteractionContext interactionContext, final @Non openInteraction(interactionContext); runnable.run(); } finally { - interactionLayers.pop(); + layerStack.pop(); } } @@ -111,7 +111,7 @@ public void runAnonymous(final @NonNull ThrowingRunnable runnable) { openInteraction(); runnable.run(); } finally { - interactionLayers.pop(); + layerStack.pop(); } } @@ -121,7 +121,7 @@ public R callAnonymous(final @NonNull Callable callable) { openInteraction(); return callable.call(); } finally { - interactionLayers.pop(); + layerStack.pop(); } } @@ -136,6 +136,11 @@ static class Interaction_forTesting implements Interaction { @Override public Command getCommand() { return null; } @Override public Execution getCurrentExecution() { return null; } @Override public Execution getPriorExecution() { return null; } - }; + } + + @Override + public TestSupport testSupport(final T model) { + throw new UnsupportedOperationException(); + } } 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 cb09a0b956a..49124ebbc3a 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 @@ -38,6 +38,7 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy; import org.apache.causeway.applib.services.xactn.TransactionService; @@ -69,6 +70,7 @@ public abstract class BackgroundService_IntegTestAbstract extends CausewayIntegr JobExecutionContext mockQuartzJobExecutionContext = Mockito.mock(JobExecutionContext.class); Bookmark bookmark; + private TestSupport testSupport; protected abstract T newCounter(String name); @@ -87,6 +89,7 @@ static void reset_environment() { @BeforeEach void setup_counter() { + this.testSupport = interactionService.testSupport(); transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { counterRepository.removeAll(); @@ -211,27 +214,29 @@ void using_background_service() { // when (simulate quartz running in the background) runBackgroundCommandsJob.execute(mockQuartzJobExecutionContext); - interactionService.nextInteraction(); - - // then bumped - transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { - var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow(); - assertThat(counter.getNum()).isEqualTo(1L); - }).ifFailureFail(); - - // and marked as started and completed - transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { - var after = commandLogEntryRepository.findAll(); - assertThat(after).hasSize(1); - CommandLogEntry commandLogEntryAfter = after.get(0); - - assertThat(commandLogEntryAfter) - .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getCompletedAt()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getResult()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getResultSummary()).isNotNull()) // changed - ; - }).ifFailureFail(); + testSupport.nextInteraction(ia->{ + + // then bumped + transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { + var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow(); + assertThat(counter.getNum()).isEqualTo(1L); + }).ifFailureFail(); + + // and marked as started and completed + transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { + var after = commandLogEntryRepository.findAll(); + assertThat(after).hasSize(1); + CommandLogEntry commandLogEntryAfter = after.get(0); + + assertThat(commandLogEntryAfter) + .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getCompletedAt()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getResult()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getResultSummary()).isNotNull()) // changed + ; + }).ifFailureFail(); + + }); } 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 2a5d2951337..8d29e0e4052 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 @@ -39,6 +39,7 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -58,6 +59,17 @@ public abstract class CommandLog_IntegTestAbstract extends CausewayIntegrationTestAbstract { + @Inject CommandLogEntryRepository commandLogEntryRepository; + @Inject SudoService sudoService; + @Inject ClockService clockService; + @Inject InteractionService interactionService; + @Inject InteractionLayerTracker interactionLayerTracker; + @Inject CounterRepository counterRepository; + @Inject WrapperFactory wrapperFactory; + @Inject BookmarkService bookmarkService; + @Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry; + + @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -65,23 +77,25 @@ static void beforeAll() { Counter counter1; Counter counter2; + private TestSupport testSupport; @BeforeEach void beforeEach() { - interactionService.nextInteraction(); + this.testSupport = interactionService.testSupport(); + testSupport.nextInteraction(ia->{ + counterRepository.removeAll(); + commandLogEntryRepository.removeAll(); - counterRepository.removeAll(); - commandLogEntryRepository.removeAll(); + assertThat(counterRepository.find()).isEmpty(); - assertThat(counterRepository.find()).isEmpty(); + counter1 = counterRepository.persist(newCounter("counter-1")); + counter2 = counterRepository.persist(newCounter("counter-2")); - counter1 = counterRepository.persist(newCounter("counter-1")); - counter2 = counterRepository.persist(newCounter("counter-2")); + assertThat(counterRepository.find()).hasSize(2); - assertThat(counterRepository.find()).hasSize(2); - - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); + }); } protected abstract T newCounter(String name); @@ -91,33 +105,34 @@ void invoke_mixin() { // when wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - interactionService.nextInteraction(); - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingMixin"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNotNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - assertThat(commandLogEntry.getCommandDto()).isNotNull(); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + testSupport.nextInteraction(ia->{ + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingMixin"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNotNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + assertThat(commandLogEntry.getCommandDto()).isNotNull(); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + }); } @Test @@ -125,57 +140,56 @@ void invoke_direct() { // when wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); - interactionService.nextInteraction(); - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNotNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - assertThat(commandLogEntry.getCommandDto()).isNotNull(); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + testSupport.nextInteraction(ia->{ + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNotNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + assertThat(commandLogEntry.getCommandDto()).isNotNull(); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + }); } @Test void invoke_mixin_disabled() { - // when wrapperFactory.wrapMixin(Counter_bumpUsingMixinWithCommandPublishingDisabled.class, counter1).act(); - interactionService.nextInteraction(); - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); + testSupport.nextInteraction(ia->{ + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); + }); } @Test void invoke_direct_disabled() { - // when wrapperFactory.wrap(counter1).bumpUsingDeclaredActionWithCommandPublishingDisabled(); - interactionService.nextInteraction(); - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); + testSupport.nextInteraction(ia->{ + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); + }); } @Test @@ -183,32 +197,33 @@ void edit() { // when wrapperFactory.wrap(counter1).setNum(99L); - interactionService.nextInteraction(); - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#num"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK (VOID)"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(PropertyDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + testSupport.nextInteraction(ia->{ + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#num"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK (VOID)"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(PropertyDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); + }); } @Test @@ -216,262 +231,271 @@ void edit_disabled() { // when wrapperFactory.wrap(counter1).setNum2(99L); - interactionService.closeInteractionLayers(); // to flush + testSupport.nextInteraction(ia->{ - interactionService.openInteraction(); + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); + }); } @Test void roundtrip_CLE_bookmarks() { + class Model { + CommandDto commandDto; + Bookmark cleBookmark; + } + var testSupport = interactionService.testSupport(new Model()); + // given wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - interactionService.nextInteraction(); + testSupport.nextInteraction(model->{ - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + CommandLogEntry commandLogEntry = commandLogEntryRepository.findMostRecentCompleted().get(); + model.commandDto = commandLogEntry.getCommandDto(); - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - CommandDto commandDto = commandLogEntry.getCommandDto(); + // when + Optional cleBookmarkIfAny = bookmarkService.bookmarkFor(commandLogEntry); - // when - Optional cleBookmarkIfAny = bookmarkService.bookmarkFor(commandLogEntry); - - // then - assertThat(cleBookmarkIfAny).isPresent(); - Bookmark cleBookmark = cleBookmarkIfAny.get(); - 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 - } else { - UUID.fromString(identifier); // should not fail, ie check the format is as we expect - } + // then + assertThat(cleBookmarkIfAny).isPresent(); + model.cleBookmark = cleBookmarkIfAny.get(); + String identifier = model.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 + } else { + UUID.fromString(identifier); // should not fail, ie check the format is as we expect + } + + }); // when we start a new session and lookup from the bookmark - interactionService.nextInteraction(); + testSupport.nextInteraction(model->{ - Optional cle2IfAny = bookmarkService.lookup(cleBookmarkIfAny.get()); - assertThat(cle2IfAny).isPresent(); + Optional cle2IfAny = bookmarkService.lookup(model.cleBookmark); + assertThat(cle2IfAny).isPresent(); - CommandLogEntry cle2 = (CommandLogEntry) cle2IfAny.get(); - CommandDto commandDto2 = cle2.getCommandDto(); + CommandLogEntry cle2 = (CommandLogEntry) cle2IfAny.get(); + CommandDto commandDto2 = cle2.getCommandDto(); - assertThat(commandDto2).isEqualTo(commandDto); + assertThat(commandDto2).isEqualTo(model.commandDto); + }); } @Test void test_all_the_repository_methods() { + class Model { + UUID commandTarget1User1Id; + UUID commandTarget1User2Id; + UUID commandTarget1User1YesterdayId; + } + var testSupport = interactionService.testSupport(new Model()); + // given sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); }); - interactionService.nextInteraction(); - // when - Optional commandTarget1User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); - - // then - Assertions.assertThat(commandTarget1User1IfAny).isPresent(); - var commandTarget1User1 = commandTarget1User1IfAny.get(); - var commandTarget1User1Id = commandTarget1User1.getInteractionId(); - - // given (different user, same target, same day) - counter1 = counterRepository.findByName("counter-1"); - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-2").build()), - () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() - ); - interactionService.nextInteraction(); + testSupport.nextInteraction(model->{ + // when + Optional commandTarget1User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); - // when - Optional commandTarget1User2IfAny = commandLogEntryRepository.findMostRecentCompleted(); - - // then - Assertions.assertThat(commandTarget1User2IfAny).isPresent(); - var commandTarget1User2 = commandTarget1User2IfAny.get(); - var commandTarget1User2Id = commandTarget1User2.getInteractionId(); - - // given (same user, same target, yesterday) - counter1 = counterRepository.findByName("counter-1"); - final UUID[] commandTarget1User1YesterdayIdHolder = new UUID[1]; - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-1").build()), - () -> { - var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); - sudoService.run( - InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), - () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - commandTarget1User1YesterdayIdHolder[0] = interactionLayerTracker.currentInteraction().get().getInteractionId(); - interactionService.closeInteractionLayers(); // to flush within changed time... - } - ); - }); - interactionService.openInteraction(); - - // when, then - final UUID commandTarget1User1YesterdayId = commandTarget1User1YesterdayIdHolder[0]; - - // given (same user, different target, same day) - counter2 = counterRepository.findByName("counter-2"); - sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); + // then + Assertions.assertThat(commandTarget1User1IfAny).isPresent(); + var commandTarget1User1 = commandTarget1User1IfAny.get(); + model.commandTarget1User1Id = commandTarget1User1.getInteractionId(); + + // given (different user, same target, same day) + counter1 = counterRepository.findByName("counter-1"); + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-2").build()), + () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() + ); }); - interactionService.nextInteraction(); - - // when - Optional commandTarget2User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); - // then - Assertions.assertThat(commandTarget2User1IfAny).isPresent(); - var commandTarget2User1 = commandTarget2User1IfAny.get(); - var commandTarget2User1Id = commandTarget2User1.getInteractionId(); + testSupport.nextInteraction(model->{ + // when + Optional commandTarget1User2IfAny = commandLogEntryRepository.findMostRecentCompleted(); - // when - Optional commandTarget1User1ById = commandLogEntryRepository.findByInteractionId(commandTarget1User1Id); - Optional commandTarget1User2ById = commandLogEntryRepository.findByInteractionId(commandTarget1User2Id); - Optional commandTarget1User1YesterdayById = commandLogEntryRepository.findByInteractionId(commandTarget1User1YesterdayId); - Optional commandTarget2User1ById = commandLogEntryRepository.findByInteractionId(commandTarget2User1Id); - - // then - Assertions.assertThat(commandTarget1User1ById).isPresent(); - Assertions.assertThat(commandTarget1User2ById).isPresent(); - Assertions.assertThat(commandTarget1User1YesterdayById).isPresent(); - Assertions.assertThat(commandTarget2User1ById).isPresent(); - Assertions.assertThat(commandTarget2User1ById.get()).isSameAs(commandTarget2User1); + // then + Assertions.assertThat(commandTarget1User2IfAny).isPresent(); + var commandTarget1User2 = commandTarget1User2IfAny.get(); + model.commandTarget1User2Id = commandTarget1User2.getInteractionId(); + + // given (same user, same target, yesterday) + counter1 = counterRepository.findByName("counter-1"); + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-1").build()), + () -> { + var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); + sudoService.run( + InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), + () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + // when, then + model.commandTarget1User1YesterdayId = interactionLayerTracker.currentInteraction().get().getInteractionId(); + interactionService.closeInteractionLayers(); // to flush within changed time... + } + ); + }); + testSupport.openInteraction(); + + // given (same user, different target, same day) + counter2 = counterRepository.findByName("counter-2"); + sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); + }); + }); - // given - commandTarget1User1 = commandTarget1User1ById.get(); - commandTarget1User2 = commandTarget1User2ById.get(); - @SuppressWarnings("unused") - var commandTarget1User1Yesterday = commandTarget1User1YesterdayById.get(); - commandTarget2User1 = commandTarget2User1ById.get(); + testSupport.nextInteraction(model->{ - var target1 = commandTarget1User1.getTarget(); - var username1 = commandTarget1User1.getUsername(); - var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate(); - var to = from.plusDays(1); + var commandTarget1User1Id = model.commandTarget1User1Id; + var commandTarget1User2Id = model.commandTarget1User2Id; + var commandTarget1User1YesterdayId = model.commandTarget1User1YesterdayId; - // when - List notYetReplayed = commandLogEntryRepository.findNotYetReplayed(); + // when + Optional commandTarget2User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); - // then - Assertions.assertThat(notYetReplayed).isEmpty(); + // then + Assertions.assertThat(commandTarget2User1IfAny).isPresent(); + var commandTarget2User1 = commandTarget2User1IfAny.get(); + var commandTarget2User1Id = commandTarget2User1.getInteractionId(); - if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { + // when + Optional commandTarget1User1ById = commandLogEntryRepository.findByInteractionId(commandTarget1User1Id); + Optional commandTarget1User2ById = commandLogEntryRepository.findByInteractionId(commandTarget1User2Id); + Optional commandTarget1User1YesterdayById = commandLogEntryRepository.findByInteractionId(commandTarget1User1YesterdayId); + Optional commandTarget2User1ById = commandLogEntryRepository.findByInteractionId(commandTarget2User1Id); - // fails in JPA; possibly need to get the agent working for dirty tracking. + // then + Assertions.assertThat(commandTarget1User1ById).isPresent(); + Assertions.assertThat(commandTarget1User2ById).isPresent(); + Assertions.assertThat(commandTarget1User1YesterdayById).isPresent(); + Assertions.assertThat(commandTarget2User1ById).isPresent(); + Assertions.assertThat(commandTarget2User1ById.get()).isSameAs(commandTarget2User1); // given - commandTarget1User1.setReplayState(ReplayState.PENDING); + var commandTarget1User1 = commandTarget1User1ById.get(); + var commandTarget1User2 = commandTarget1User2ById.get(); + @SuppressWarnings("unused") + var commandTarget1User1Yesterday = commandTarget1User1YesterdayById.get(); + commandTarget2User1 = commandTarget2User1ById.get(); + + var target1 = commandTarget1User1.getTarget(); + var username1 = commandTarget1User1.getUsername(); + var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate(); + var to = from.plusDays(1); // when - List notYetReplayed2 = commandLogEntryRepository.findNotYetReplayed(); + List notYetReplayed = commandLogEntryRepository.findNotYetReplayed(); // then - Assertions.assertThat(notYetReplayed2).hasSize(1); - Assertions.assertThat(notYetReplayed2.get(0).getInteractionId()).isEqualTo(commandTarget1User1.getInteractionId()); - } + Assertions.assertThat(notYetReplayed).isEmpty(); - // when - List byFromAndTo = commandLogEntryRepository.findByFromAndTo(from, to); + if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { - // then - Assertions.assertThat(byFromAndTo).hasSize(3); - Assertions.assertThat(byFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // fails in JPA; possibly need to get the agent working for dirty tracking. - // when - List byTarget1AndFromAndTo = commandLogEntryRepository.findByTargetAndFromAndTo(target1, from, to); + // given + commandTarget1User1.setReplayState(ReplayState.PENDING); - // then - Assertions.assertThat(byTarget1AndFromAndTo).hasSize(2); - Assertions.assertThat(byTarget1AndFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent + // when + List notYetReplayed2 = commandLogEntryRepository.findNotYetReplayed(); - // when - List recentByTargetOfCommand1 = commandLogEntryRepository.findRecentByTarget(target1); + // then + Assertions.assertThat(notYetReplayed2).hasSize(1); + Assertions.assertThat(notYetReplayed2.get(0).getInteractionId()).isEqualTo(commandTarget1User1.getInteractionId()); + } - // then - Assertions.assertThat(recentByTargetOfCommand1).hasSize(3); - Assertions.assertThat(recentByTargetOfCommand1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent + // when + List byFromAndTo = commandLogEntryRepository.findByFromAndTo(from, to); - // when - List recentByUsername = commandLogEntryRepository.findRecentByUsername(username1); + // then + Assertions.assertThat(byFromAndTo).hasSize(3); + Assertions.assertThat(byFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // then - Assertions.assertThat(recentByUsername).hasSize(3); - Assertions.assertThat(recentByUsername.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // when + List byTarget1AndFromAndTo = commandLogEntryRepository.findByTargetAndFromAndTo(target1, from, to); - // when - List byParent = commandLogEntryRepository.findByParent(commandTarget1User1); + // then + Assertions.assertThat(byTarget1AndFromAndTo).hasSize(2); + Assertions.assertThat(byTarget1AndFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent - // then // TODO: would need nested executions for this to show up. - Assertions.assertThat(byParent).isEmpty(); + // when + List recentByTargetOfCommand1 = commandLogEntryRepository.findRecentByTarget(target1); - // when - List completed = commandLogEntryRepository.findCompleted(); + // then + Assertions.assertThat(recentByTargetOfCommand1).hasSize(3); + Assertions.assertThat(recentByTargetOfCommand1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent - // then - Assertions.assertThat(completed).hasSize(4); - Assertions.assertThat(completed.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // when + List recentByUsername = commandLogEntryRepository.findRecentByUsername(username1); - // when - List current = commandLogEntryRepository.findCurrent(); + // then + Assertions.assertThat(recentByUsername).hasSize(3); + Assertions.assertThat(recentByUsername.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // then // TODO: would need more sophistication in fixtures to test - Assertions.assertThat(current).isEmpty(); + // when + List byParent = commandLogEntryRepository.findByParent(commandTarget1User1); - // when - List since = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 3); + // then // TODO: would need nested executions for this to show up. + Assertions.assertThat(byParent).isEmpty(); - // then - Assertions.assertThat(since).hasSize(2); - Assertions.assertThat(since.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest first + // when + List completed = commandLogEntryRepository.findCompleted(); - // when - List sinceWithBatchSize1 = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 1); + // then + Assertions.assertThat(completed).hasSize(4); + Assertions.assertThat(completed.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // then - Assertions.assertThat(sinceWithBatchSize1).hasSize(1); - Assertions.assertThat(sinceWithBatchSize1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest fist + // when + List current = commandLogEntryRepository.findCurrent(); - // when - Optional mostRecentReplayedIfAny = commandLogEntryRepository.findMostRecentReplayed(); + // then // TODO: would need more sophistication in fixtures to test + Assertions.assertThat(current).isEmpty(); - // then - Assertions.assertThat(mostRecentReplayedIfAny).isEmpty(); + // when + List since = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 3); - if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { + // then + Assertions.assertThat(since).hasSize(2); + Assertions.assertThat(since.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest first - // fails in JPA; possibly need to get the agent working for dirty tracking. + // when + List sinceWithBatchSize1 = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 1); - // given - commandTarget1User1.setReplayState(ReplayState.OK); + // then + Assertions.assertThat(sinceWithBatchSize1).hasSize(1); + Assertions.assertThat(sinceWithBatchSize1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest fist // when - Optional mostRecentReplayedIfAny2 = commandLogEntryRepository.findMostRecentReplayed(); + Optional mostRecentReplayedIfAny = commandLogEntryRepository.findMostRecentReplayed(); // then - Assertions.assertThat(mostRecentReplayedIfAny2).isPresent(); - Assertions.assertThat(mostRecentReplayedIfAny2.get().getInteractionId()).isEqualTo(commandTarget1User1Id); - } - } + Assertions.assertThat(mostRecentReplayedIfAny).isEmpty(); - @Inject CommandLogEntryRepository commandLogEntryRepository; - @Inject SudoService sudoService; - @Inject ClockService clockService; - @Inject InteractionService interactionService; - @Inject InteractionLayerTracker interactionLayerTracker; - @Inject CounterRepository counterRepository; - @Inject WrapperFactory wrapperFactory; - @Inject BookmarkService bookmarkService; - @Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry; + if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { + + // fails in JPA; possibly need to get the agent working for dirty tracking. + + // given + commandTarget1User1.setReplayState(ReplayState.OK); + + // when + Optional mostRecentReplayedIfAny2 = commandLogEntryRepository.findMostRecentReplayed(); + + // then + Assertions.assertThat(mostRecentReplayedIfAny2).isPresent(); + Assertions.assertThat(mostRecentReplayedIfAny2.get().getInteractionId()).isEqualTo(commandTarget1User1Id); + } + }); + } } diff --git a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java index f235a176dfc..5558698bd76 100644 --- a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java +++ b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java @@ -39,6 +39,7 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -56,6 +57,15 @@ public abstract class ExecutionLog_IntegTestAbstract extends CausewayIntegrationTestAbstract { + @Inject ExecutionLogEntryRepository executionLogEntryRepository; + @Inject SudoService sudoService; + @Inject ClockService clockService; + @Inject InteractionService interactionService; + @Inject InteractionLayerTracker interactionLayerTracker; + @Inject CounterRepository counterRepository; + @Inject WrapperFactory wrapperFactory; + @Inject BookmarkService bookmarkService; + @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -63,10 +73,11 @@ static void beforeAll() { Counter counter1; Counter counter2; + private TestSupport testSupport; @BeforeEach void beforeEach() { - + this.testSupport = interactionService.testSupport(); counterRepository.removeAll(); executionLogEntryRepository.removeAll(); @@ -223,21 +234,29 @@ void roundtrip_ELE_bookmarks() { Integer.parseInt(identifier.substring(identifier.indexOf("_")+1)); // should not fail, ie check the format is as we expect // when we start a new session and lookup from the bookmark - interactionService.nextInteraction(); + testSupport.nextInteraction(model->{ - Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); - assertThat(cle2IfAny).isPresent(); + Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); + assertThat(cle2IfAny).isPresent(); - ExecutionLogEntry ele2 = (ExecutionLogEntry) cle2IfAny.get(); - InteractionDto interactionDto2 = ele2.getInteractionDto(); + ExecutionLogEntry ele2 = (ExecutionLogEntry) cle2IfAny.get(); + InteractionDto interactionDto2 = ele2.getInteractionDto(); - assertThat(interactionDto2).isEqualTo(interactionDto); + assertThat(interactionDto2).isEqualTo(interactionDto); + }); } @Test void test_all_the_repository_methods() { + class Model { + UUID executionTarget1User1Id; + UUID executionTarget1User2Id; + UUID executionTarget1User1YesterdayId; + } + var testSupport = interactionService.testSupport(new Model()); + // given sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); @@ -248,8 +267,10 @@ void test_all_the_repository_methods() { // then assertThat(executionsForTarget1User1).hasSize(1); - var executionTarget1User1 = executionsForTarget1User1.get(0); - var executionTarget1User1Id = executionTarget1User1.getInteractionId(); + { + var executionTarget1User1 = executionsForTarget1User1.get(0); + testSupport.model().executionTarget1User1Id = executionTarget1User1.getInteractionId(); + } // given (different user, same target, same day) counter1 = counterRepository.findByName("counter-1"); @@ -258,136 +279,134 @@ void test_all_the_repository_methods() { UserMemento.builder("user-2").build()), () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() ); - interactionService.nextInteraction(); - - // when - List executionsForTarget1User2 = executionLogEntryRepository.findMostRecent(1); - // then - assertThat(executionsForTarget1User2).hasSize(1); - var executionTarget1User2 = executionsForTarget1User2.get(0); - var executionTarget1User2Id = executionTarget1User2.getInteractionId(); + testSupport.nextInteraction(model->{ + + // when + List executionsForTarget1User2 = executionLogEntryRepository.findMostRecent(1); + + // then + assertThat(executionsForTarget1User2).hasSize(1); + var executionTarget1User2 = executionsForTarget1User2.get(0); + model.executionTarget1User2Id = executionTarget1User2.getInteractionId(); + + // given (same user, same target, yesterday) + counter1 = counterRepository.findByName("counter-1"); + + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-1").build()), + () -> { + var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); + sudoService.run( + InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), + () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + // when, then + model.executionTarget1User1YesterdayId = interactionLayerTracker.currentInteraction().get().getInteractionId(); + interactionService.closeInteractionLayers(); // to flush within changed time... + } + ); + }); + }); - // given (same user, same target, yesterday) - counter1 = counterRepository.findByName("counter-1"); - final UUID[] executionTarget1User1YesterdayIdHolder = new UUID[1]; - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-1").build()), - () -> { - var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); - sudoService.run( - InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), - () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - executionTarget1User1YesterdayIdHolder[0] = interactionLayerTracker.currentInteraction().get().getInteractionId(); - interactionService.closeInteractionLayers(); // to flush within changed time... - } - ); - }); - interactionService.openInteraction(); - - // when, then - final UUID executionTarget1User1YesterdayId = executionTarget1User1YesterdayIdHolder[0]; + testSupport.openInteraction(); // given (same user, different target, same day) counter2 = counterRepository.findByName("counter-2"); sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); }); - interactionService.nextInteraction(); - // when - List executionTarget2User1IfAny = executionLogEntryRepository.findMostRecent(1); + testSupport.nextInteraction(model->{ - // then - assertThat(executionTarget2User1IfAny).hasSize(1); - var executionTarget2User1 = executionTarget2User1IfAny.get(0); - var executionTarget2User1Id = executionTarget2User1.getInteractionId(); + var executionTarget1User1Id = model.executionTarget1User1Id; + var executionTarget1User2Id = model.executionTarget1User2Id; + var executionTarget1User1YesterdayId = model.executionTarget1User1YesterdayId; - // when - Optional executionTarget1User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1Id, 0); - Optional executionTarget1User2ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User2Id, 0); - Optional executionTarget1User1YesterdayById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1YesterdayId, 0); - Optional executionTarget2User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget2User1Id, 0); + // when + List executionTarget2User1IfAny = executionLogEntryRepository.findMostRecent(1); - // then - assertThat(executionTarget1User1ById).isPresent(); - assertThat(executionTarget1User2ById).isPresent(); - assertThat(executionTarget1User1YesterdayById).isPresent(); - assertThat(executionTarget2User1ById).isPresent(); - assertThat(executionTarget2User1ById.get()).isSameAs(executionTarget2User1); + // then + assertThat(executionTarget2User1IfAny).hasSize(1); + var executionTarget2User1 = executionTarget2User1IfAny.get(0); + var executionTarget2User1Id = executionTarget2User1.getInteractionId(); - // given - counter1 = counterRepository.findByName("counter-1"); - executionTarget1User1 = executionTarget1User1ById.get(); - executionTarget1User2 = executionTarget1User2ById.get(); - var executionTarget1User1Yesterday = executionTarget1User1YesterdayById.get(); - executionTarget2User1 = executionTarget2User1ById.get(); + // when + Optional executionTarget1User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1Id, 0); + Optional executionTarget1User2ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User2Id, 0); + Optional executionTarget1User1YesterdayById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1YesterdayId, 0); + Optional executionTarget2User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget2User1Id, 0); - var target1 = executionTarget1User1.getTarget(); - var username1 = executionTarget1User1.getUsername(); - Timestamp from1 = executionTarget1User1.getStartedAt(); - Timestamp to1 = Timestamp.valueOf(from1.toLocalDateTime().plusDays(1)); - var bookmark1 = bookmarkService.bookmarkForElseFail(counter1); + // then + assertThat(executionTarget1User1ById).isPresent(); + assertThat(executionTarget1User2ById).isPresent(); + assertThat(executionTarget1User1YesterdayById).isPresent(); + assertThat(executionTarget2User1ById).isPresent(); + assertThat(executionTarget2User1ById.get()).isSameAs(executionTarget2User1); - // when - List recentByTarget = executionLogEntryRepository.findRecentByTarget(bookmark1); + // given + counter1 = counterRepository.findByName("counter-1"); + var executionTarget1User1 = executionTarget1User1ById.get(); + var executionTarget1User2 = executionTarget1User2ById.get(); + var executionTarget1User1Yesterday = executionTarget1User1YesterdayById.get(); + executionTarget2User1 = executionTarget2User1ById.get(); - // then - assertThat(recentByTarget).hasSize(3); + var target1 = executionTarget1User1.getTarget(); + var username1 = executionTarget1User1.getUsername(); + Timestamp from1 = executionTarget1User1.getStartedAt(); + Timestamp to1 = Timestamp.valueOf(from1.toLocalDateTime().plusDays(1)); + var bookmark1 = bookmarkService.bookmarkForElseFail(counter1); - // when - List byTargetAndTimestampBefore = executionLogEntryRepository.findByTargetAndTimestampBefore(bookmark1, from1); + // when + List recentByTarget = executionLogEntryRepository.findRecentByTarget(bookmark1); - // then - assertThat(byTargetAndTimestampBefore).hasSize(2); // yesterday, plus cmd1 + // then + assertThat(recentByTarget).hasSize(3); - // when - List byTargetAndTimestampAfter = executionLogEntryRepository.findByTargetAndTimestampAfter(bookmark1, from1); + // when + List byTargetAndTimestampBefore = executionLogEntryRepository.findByTargetAndTimestampBefore(bookmark1, from1); - // then - assertThat(byTargetAndTimestampAfter).hasSize(2); // cmd1, 2nd + // then + assertThat(byTargetAndTimestampBefore).hasSize(2); // yesterday, plus cmd1 - // when - List byTargetAndTimestampBetween = executionLogEntryRepository.findByTargetAndTimestampBetween(bookmark1, from1, to1); + // when + List byTargetAndTimestampAfter = executionLogEntryRepository.findByTargetAndTimestampAfter(bookmark1, from1); - // then - assertThat(byTargetAndTimestampBetween).hasSize(2); // 1st and 2nd for this target + // then + assertThat(byTargetAndTimestampAfter).hasSize(2); // cmd1, 2nd - // when - List byTimestampBefore = executionLogEntryRepository.findByTimestampBefore(from1); + // when + List byTargetAndTimestampBetween = executionLogEntryRepository.findByTargetAndTimestampBetween(bookmark1, from1, to1); - // then - assertThat(byTimestampBefore).hasSize(2); // cmd1 plus yesterday + // then + assertThat(byTargetAndTimestampBetween).hasSize(2); // 1st and 2nd for this target - // when - List byTimestampAfter = executionLogEntryRepository.findByTimestampAfter(from1); + // when + List byTimestampBefore = executionLogEntryRepository.findByTimestampBefore(from1); - // then - assertThat(byTimestampAfter).hasSize(3); // cmd1, 2nd, and for other target + // then + assertThat(byTimestampBefore).hasSize(2); // cmd1 plus yesterday - // when - List byTimestampBetween = executionLogEntryRepository.findByTimestampBetween(from1, to1); + // when + List byTimestampAfter = executionLogEntryRepository.findByTimestampAfter(from1); - // then - assertThat(byTimestampBetween).hasSize(3); // 1st and 2nd for this target, and other target + // then + assertThat(byTimestampAfter).hasSize(3); // cmd1, 2nd, and for other target - // when - List byUsername = executionLogEntryRepository.findRecentByUsername(username1); + // when + List byTimestampBetween = executionLogEntryRepository.findByTimestampBetween(from1, to1); - // then - assertThat(byUsername).hasSize(3); + // then + assertThat(byTimestampBetween).hasSize(3); // 1st and 2nd for this target, and other target - } + // when + List byUsername = executionLogEntryRepository.findRecentByUsername(username1); - @Inject ExecutionLogEntryRepository executionLogEntryRepository; - @Inject SudoService sudoService; - @Inject ClockService clockService; - @Inject InteractionService interactionService; - @Inject InteractionLayerTracker interactionLayerTracker; - @Inject CounterRepository counterRepository; - @Inject WrapperFactory wrapperFactory; - @Inject BookmarkService bookmarkService; + // then + assertThat(byUsername).hasSize(3); + }); + + } } diff --git a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java index c35fe6211d9..c98204dd148 100644 --- a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java +++ b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java @@ -36,6 +36,7 @@ import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -60,9 +61,11 @@ static void beforeAll() { Counter counter1; Counter counter2; + private TestSupport testSupport; @BeforeEach void beforeEach() { + this.testSupport = interactionService.testSupport(); counterRepository.removeAll(); executionOutboxEntryRepository.removeAll(); @@ -86,7 +89,7 @@ void invoke_mixin() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -117,7 +120,7 @@ void invoke_direct() { wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -148,7 +151,7 @@ void invoke_mixin_disabled() { wrapperFactory.wrapMixin(Counter_bumpUsingMixinWithExecutionPublishingDisabled.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -162,7 +165,7 @@ void invoke_direct_disabled() { wrapperFactory.wrap(counter1).bumpUsingDeclaredActionWithExecutionPublishingDisabled(); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -176,7 +179,7 @@ void edit() { wrapperFactory.wrap(counter1).setNum(99L); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -206,7 +209,7 @@ void edit_disabled() { wrapperFactory.wrap(counter1).setNum2(99L); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -220,7 +223,7 @@ void roundtrip_EOE_bookmarks() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); List all = executionOutboxEntryRepository.findOldest(); ExecutionOutboxEntry executionLogEntry = all.get(0); @@ -238,7 +241,7 @@ void roundtrip_EOE_bookmarks() { // when we start a new session and lookup from the bookmark interactionService.closeInteractionLayers(); - interactionService.openInteraction(); + testSupport.openInteraction(); Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); assertThat(cle2IfAny).isPresent(); @@ -257,7 +260,7 @@ void test_all_the_repository_methods() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); }); interactionService.closeInteractionLayers(); // to flush - interactionService.openInteraction(); + testSupport.openInteraction(); // when List executionTarget1User1IfAny = executionOutboxEntryRepository.findOldest(); diff --git a/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java b/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java index d182ff274b2..54042cafebc 100644 --- a/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java +++ b/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java @@ -31,6 +31,7 @@ import org.apache.causeway.applib.mixins.system.DomainChangeRecord; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.core.config.presets.CausewayPresets; import org.apache.causeway.extensions.audittrail.applib.dom.AuditTrailEntry; @@ -42,6 +43,8 @@ public abstract class AuditTrail_IntegTestAbstract extends CausewayIntegrationTestAbstract { + private TestSupport testSupport; + @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -49,14 +52,15 @@ static void beforeAll() { @BeforeEach void setUp() { + this.testSupport = interactionService.testSupport(); counterRepository.removeAll(); - interactionService.nextInteraction(); - - auditTrailEntryRepository.removeAll(); - interactionService.nextInteraction(); - - assertThat(counterRepository.find()).isEmpty(); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + testSupport.nextInteraction(model->{ + auditTrailEntryRepository.removeAll(); + }); + testSupport.nextInteraction(model->{ + assertThat(counterRepository.find()).isEmpty(); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + }); } protected abstract Counter newCounter(String name); @@ -67,32 +71,33 @@ void created() { // when var counter1 = counterRepository.persist(newCounter("counter-1")); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - interactionService.nextInteraction(); - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).contains("name", "num", "num2"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("counter-1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); - assertThat(entriesById.get("num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); + testSupport.nextInteraction(model->{ + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).contains("name", "num", "num2"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("counter-1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); + assertThat(entriesById.get("num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); + }); } @Test @@ -101,55 +106,62 @@ void updated_using_mixin() { // given var counter1 = counterRepository.persist(newCounter("counter-1")); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - interactionService.nextInteraction(); - - auditTrailEntryRepository.removeAll(); - interactionService.nextInteraction(); - - assertThat(counterRepository.find()).hasSize(1); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); - - // when - counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - interactionService.nextInteraction(); - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - - // given - auditTrailEntryRepository.removeAll(); - interactionService.nextInteraction(); - - // when bump again - counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - interactionService.nextInteraction(); - - // then - entries = auditTrailEntryRepository.findAll(); - propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("2")); - + testSupport.nextInteraction(ia->{ + auditTrailEntryRepository.removeAll(); + }); + + testSupport.nextInteraction(ia->{ + assertThat(counterRepository.find()).hasSize(1); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + + // when + var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); + }); + + testSupport.nextInteraction(ia->{ + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + + // given + auditTrailEntryRepository.removeAll(); + + }); + + testSupport.nextInteraction(ia->{ + + // when bump again + var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); + }); + + testSupport.nextInteraction(ia->{ + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("2")); + }); } @Test @@ -160,40 +172,44 @@ void deleted() { counter1.setNum(1L); counter1.setNum2(2L); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - interactionService.nextInteraction(); - - auditTrailEntryRepository.removeAll(); - interactionService.nextInteraction(); - // when - counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - counterRepository.remove(counter1); - interactionService.nextInteraction(); - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).contains("name", "num", "num2"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("counter-1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); - assertThat(entriesById.get("num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); + testSupport.nextInteraction(ia->{ + auditTrailEntryRepository.removeAll(); + }); + + testSupport.nextInteraction(ia->{ + // when + var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + counterRepository.remove(counter2); + }); + + testSupport.nextInteraction(ia->{ + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).contains("name", "num", "num2"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("counter-1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); + assertThat(entriesById.get("num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); + }); } diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java index d4af4aaf560..b535c6edda3 100644 --- a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java @@ -78,7 +78,7 @@ CausewayModuleViewerWicketViewer.class, }) public class Configuration_usingWicket { - + @Bean public WicketTesterFactory wicketTesterFactory(final MetaModelContext mmc) { return new WicketTesterFactory(mmc); @@ -282,25 +282,22 @@ private static class PageFactory_forTesting implements IPageFactory { @Override public C newPage(final Class pageClass, final PageParameters parameters) { - if(DomainObjectPage.class.equals(pageClass)) { + if(DomainObjectPage.class.equals(pageClass)) return _Casts.uncheckedCast(DomainObjectPage.forPageParameters(parameters)); - } return delegate.newPage(pageClass, parameters); } @Override public C newPage(final Class pageClass) { - if(DomainObjectPage.class.equals(pageClass)) { + if(DomainObjectPage.class.equals(pageClass)) throw _Exceptions.illegalArgument("cannot instantiate DomainObjectPage without PageParameters"); - } return delegate.newPage(pageClass); } @Override public boolean isBookmarkable(final Class pageClass) { - if(DomainObjectPage.class.equals(pageClass)) { + if(DomainObjectPage.class.equals(pageClass)) return true; - } return delegate.isBookmarkable(pageClass); } } @@ -353,7 +350,7 @@ protected IPageFactory newPageFactory() { protected void internalInit() { super.internalInit(); // intercept AJAX requests and reload view-models so any detached entities are re-fetched - CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this, metaModelContext); + CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this); } } diff --git a/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java b/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java index 3b24b9bc08f..14ca623834e 100644 --- a/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java +++ b/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java @@ -33,6 +33,7 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; import org.apache.causeway.core.config.presets.CausewayPresets; @@ -59,32 +60,35 @@ static void beforeAll() { } Bookmark target1; + private TestSupport testSupport; @BeforeEach void beforeEach() { - interactionService.nextInteraction(); - - counterRepository.removeAll(); - - assertThat(counterRepository.find()).isEmpty(); - - var counter1 = counterRepository.persist(newCounter("counter-1")); - target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - - assertThat(counterRepository.find()).hasSize(1); - - interactionService.nextInteraction(); - commandLogEntryRepository.removeAll(); - executionLogEntryRepository.removeAll(); - executionOutboxEntryRepository.removeAll(); - auditTrailEntryRepository.removeAll(); - - interactionService.nextInteraction(); - - assertThat(commandLogEntryRepository.findAll()).isEmpty(); - assertThat(executionLogEntryRepository.findAll()).isEmpty(); - assertThat(executionOutboxEntryRepository.findAll()).isEmpty(); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + this.testSupport = interactionService.testSupport(); + testSupport.nextInteraction(ia->{ + counterRepository.removeAll(); + + assertThat(counterRepository.find()).isEmpty(); + + var counter1 = counterRepository.persist(newCounter("counter-1")); + target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); + + assertThat(counterRepository.find()).hasSize(1); + }); + + testSupport.nextInteraction(ia->{ + commandLogEntryRepository.removeAll(); + executionLogEntryRepository.removeAll(); + executionOutboxEntryRepository.removeAll(); + auditTrailEntryRepository.removeAll(); + }); + + testSupport.nextInteraction(ia->{ + assertThat(commandLogEntryRepository.findAll()).isEmpty(); + assertThat(executionLogEntryRepository.findAll()).isEmpty(); + assertThat(executionOutboxEntryRepository.findAll()).isEmpty(); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + }); } protected abstract Counter newCounter(String name); @@ -93,7 +97,7 @@ void beforeEach() { protected void assertEntityPublishingDisabledFor(final Class entityClass) { var objectSpecification = specificationLoader.loadSpecification(entityClass); - EntityChangePublishingFacet facet = objectSpecification.getFacet(EntityChangePublishingFacet.class); + EntityChangePublishingFacet facet = objectSpecification.lookupFacet(EntityChangePublishingFacet.class).orElse(null); Assertions.assertThat(facet) .satisfies(f -> assertThat(f).isNotNull()) .satisfies(f -> assertThat(f.isEnabled()).isFalse()) @@ -107,6 +111,8 @@ void invoke_mixin() { var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); var interaction = interactionService.currentInteraction().orElseThrow(); + { + // when wrapperFactory.wrapMixinT(Counter_bumpUsingMixin.class, counter1).act(); @@ -190,42 +196,46 @@ void invoke_mixin() { // ... and audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); + } // when - interactionService.nextInteraction(); // flushes the command and audit trail entries + testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries + + // then + // ... command entry now marked as complete + var commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNotNull()) + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + var auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + + var propertyIds = auditTrailEntries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + var entriesById = auditTrailEntries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("cmdexecauditsess.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isEqualTo(interaction.getInteractionId())) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + } + + }); - // then - // ... command entry now marked as complete - commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNotNull()) - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); - - var propertyIds = auditTrailEntries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - var entriesById = auditTrailEntries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("cmdexecauditsess.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isEqualTo(interaction.getInteractionId())) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - } } @Test @@ -233,6 +243,7 @@ void invoke_direct() { // given var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + { // when wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); @@ -259,27 +270,29 @@ void invoke_direct() { // ... but audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); + } // when - interactionService.nextInteraction(); // flushes the command and audit trail entries - - // then - // ... command entry now marked as complete - commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNotNull()) - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); - } + testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries + + // then + // ... command entry now marked as complete + var commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNotNull()) + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + var auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + } + }); } @@ -288,7 +301,7 @@ void edit() { // given var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - + { // when wrapperFactory.wrap(counter1).setNum(99L); @@ -311,27 +324,28 @@ void edit() { // ... and audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); - - // when - interactionService.nextInteraction(); // flushes the command and audit trail entries - - // then - // ... command entry now marked as complete - commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNull()) // property edits are effectively void actions - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK (VOID)")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); } + // when + testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries + + // then + // ... command entry now marked as complete + var commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNull()) // property edits are effectively void actions + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK (VOID)")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + var auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + } + }); } diff --git a/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java b/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java index 17f1fdbac40..eb26dc54a5b 100644 --- a/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java +++ b/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java @@ -52,6 +52,7 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.metamodel.MetaModelService; import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; import org.apache.causeway.core.config.presets.CausewayPresets; @@ -123,16 +124,15 @@ static void beforeAll() { } Bookmark target1; + private TestSupport testSupport; @BeforeEach void beforeEach() { - interactionService.nextInteraction(); - - Optional bookmark = bookmarkService.bookmarkFor(newCounter("counter-1")); - target1 = bookmark.orElseThrow(); - - interactionService.nextInteraction(); - + this.testSupport = interactionService.testSupport(); + testSupport.nextInteraction(model->{ + Optional bookmark = bookmarkService.bookmarkFor(newCounter("counter-1")); + target1 = bookmark.orElseThrow(); + }); } protected Counter newCounter(final String name) { diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java index cd1a1ea1c66..51f4fbea87b 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java @@ -47,12 +47,12 @@ public void beforeEach(final ExtensionContext extensionContext) throws Exception _Helper .getInteractionFactory(extensionContext) - .ifPresent(interactionService-> + .map(InteractionService::testSupport) + .ifPresent(testSupport-> _Helper .getCustomInteractionContext(extensionContext) - .ifPresentOrElse( - interactionService::openInteraction, - interactionService::openInteraction)); + .map(testSupport::openInteraction) + .orElseGet(testSupport::openInteraction)); } @Override diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java index 00a9d0dec0d..ba0ff65fbf9 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java @@ -49,7 +49,7 @@ public void beforeEach(final ExtensionContext extensionContext) { interactionService.currentInteractionContext().ifPresent( currentInteractionContext -> { var sudoUser = currentInteractionContext.getUser().withRoleAdded(SudoService.ACCESS_ALL_ROLE.name()); - interactionService.openInteraction(currentInteractionContext.withUser(sudoUser)); + interactionService.testSupport().openInteraction(currentInteractionContext.withUser(sudoUser)); } ) ); diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java index ebd64fdb387..1a97dbb1774 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java @@ -17,7 +17,6 @@ * under the License. * */ - package org.apache.causeway.testing.integtestsupport.applib; import org.junit.jupiter.api.extension.BeforeEachCallback; @@ -54,7 +53,7 @@ public void beforeEach(final ExtensionContext extensionContext) { for (UserMementoRefiner userMementoRefiner : serviceRegistry.select(UserMementoRefiner.class)) { user = userMementoRefiner.refine(user); } - interactionService.openInteraction(currentInteractionContext.withUser(user)); + interactionService.testSupport().openInteraction(currentInteractionContext.withUser(user)); } ) ) diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/AuthenticatedWebSessionForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/AuthenticatedWebSessionForCauseway.java index 9b997f42af5..9e2f1d3f04b 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/AuthenticatedWebSessionForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/AuthenticatedWebSessionForCauseway.java @@ -27,7 +27,7 @@ import org.apache.wicket.authroles.authorization.strategies.role.Roles; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.RequestCycle; - +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.clock.VirtualClock; @@ -50,23 +50,17 @@ import org.apache.causeway.viewer.wicket.ui.pages.BookmarkedPagesModelProvider; import lombok.Getter; -import org.jspecify.annotations.NonNull; import lombok.extern.slf4j.Slf4j; /** * Viewer-specific implementation of {@link AuthenticatedWebSession}, which - * delegates to the Causeway' configured {@link AuthenticationManager}, and which - * also tracks thread usage (so that multiple concurrent requests are all + * delegates to the Causeway' configured {@link AuthenticationManager}, and + * which also tracks thread usage (so that multiple concurrent requests are all * associated with the same session). */ @Slf4j -public class AuthenticatedWebSessionForCauseway -extends AuthenticatedWebSession -implements - BreadcrumbModelProvider, - BookmarkedPagesModelProvider, - HasMetaModelContext, - HasAmendableInteractionContext { +public class AuthenticatedWebSessionForCauseway extends AuthenticatedWebSession implements BreadcrumbModelProvider, + BookmarkedPagesModelProvider, HasMetaModelContext, HasAmendableInteractionContext { private static final long serialVersionUID = 1L; @@ -78,21 +72,20 @@ public static AuthenticatedWebSessionForCauseway get() { * lazily populated in {@link #getBreadcrumbModel()} */ private BreadcrumbModel breadcrumbModel; + @Override public BreadcrumbModel getBreadcrumbModel() { - return breadcrumbModel != null - ? breadcrumbModel - : (breadcrumbModel = new BreadcrumbModel()); + return breadcrumbModel != null ? breadcrumbModel : (breadcrumbModel = new BreadcrumbModel()); } /** * lazily populated in {@link #getBookmarkedPagesModel()} */ private BookmarkedPagesModel bookmarkedPagesModel; + @Override public BookmarkedPagesModel getBookmarkedPagesModel() { - return bookmarkedPagesModel != null - ? bookmarkedPagesModel + return bookmarkedPagesModel != null ? bookmarkedPagesModel : (bookmarkedPagesModel = new BookmarkedPagesModel()); } @@ -100,23 +93,23 @@ public BookmarkedPagesModel getBookmarkedPagesModel() { * As populated in {@link #signIn(String, String)}. */ private InteractionContext interactionContext; + private void setInteractionContext(final @Nullable InteractionContext interactionContext) { - _Assert.assertFalse( - interactionContext !=null - && interactionContext.getUser().isImpersonating(), ()-> - "framework bug: cannot signin with an impersonated user"); + _Assert.assertFalse(interactionContext != null && interactionContext.getUser().isImpersonating(), + () -> "framework bug: cannot signin with an impersonated user"); this.interactionContext = interactionContext; } /** - * If there is an {@link InteractionContext} already (primed) - * (as some authentication mechanisms setup in filters, - * eg SpringSecurityFilter), then just use it. + * If there is an {@link InteractionContext} already (primed) (as some + * authentication mechanisms setup in filters, eg SpringSecurityFilter), then + * just use it. + * *

* However, for authorization, the authentication still must pass - * {@link AuthenticationManager} checks, - * as done in {@link #getInteractionContext()}, - * which on success also sets the signIn flag. + * {@link AuthenticationManager} checks, as done in + * {@link #getInteractionContext()}, which on success also sets the signIn flag. + * *

* Called by {@link WebRequestCycleForCauseway}. */ @@ -129,14 +122,14 @@ public void setPrimedInteractionContext(final @NonNull InteractionContext authen private String cachedSessionId; /** - * Optionally the current HttpSession's Id, - * based on whether such a session is available. - * @implNote side-effect free, that is, - * must not create a session if there is none yet + * Optionally the current HttpSession's Id, based on whether such a session is + * available. + * + * @implNote side-effect free, that is, must not create a session if there is + * none yet */ public Optional getCachedSessionId() { - if (cachedSessionId == null - && Session.exists()) { + if (cachedSessionId == null && Session.exists()) { cachedSessionId = getId(); } return Optional.ofNullable(cachedSessionId); @@ -156,9 +149,8 @@ public synchronized boolean authenticate(final String username, final String pas if (interactionContext != null) { log(SessionSubscriber.Type.LOGIN, username, null); return true; - } else { + } else return false; - } } @Override @@ -172,10 +164,8 @@ public synchronized void invalidateNow() { // principals for it to logout // - getAuthenticationManager().closeSession( - Optional.ofNullable(interactionContext) - .map(InteractionContext::getUser) - .orElse(null)); + getAuthenticationManager() + .closeSession(Optional.ofNullable(interactionContext).map(InteractionContext::getUser).orElse(null)); super.invalidateNow(); @@ -192,8 +182,7 @@ public synchronized void onInvalidate() { super.onInvalidate(); - var causedBy = RequestCycle.get() != null - ? SessionSubscriber.CausedBy.USER + var causedBy = RequestCycle.get() != null ? SessionSubscriber.CausedBy.USER : SessionSubscriber.CausedBy.SESSION_EXPIRATION; log(SessionSubscriber.Type.LOGOUT, userName, causedBy); @@ -205,23 +194,21 @@ public void amendInteractionContext(final UnaryOperator upda } /** - * Returns an {@link InteractionContext} either as authenticated (and then cached on the session subsequently), - * or taking into account {@link UserService impersonation}. + * Returns an {@link InteractionContext} either as authenticated (and then + * cached on the session subsequently), or taking into account + * {@link UserService impersonation}. + * *

- * The session must still {@link AuthenticationManager#isSessionValid(InteractionContext) be valid}, though - * note that this will always be true for externally authenticated users. + * The session must still + * {@link AuthenticationManager#isSessionValid(InteractionContext) be valid}, + * though note that this will always be true for externally authenticated users. */ - synchronized InteractionContext getInteractionContext() { - - if(interactionContext == null) { + synchronized InteractionContext getInteractionContext() { + if (interactionContext == null) return null; - } - if (Optional.ofNullable(getMetaModelContext()) - .map(MetaModelContext::getAuthenticationManager) - .filter(x -> x.isSessionValid(interactionContext)) - .isEmpty()) { + if (Optional.ofNullable(getMetaModelContext()).map(MetaModelContext::getAuthenticationManager) + .filter(x -> x.isSessionValid(interactionContext)).isEmpty()) return null; - } signIn(true); return interactionContext; @@ -229,50 +216,39 @@ synchronized InteractionContext getInteractionContext() { @Override public AuthenticationManager getAuthenticationManager() { - return Optional.ofNullable(getMetaModelContext()) - .map(MetaModelContext::getAuthenticationManager) - .orElse(null); + return Optional.ofNullable(getMetaModelContext()).map(MetaModelContext::getAuthenticationManager).orElse(null); } /** - * This is a no-op if the {@link #getInteractionContext() authentication session}'s - * {@link UserMemento#authenticationSource() source} is - * {@link AuthenticationSource#EXTERNAL external} - * (eg as managed by keycloak). + * This is a no-op if the {@link #getInteractionContext() authentication + * session}'s {@link UserMemento#authenticationSource() source} is + * {@link AuthenticationSource#EXTERNAL external} (eg as managed by keycloak). */ @Override public void invalidate() { - if(interactionContext !=null - && interactionContext.getUser().authenticationSource().isExternal()) { + if (interactionContext != null && interactionContext.getUser().authenticationSource().isExternal()) return; - } // otherwise super.invalidate(); } @Override public synchronized Roles getRoles() { - if (!isSignedIn()) { + if (!isSignedIn()) return null; - } - return getInteractionService() - .currentInteractionContext() - .map(InteractionContext::getUser) - .map(user->{ - var roles = new Roles(); - user.streamRoleNames() - .forEach(roles::add); - return roles; - }) - .orElse(null); + return getInteractionService().currentInteractionContext().map(InteractionContext::getUser).map(user -> { + var roles = new Roles(); + user.streamRoleNames().forEach(roles::add); + return roles; + }).orElse(null); } @Override public synchronized void detach() { - if(breadcrumbModel!=null) { + if (breadcrumbModel != null) { breadcrumbModel.detach(); } - if(bookmarkedPagesModel!=null) { + if (bookmarkedPagesModel != null) { bookmarkedPagesModel.detach(); } super.detach(); @@ -284,12 +260,10 @@ public void replaceSession() { // see https://issues.apache.org/jira/browse/CAUSEWAY-1018 } - private void log( - final SessionSubscriber.Type type, - final String username, + private void log(final SessionSubscriber.Type type, final String username, final SessionSubscriber.CausedBy causedBy) { - if(getMetaModelContext()==null) { + if (getMetaModelContext() == null) { log.warn("Failed to callback SessionLoggingServices due to unavailable MetaModelContext.\n" + "\tEvent Data: type={}, username={}, causedBy={}", type, username, causedBy); return; @@ -298,18 +272,16 @@ private void log( var interactionService = getInteractionService(); var sessionLoggingServices = getSessionLoggingServices(); - final Runnable loggingTask = ()->{ + final Runnable loggingTask = () -> { var now = virtualClock().nowAsJavaUtilDate(); - var httpSessionId = AuthenticatedWebSessionForCauseway.this.getCachedSessionId() - .orElse("(none)"); + var httpSessionId = AuthenticatedWebSessionForCauseway.this.getCachedSessionId().orElse("(none)"); - sessionLoggingServices - .forEach(sessionLoggingService -> - sessionLoggingService.log(type, username, now, causedBy, getSessionGuid(), httpSessionId)); + sessionLoggingServices.forEach(sessionLoggingService -> sessionLoggingService.log(type, username, now, + causedBy, getSessionGuid(), httpSessionId)); }; - if(interactionService!=null) { + if (interactionService != null) { interactionService.runAnonymous(loggingTask::run); } else { loggingTask.run(); @@ -322,9 +294,7 @@ protected Can getSessionLoggingServices() { private VirtualClock virtualClock() { try { - return getServiceRegistry() - .lookupService(ClockService.class) - .map(ClockService::getClock) + return getServiceRegistry().lookupService(ClockService.class).map(ClockService::getClock) .orElseGet(this::nowFallback); } catch (Exception e) { return nowFallback(); diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java new file mode 100644 index 00000000000..a7631deb424 --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -0,0 +1,84 @@ +/* + * 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.viewer.wicket.viewer.integration; + +import org.apache.wicket.request.IRequestCycle; +import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.cycle.RequestCycleContext; + +import org.apache.causeway.applib.services.iactnlayer.InteractionContext; +import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.user.UserService; +import org.apache.causeway.core.metamodel.context.MetaModelContext; + +import lombok.extern.slf4j.Slf4j; + +public class RequestCycle2 extends RequestCycle { + + public RequestCycle2(final RequestCycleContext context) { + super(context); + } + + record Wrapper(IRequestHandler delegate) implements IRequestHandler { + @Override + public void respond(final IRequestCycle requestCycle) { + delegate.respond(requestCycle); + } + @Override + public void detach(final IRequestCycle requestCycle) { + delegate.detach(requestCycle); + } + } + + @Slf4j + record RequestHandlerWrapper( + InteractionService interactionService, + InteractionContext interactionContext, + IRequestHandler delegate) implements IRequestHandler { + + @Override + public void respond(final IRequestCycle requestCycle) { + if(interactionContext==null) { + delegate.respond(requestCycle); + return; + } + interactionService.run(interactionContext, ()->delegate.respond(requestCycle)); + } + @Override + public void detach(final IRequestCycle requestCycle) { + delegate.detach(requestCycle); + } + } + + @Override + protected IRequestHandler resolveRequestHandler() { + var mmc = MetaModelContext.instanceElseFail(); + + var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) + .determineInteractionContext() + .orElse(null); + + return new RequestHandlerWrapper( + mmc.getInteractionService(), + ic, + super.resolveRequestHandler()); + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java new file mode 100644 index 00000000000..85f726aeec7 --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java @@ -0,0 +1,61 @@ +/* + * 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.viewer.wicket.viewer.integration; + +import java.util.Optional; + +import org.apache.causeway.applib.services.iactnlayer.InteractionContext; +import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.user.UserService; +import org.apache.causeway.commons.internal.exceptions._Exceptions; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +record SessionAuthenticator( + InteractionService interactionService, + UserService userService) { + + public Optional determineInteractionContext() { + // participate if an InteractionContext was already provided through some other mechanism, + // but fail early if the current user is impersonating + // (seeing this if going back the browser history into a page, that was previously impersonated) + var session = AuthenticatedWebSessionForCauseway.get(); + + interactionService.currentInteractionContext() + .ifPresent(ic->{ + if(ic.getUser().isImpersonating()) + throw _Exceptions.illegalState("cannot enter a new request cycle with a left over impersonating user"); + session.setPrimedInteractionContext(ic); + }); + + var interactionContext = session.getInteractionContext(); + if (interactionContext == null) { + log.warn("onBeginRequest out - session was not opened (because no authentication)"); + return Optional.empty(); + } + + return Optional.of( + userService + .lookupImpersonatedUser() + .map(interactionContext::withUser) + .orElse(interactionContext)); + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java index 6b04e2e1a0f..a4240799c16 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java @@ -54,9 +54,7 @@ import org.apache.causeway.applib.services.exceprecog.Recognition; import org.apache.causeway.applib.services.i18n.TranslationContext; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.metrics.MetricsService; -import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.base._Timing; @@ -119,7 +117,7 @@ static boolean isExpiryMessageTimeframeExpired() { } private static final MetaDataKey SESSION_LIFECYCLE_PHASE_KEY = - new MetaDataKey() { private static final long serialVersionUID = 1L; }; + new MetaDataKey<>() { private static final long serialVersionUID = 1L; }; @Setter private PageClassRegistry pageClassRegistry; @@ -142,50 +140,11 @@ public synchronized void onBeginRequest(final RequestCycle requestCycle) { log.trace("flagging the RequestCycle as expired (rememberMe feature is active for the current user)"); } } - if(log.isTraceEnabled()) { - log.trace("onBeginRequest out - session was not opened (because no Session)"); - } - return; - } - - // participate if an InteractionContext was already provided through some other mechanism, - // but fail early if the current user is impersonating - // (seeing this if going back the browser history into a page, that was previously impersonated) - var interactionService = getInteractionService(); - var authenticatedWebSession = AuthenticatedWebSessionForCauseway.get(); - - /*XXX for debugging delegated user ... - interactionService.openInteraction(InteractionContext - .ofUserWithSystemDefaults( - UserMemento.ofName("delegated") - .withRoleAdded(UserMemento.AUTHORIZED_USER_ROLE) - .withAuthenticationSource(AuthenticationSource.EXTERNAL)));*/ - - var currentInteractionContext = interactionService.currentInteractionContext(); - if(currentInteractionContext.isPresent()) { - if(currentInteractionContext.get().getUser().isImpersonating()) { - throw _Exceptions.illegalState("cannot enter a new request cycle with a left over impersonating user"); - } - authenticatedWebSession.setPrimedInteractionContext(currentInteractionContext.get()); - } - - var interactionContext0 = authenticatedWebSession.getInteractionContext(); - if (interactionContext0 == null) { - log.warn("onBeginRequest out - session was not opened (because no authentication)"); return; } - - // impersonation support - var interactionContext1 = lookupServiceElseFail(UserService.class) - .lookupImpersonatedUser() - .map(sudoUser -> interactionContext0.withUser(sudoUser)) - .orElse(interactionContext0); - - // Note: this is a no-op if an interactionContext layer was already opened and is unchanged. - interactionService.openInteraction(interactionContext1); - + if(log.isTraceEnabled()) { - log.trace("onBeginRequest out - session was opened"); + log.trace("onBeginRequest out - session about to open"); } if(log.isDebugEnabled()) { @@ -212,19 +171,17 @@ public void onRequestHandlerResolved(final RequestCycle requestCycle, final IReq } SessionLifecyclePhase.transferExpiredFlagToSession(); - } else if(handler instanceof RenderPageRequestHandler) { + } else if(handler instanceof RenderPageRequestHandler requestHandler) { // using side-effect free access to MM validation result var validationResult = getMetaModelContext().getSpecificationLoader().getValidationResult() .orElseThrow(()->_Exceptions.illegalState("Application is not fully initialized yet.")); if(validationResult.hasFailures()) { - RenderPageRequestHandler requestHandler = (RenderPageRequestHandler) handler; final IRequestablePage nextPage = requestHandler.getPage(); - if(nextPage instanceof ErrorPage || nextPage instanceof MmvErrorPage) { + if(nextPage instanceof ErrorPage || nextPage instanceof MmvErrorPage) // do nothing return; - } throw new MetaModelInvalidException(validationResult.getAsLineNumberedString()); } @@ -291,9 +248,6 @@ public synchronized void onEndRequest(final RequestCycle requestCycle) { } } - getMetaModelContext().lookupService(InteractionService.class).ifPresent( - InteractionService::closeInteractionLayers - ); } @Override @@ -321,8 +275,7 @@ public IRequestHandler onException(final RequestCycle cycle, final Exception ex) try { // adapted from http://markmail.org/message/un7phzjbtmrrperc - if(ex instanceof ListenerInvocationNotAllowedException) { - final ListenerInvocationNotAllowedException linaex = (ListenerInvocationNotAllowedException) ex; + if(ex instanceof final ListenerInvocationNotAllowedException linaex) { if(linaex.getComponent() != null && PromptFormAbstract.ID_CANCEL_BUTTON.equals(linaex.getComponent().getId())) { // no message. // this seems to occur when press ESC twice in rapid succession on a modal dialog. @@ -439,9 +392,8 @@ protected IRequestablePage errorPageFor(final Exception ex) { var validationResult = mmc.getSpecificationLoader().getValidationResult() .orElse(null); if(validationResult!=null - && validationResult.hasFailures()) { + && validationResult.hasFailures()) return new MmvErrorPage(validationResult.getMessages("[%d] %s")); - } var exceptionRecognizerService = mmc.getServiceRegistry() .lookupServiceElseFail(ExceptionRecognizerService.class); @@ -499,9 +451,8 @@ private IRequestablePage newSignInPage(final ExceptionModel exceptionModel) { * Matters should improve once CAUSEWAY-299 gets implemented... */ protected boolean isSignedIn() { - if(!isInInteraction()) { + if(!isInInteraction()) return false; - } return getWicketAuthenticatedWebSession().isSignedIn(); } @@ -514,9 +465,8 @@ private boolean userHasSessionWithRememberMe(final RequestCycle requestCycle) { getConfiguration().viewer().wicket().rememberMe().cookieKey()); for (var cookie : cookies) { - if (cookieKey.equals(cookie.getName())) { + if (cookieKey.equals(cookie.getName())) return true; - } } } return false; diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java index d155e62fd94..6a7c3ed4b11 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java @@ -27,7 +27,6 @@ import org.apache.wicket.request.Request; import org.apache.wicket.request.component.IRequestablePage; -import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; import lombok.experimental.UtilityClass; @@ -36,8 +35,7 @@ public final class CausewayWicketAjaxRequestListenerUtil { public void setRootRequestMapper( - final WebApplication app, - final MetaModelContext commonContext) { + final WebApplication app) { app.setRootRequestMapper(new SystemMapper(app) { @Override @@ -55,8 +53,7 @@ public IRequestHandler mapRequest(final Request request) { final IRequestablePage iRequestablePage = ((ListenerRequestHandler)handler).getPage(); - if(iRequestablePage instanceof PageAbstract) { - var pageAbstract = (PageAbstract) iRequestablePage; + if(iRequestablePage instanceof PageAbstract pageAbstract) { pageAbstract.onNewRequestCycle(); } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 6dbfe9c7d07..706c9855140 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -68,6 +68,7 @@ import org.apache.causeway.viewer.wicket.viewer.integration.AuthenticatedWebSessionForCauseway; import org.apache.causeway.viewer.wicket.viewer.integration.CausewayResourceSettings; import org.apache.causeway.viewer.wicket.viewer.integration.ConverterForObjectAdapter; +import org.apache.causeway.viewer.wicket.viewer.integration.RequestCycle2; import org.apache.causeway.viewer.wicket.viewer.integration.WebRequestCycleForCauseway; import lombok.Getter; @@ -136,11 +137,11 @@ protected void internalInit() { // in which search for i18n properties, to search for the application-specific // settings before any other. setResourceSettings(new CausewayResourceSettings(this)); - super.internalInit(); + setRequestCycleProvider(RequestCycle2::new); // intercept AJAX requests and reload view-models so any detached entities are re-fetched - CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this, metaModelContext); + CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this); } private AjaxRequestTarget decorate(final AjaxRequestTarget ajaxRequestTarget) { @@ -328,9 +329,8 @@ protected void mountPage(final String mountPath, final PageType pageType) { @Override public final RuntimeConfigurationType getConfigurationType() { - if(systemEnvironment==null) { + if(systemEnvironment==null) return RuntimeConfigurationType.DEPLOYMENT; - } return systemEnvironment.isPrototyping() ? RuntimeConfigurationType.DEVELOPMENT From 67f8b8dea669d10b7e74c36cacd4cce02369bfff Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 20 Mar 2026 23:38:59 +0100 Subject: [PATCH 03/31] CAUSEWAY-3975: consolidating custom request processing logic (wicket) --- .../conf/Configuration_usingWicket.java | 5 +- ...uestCycle2.java => RootRequestMapper.java} | 67 +++++++++----- .../WebRequestCycleForCauseway.java | 10 +- ...CausewayWicketAjaxRequestListenerUtil.java | 91 ------------------- .../wicketapp/CausewayWicketApplication.java | 7 +- 5 files changed, 56 insertions(+), 124 deletions(-) rename viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/{RequestCycle2.java => RootRequestMapper.java} (54%) delete mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java index b535c6edda3..e8c488746d9 100644 --- a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java @@ -61,7 +61,7 @@ import org.apache.causeway.viewer.wicket.ui.pages.PageClassRegistry; import org.apache.causeway.viewer.wicket.ui.pages.obj.DomainObjectPage; import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer; -import org.apache.causeway.viewer.wicket.viewer.wicketapp.CausewayWicketAjaxRequestListenerUtil; +import org.apache.causeway.viewer.wicket.viewer.integration.RootRequestMapper; import lombok.AccessLevel; import lombok.Getter; @@ -349,8 +349,7 @@ protected IPageFactory newPageFactory() { @Override protected void internalInit() { super.internalInit(); - // intercept AJAX requests and reload view-models so any detached entities are re-fetched - CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this); + setRootRequestMapper(new RootRequestMapper(this)); } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java similarity index 54% rename from viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java rename to viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java index a7631deb424..1d50d625120 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java @@ -18,34 +18,26 @@ */ package org.apache.causeway.viewer.wicket.viewer.integration; +import org.apache.wicket.Application; +import org.apache.wicket.SystemMapper; +import org.apache.wicket.core.request.handler.ListenerRequestHandler; +import org.apache.wicket.core.request.mapper.PageInstanceMapper; import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.IRequestHandler; -import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.wicket.request.cycle.RequestCycleContext; +import org.apache.wicket.request.IRequestMapper; +import org.apache.wicket.request.Request; +import org.apache.wicket.request.component.IRequestablePage; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; import lombok.extern.slf4j.Slf4j; -public class RequestCycle2 extends RequestCycle { +public final class RootRequestMapper extends SystemMapper implements IRequestMapper { - public RequestCycle2(final RequestCycleContext context) { - super(context); - } - - record Wrapper(IRequestHandler delegate) implements IRequestHandler { - @Override - public void respond(final IRequestCycle requestCycle) { - delegate.respond(requestCycle); - } - @Override - public void detach(final IRequestCycle requestCycle) { - delegate.detach(requestCycle); - } - } + public static ThreadLocal X = new ThreadLocal<>(); @Slf4j record RequestHandlerWrapper( @@ -67,18 +59,43 @@ public void detach(final IRequestCycle requestCycle) { } } + public RootRequestMapper(final Application application) { + super(application); + } + @Override - protected IRequestHandler resolveRequestHandler() { + public IRequestHandler mapRequest(final Request request) { var mmc = MetaModelContext.instanceElseFail(); - - var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) - .determineInteractionContext() - .orElse(null); +// var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) +// .determineInteractionContext() +// .orElse(null); return new RequestHandlerWrapper( mmc.getInteractionService(), - ic, - super.resolveRequestHandler()); + X.get(), + super.mapRequest(request)); + } + + // intercept AJAX requests and reload view-models so any detached entities are re-fetched + @Override + protected IRequestMapper newPageInstanceMapper() { + return new PageInstanceMapper() { + @Override + public IRequestHandler mapRequest(final Request request) { + var handler = super.mapRequest(request); + + if (handler instanceof ListenerRequestHandler) { + + final IRequestablePage iRequestablePage = ((ListenerRequestHandler) handler).getPage(); + + if (iRequestablePage instanceof PageAbstract pageAbstract) { + pageAbstract.onNewRequestCycle(); + } + } + + return handler; + } + }; } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java index a4240799c16..5f52bc2a2ba 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java @@ -55,12 +55,14 @@ import org.apache.causeway.applib.services.i18n.TranslationContext; import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.metrics.MetricsService; +import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.base._Timing; import org.apache.causeway.commons.internal.base._Timing.StopWatch; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.context.HasMetaModelContext; +import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.specloader.validator.MetaModelInvalidException; import org.apache.causeway.viewer.commons.model.error.ExceptionModel; @@ -142,7 +144,13 @@ public synchronized void onBeginRequest(final RequestCycle requestCycle) { } return; } - + + var mmc = MetaModelContext.instanceElseFail(); + var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) + .determineInteractionContext() + .orElse(null); + RootRequestMapper.X.set(ic); + if(log.isTraceEnabled()) { log.trace("onBeginRequest out - session about to open"); } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java deleted file mode 100644 index 6a7c3ed4b11..00000000000 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java +++ /dev/null @@ -1,91 +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.viewer.wicket.viewer.wicketapp; - -import org.apache.wicket.SystemMapper; -import org.apache.wicket.core.request.handler.ListenerRequestHandler; -import org.apache.wicket.core.request.mapper.PageInstanceMapper; -import org.apache.wicket.protocol.http.WebApplication; -import org.apache.wicket.request.IRequestHandler; -import org.apache.wicket.request.IRequestMapper; -import org.apache.wicket.request.Request; -import org.apache.wicket.request.component.IRequestablePage; - -import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public final class CausewayWicketAjaxRequestListenerUtil { - - public void setRootRequestMapper( - final WebApplication app) { - - app.setRootRequestMapper(new SystemMapper(app) { - @Override - protected IRequestMapper newPageInstanceMapper() { - return new PageInstanceMapper() { - @Override - public IRequestHandler mapRequest(final Request request) { - var handler = super.mapRequest(request); - //final boolean isAjax = ((WebRequest)request).isAjax(); - - if(handler instanceof ListenerRequestHandler) { -// _Debug.log("AJAX via ListenerRequestHandler"); -// RequestCycle.get().getListeners().add(newRequestCycleListener()); - - final IRequestablePage iRequestablePage = - ((ListenerRequestHandler)handler).getPage(); - - if(iRequestablePage instanceof PageAbstract pageAbstract) { - pageAbstract.onNewRequestCycle(); - } - - } - - return handler; - } - }; - } - }); - } - -// public IListener newAjaxListener() { -// -// RequestCycle x; -// -// return new IListener() {; -// @Override -// public void onBeforeRespond(final Map map, final AjaxRequestTarget target) { -// _Debug.log("AJAX via IListener"); -// DomainObjectPage.broadcastAjaxRequest(target.getPage(), target); -// } -// }; -// } - -// private IRequestCycleListener newRequestCycleListener() { -// return new IRequestCycleListener() { -// @Override -// public void onRequestHandlerResolved(final RequestCycle cycle, final IRequestHandler handler) { -// _Debug.log("RequestCycle: handler resolved %s", handler); -// } -// }; -// } - -} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 706c9855140..41bb10f8294 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -68,7 +68,7 @@ import org.apache.causeway.viewer.wicket.viewer.integration.AuthenticatedWebSessionForCauseway; import org.apache.causeway.viewer.wicket.viewer.integration.CausewayResourceSettings; import org.apache.causeway.viewer.wicket.viewer.integration.ConverterForObjectAdapter; -import org.apache.causeway.viewer.wicket.viewer.integration.RequestCycle2; +import org.apache.causeway.viewer.wicket.viewer.integration.RootRequestMapper; import org.apache.causeway.viewer.wicket.viewer.integration.WebRequestCycleForCauseway; import lombok.Getter; @@ -138,10 +138,9 @@ protected void internalInit() { // settings before any other. setResourceSettings(new CausewayResourceSettings(this)); super.internalInit(); - setRequestCycleProvider(RequestCycle2::new); + //setRequestCycleProvider(RequestCycle2::new); - // intercept AJAX requests and reload view-models so any detached entities are re-fetched - CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this); + setRootRequestMapper(new RootRequestMapper(this)); } private AjaxRequestTarget decorate(final AjaxRequestTarget ajaxRequestTarget) { From b48185e87e94673a54a424dc003f177e45a25ae8 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 00:09:20 +0100 Subject: [PATCH 04/31] CAUSEWAY-3975: interaction is too short lived - using the old method instead --- .../viewer/integration/RootRequestMapper.java | 22 ++++++++++++++----- .../WebRequestCycleForCauseway.java | 2 ++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java index 1d50d625120..9f3c1204fe5 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java @@ -30,6 +30,7 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.commons.internal.base._Lazy; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; @@ -43,19 +44,30 @@ public final class RootRequestMapper extends SystemMapper implements IRequestMap record RequestHandlerWrapper( InteractionService interactionService, InteractionContext interactionContext, - IRequestHandler delegate) implements IRequestHandler { + _Lazy delegate) implements IRequestHandler { @Override public void respond(final IRequestCycle requestCycle) { if(interactionContext==null) { - delegate.respond(requestCycle); + delegate.get().respond(requestCycle); return; } - interactionService.run(interactionContext, ()->delegate.respond(requestCycle)); + interactionService.testSupport().openInteraction(interactionContext); + X.remove(); + delegate.get().respond(requestCycle); + + +// interactionService.run(interactionContext, ()->{ +// delegate.get().respond(requestCycle); +// X.remove(); +// }); } @Override public void detach(final IRequestCycle requestCycle) { - delegate.detach(requestCycle); + if(delegate.isMemoized()) { + delegate.get().detach(requestCycle); + } + interactionService.closeInteractionLayers(); } } @@ -73,7 +85,7 @@ public IRequestHandler mapRequest(final Request request) { return new RequestHandlerWrapper( mmc.getInteractionService(), X.get(), - super.mapRequest(request)); + _Lazy.threadSafe(()->super.mapRequest(request))); } // intercept AJAX requests and reload view-models so any detached entities are re-fetched diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java index 5f52bc2a2ba..b1fb6b7a8fe 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java @@ -437,6 +437,8 @@ private IRequestablePage newSignInPage(final ExceptionModel exceptionModel) { signInPageClass = WicketSignInPage.class; } final PageParameters parameters = new PageParameters(); + if(signInPageClass == WicketSignInPage.class) + return new WicketSignInPage(parameters, exceptionModel); Page signInPage; try { Constructor constructor = signInPageClass.getConstructor(PageParameters.class, ExceptionModel.class); From 1002386683397b825102e61ce0598d0d85202f30 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 08:17:09 +0100 Subject: [PATCH 05/31] CAUSEWAY-3975: interaction creation fixes --- .../viewer/integration/RequestCycle2.java | 57 ++++++++++ .../viewer/integration/RootRequestMapper.java | 66 +---------- .../integration/SessionAuthenticator.java | 3 +- .../integration/TelemetryStartHandler.java | 51 +++++++++ .../integration/TelemetryStopHandler.java | 47 ++++++++ .../WebRequestCycleForCauseway.java | 107 ++++-------------- .../wicketapp/CausewayWicketApplication.java | 31 ++--- 7 files changed, 201 insertions(+), 161 deletions(-) create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java new file mode 100644 index 00000000000..7efdc44585c --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -0,0 +1,57 @@ +/* + * 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.viewer.wicket.viewer.integration; + +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.cycle.RequestCycleContext; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Scope; + +public class RequestCycle2 extends RequestCycle { + + final long startTimeNanos; + Observation observation; + Scope scope; + + public RequestCycle2(final RequestCycleContext context) { + super(context); + this.startTimeNanos = System.nanoTime(); + } + + long millisSinceStart() { + return (System.nanoTime() - startTimeNanos)/1000_000; + } + + void observationStartAndOpenScope() { + if(observation==null) return; + observation.start(); + this.scope = observation.openScope(); + } + + void observationCloseScopeAndStop() { + if(observation==null) return; + if(scope!=null) { + this.scope.close(); + this.scope = null; + } + observation.stop(); + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java index 9f3c1204fe5..7e6b7cfd9c7 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java @@ -22,72 +22,18 @@ import org.apache.wicket.SystemMapper; import org.apache.wicket.core.request.handler.ListenerRequestHandler; import org.apache.wicket.core.request.mapper.PageInstanceMapper; -import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestMapper; import org.apache.wicket.request.Request; -import org.apache.wicket.request.component.IRequestablePage; -import org.apache.causeway.applib.services.iactnlayer.InteractionContext; -import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.commons.internal.base._Lazy; -import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; -import lombok.extern.slf4j.Slf4j; - public final class RootRequestMapper extends SystemMapper implements IRequestMapper { - public static ThreadLocal X = new ThreadLocal<>(); - - @Slf4j - record RequestHandlerWrapper( - InteractionService interactionService, - InteractionContext interactionContext, - _Lazy delegate) implements IRequestHandler { - - @Override - public void respond(final IRequestCycle requestCycle) { - if(interactionContext==null) { - delegate.get().respond(requestCycle); - return; - } - interactionService.testSupport().openInteraction(interactionContext); - X.remove(); - delegate.get().respond(requestCycle); - - -// interactionService.run(interactionContext, ()->{ -// delegate.get().respond(requestCycle); -// X.remove(); -// }); - } - @Override - public void detach(final IRequestCycle requestCycle) { - if(delegate.isMemoized()) { - delegate.get().detach(requestCycle); - } - interactionService.closeInteractionLayers(); - } - } - public RootRequestMapper(final Application application) { super(application); } - @Override - public IRequestHandler mapRequest(final Request request) { - var mmc = MetaModelContext.instanceElseFail(); -// var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) -// .determineInteractionContext() -// .orElse(null); - - return new RequestHandlerWrapper( - mmc.getInteractionService(), - X.get(), - _Lazy.threadSafe(()->super.mapRequest(request))); - } - // intercept AJAX requests and reload view-models so any detached entities are re-fetched @Override protected IRequestMapper newPageInstanceMapper() { @@ -95,16 +41,10 @@ protected IRequestMapper newPageInstanceMapper() { @Override public IRequestHandler mapRequest(final Request request) { var handler = super.mapRequest(request); - - if (handler instanceof ListenerRequestHandler) { - - final IRequestablePage iRequestablePage = ((ListenerRequestHandler) handler).getPage(); - - if (iRequestablePage instanceof PageAbstract pageAbstract) { - pageAbstract.onNewRequestCycle(); - } + if (handler instanceof ListenerRequestHandler listenerRequestHandler + && listenerRequestHandler.getPage() instanceof PageAbstract pageAbstract) { + pageAbstract.onNewRequestCycle(); } - return handler; } }; diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java index 85f726aeec7..4d1e223f174 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java @@ -47,10 +47,11 @@ public Optional determineInteractionContext() { var interactionContext = session.getInteractionContext(); if (interactionContext == null) { - log.warn("onBeginRequest out - session was not opened (because no authentication)"); + log.warn("session was not opened (because not authenticated)"); return Optional.empty(); } + // impersonation support return Optional.of( userService .lookupImpersonatedUser() diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java new file mode 100644 index 00000000000..b5eb4badd62 --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.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.viewer.wicket.viewer.integration; + +import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.cycle.IRequestCycleListener; +import org.apache.wicket.request.cycle.RequestCycle; + +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; + +/** + * @since 4.0 + */ +public record TelemetryStartHandler( + ObservationProvider observationProvider) +implements IRequestCycleListener { + + @Override + public synchronized void onBeginRequest(final RequestCycle requestCycle) { + if (requestCycle instanceof RequestCycle2 requestCycle2) { + requestCycle2.observation = observationProvider.get("Apache Wicket Request Cycle"); + requestCycle2.observationStartAndOpenScope(); + } + } + + @Override + public IRequestHandler onException(final RequestCycle requestCycle, final Exception ex) { + if (requestCycle instanceof RequestCycle2 requestCycle2 + && requestCycle2.observation!=null) { + requestCycle2.observation.error(ex); + } + return null; + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java new file mode 100644 index 00000000000..d33a11f1ff0 --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java @@ -0,0 +1,47 @@ +/* + * 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.viewer.wicket.viewer.integration; + +import org.apache.wicket.request.cycle.IRequestCycleListener; +import org.apache.wicket.request.cycle.RequestCycle; + +import org.apache.causeway.applib.services.metrics.MetricsService; + +/** + * @since 4.0 + */ +public record TelemetryStopHandler( + MetricsService metricsService) +implements IRequestCycleListener { + + @Override + public void onEndRequest(final RequestCycle requestCycle) { + if (requestCycle instanceof RequestCycle2 requestCycle2 + && requestCycle2.observation!=null) { + + if(requestCycle2.millisSinceStart() > 50) { // avoid clutter + requestCycle2.observation.highCardinalityKeyValue("numberEntitiesLoaded", ""+metricsService.numberEntitiesLoaded()); + requestCycle2.observation.highCardinalityKeyValue("numberEntitiesDirtied", ""+metricsService.numberEntitiesDirtied()); + } + + requestCycle2.observation.stop(); + } + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java index b1fb6b7a8fe..b2f1650080e 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java @@ -54,16 +54,15 @@ import org.apache.causeway.applib.services.exceprecog.Recognition; import org.apache.causeway.applib.services.i18n.TranslationContext; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.applib.services.metrics.MetricsService; +import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.base._Strings; -import org.apache.causeway.commons.internal.base._Timing; -import org.apache.causeway.commons.internal.base._Timing.StopWatch; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.context.HasMetaModelContext; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import org.apache.causeway.core.metamodel.specloader.validator.MetaModelInvalidException; import org.apache.causeway.viewer.commons.model.error.ExceptionModel; import org.apache.causeway.viewer.wicket.model.models.PageType; @@ -73,7 +72,6 @@ import org.apache.causeway.viewer.wicket.ui.pages.mmverror.MmvErrorPage; import org.apache.causeway.viewer.wicket.ui.panels.PromptFormAbstract; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** @@ -84,7 +82,12 @@ * @since 2.0 */ @Slf4j -public class WebRequestCycleForCauseway +public record WebRequestCycleForCauseway( + InteractionService interactionService, + PageClassRegistry pageClassRegistry, + ExceptionRecognizerService exceptionRecognizerService, + SpecificationLoader specificationLoader, + SessionAuthenticator sessionAuthenticator) implements HasMetaModelContext, IRequestCycleListener { @@ -121,17 +124,14 @@ static boolean isExpiryMessageTimeframeExpired() { private static final MetaDataKey SESSION_LIFECYCLE_PHASE_KEY = new MetaDataKey<>() { private static final long serialVersionUID = 1L; }; - @Setter - private PageClassRegistry pageClassRegistry; - - private static ThreadLocal timings = ThreadLocal.withInitial(_Timing::now); + public WebRequestCycleForCauseway(final MetaModelContext mmc, final PageClassRegistry pageClassRegistry) { + this(mmc.getInteractionService(), pageClassRegistry, mmc.lookupServiceElseFail(ExceptionRecognizerService.class), + mmc.getSpecificationLoader(), + new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class))); + } @Override - public synchronized void onBeginRequest(final RequestCycle requestCycle) { - - if(log.isTraceEnabled()) { - log.trace("onBeginRequest in"); - } + public void onBeginRequest(final RequestCycle requestCycle) { if (!Session.exists()) { // Track if session was created from an expired one to notify user of the refresh. @@ -145,19 +145,8 @@ public synchronized void onBeginRequest(final RequestCycle requestCycle) { return; } - var mmc = MetaModelContext.instanceElseFail(); - var ic = new SessionAuthenticator(mmc.getInteractionService(), mmc.lookupServiceElseFail(UserService.class)) - .determineInteractionContext() - .orElse(null); - RootRequestMapper.X.set(ic); - - if(log.isTraceEnabled()) { - log.trace("onBeginRequest out - session about to open"); - } - - if(log.isDebugEnabled()) { - timings.set(_Timing.now()); - } + sessionAuthenticator.determineInteractionContext() + .ifPresent(interactionService.testSupport()::openInteraction); } @Override @@ -182,7 +171,7 @@ public void onRequestHandlerResolved(final RequestCycle requestCycle, final IReq } else if(handler instanceof RenderPageRequestHandler requestHandler) { // using side-effect free access to MM validation result - var validationResult = getMetaModelContext().getSpecificationLoader().getValidationResult() + var validationResult = specificationLoader().getValidationResult() .orElseThrow(()->_Exceptions.illegalState("Application is not fully initialized yet.")); if(validationResult.hasFailures()) { @@ -217,9 +206,6 @@ public void onRequestHandlerResolved(final RequestCycle requestCycle, final IReq } } - if(log.isTraceEnabled()) { - log.trace("onRequestHandlerResolved out"); - } } /** @@ -238,41 +224,14 @@ public void onRequestHandlerExecuted(final RequestCycle requestCycle, final IReq */ @Override public synchronized void onEndRequest(final RequestCycle requestCycle) { - - if(log.isDebugEnabled()) { - var metricsServiceIfAny = getMetaModelContext().lookupService(MetricsService.class); - long took = timings.get().getMillis(); - if(took > 50) { // avoid too much clutter - if(metricsServiceIfAny.isPresent()) { - var metricsService = metricsServiceIfAny.get(); - int numberEntitiesLoaded = metricsService.numberEntitiesLoaded(); - int numberEntitiesDirtied = metricsService.numberEntitiesDirtied(); - if(numberEntitiesLoaded > 0 || numberEntitiesDirtied > 0) { - log.debug("onEndRequest took: {}ms numberEntitiesLoaded: {}, numberEntitiesDirtied: {}", took, numberEntitiesLoaded, numberEntitiesDirtied); - } - } else { - log.debug("onEndRequest took: {}ms", took); - } - } - } - - } - - @Override - public void onDetach(final RequestCycle requestCycle) { - // detach the current @RequestScope, if any - IRequestCycleListener.super.onDetach(requestCycle); + interactionService.closeInteractionLayers(); } @Override public IRequestHandler onException(final RequestCycle cycle, final Exception ex) { - if(log.isDebugEnabled()) { - log.debug("onException {} took: {}ms", ex.getClass().getSimpleName(), timings.get().getMillis()); - } - // using side-effect free access to MM validation result - var validationResult = getMetaModelContext().getSpecificationLoader().getValidationResult() + var validationResult = specificationLoader().getValidationResult() .orElse(null); if(validationResult!=null && validationResult.hasFailures()) { @@ -295,8 +254,7 @@ public IRequestHandler onException(final RequestCycle cycle, final Exception ex) } // handle recognized exceptions gracefully also - var exceptionRecognizerService = getExceptionRecognizerService(); - var recognizedIfAny = exceptionRecognizerService.recognize(ex); + var recognizedIfAny = exceptionRecognizerService().recognize(ex); if(recognizedIfAny.isPresent()) { addWarning(recognizedIfAny.get().toMessage(getMetaModelContext().getTranslationService())); return respondGracefully(cycle); @@ -397,16 +355,13 @@ protected IRequestablePage errorPageFor(final Exception ex) { } // using side-effect free access to MM validation result - var validationResult = mmc.getSpecificationLoader().getValidationResult() + var validationResult = specificationLoader().getValidationResult() .orElse(null); if(validationResult!=null && validationResult.hasFailures()) return new MmvErrorPage(validationResult.getMessages("[%d] %s")); - var exceptionRecognizerService = mmc.getServiceRegistry() - .lookupServiceElseFail(ExceptionRecognizerService.class); - - final Optional recognition = exceptionRecognizerService + final Optional recognition = exceptionRecognizerService() .recognizeFromSelected( Can.of( pageExpiredExceptionRecognizer, @@ -461,9 +416,9 @@ private IRequestablePage newSignInPage(final ExceptionModel exceptionModel) { * Matters should improve once CAUSEWAY-299 gets implemented... */ protected boolean isSignedIn() { - if(!isInInteraction()) + if(!interactionService.isInInteraction()) return false; - return getWicketAuthenticatedWebSession().isSignedIn(); + return AuthenticatedWebSession.get().isSignedIn(); } private boolean userHasSessionWithRememberMe(final RequestCycle requestCycle) { @@ -482,18 +437,4 @@ private boolean userHasSessionWithRememberMe(final RequestCycle requestCycle) { return false; } - // -- DEPENDENCIES - - private ExceptionRecognizerService getExceptionRecognizerService() { - return getMetaModelContext().getServiceRegistry().lookupServiceElseFail(ExceptionRecognizerService.class); - } - - private boolean isInInteraction() { - return getMetaModelContext().getInteractionService().isInInteraction(); - } - - private AuthenticatedWebSession getWicketAuthenticatedWebSession() { - return AuthenticatedWebSession.get(); - } - } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 41bb10f8294..61d9db93014 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.function.Function; @@ -41,17 +42,19 @@ import org.apache.wicket.markup.head.ResourceAggregator; import org.apache.wicket.markup.head.filter.JavaScriptFilteredIntoFooterHeaderResponse; import org.apache.wicket.markup.html.WebPage; -import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.PageRequestHandlerTracker; import org.apache.wicket.request.resource.CssResourceReference; import org.apache.wicket.settings.RequestCycleSettings; import org.apache.wicket.spring.injection.annot.SpringComponentInjector; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.apache.causeway.applib.services.inject.ServiceInjector; +import org.apache.causeway.applib.services.metrics.MetricsService; import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.environment.CausewaySystemEnvironment; import org.apache.causeway.core.metamodel.context.MetaModelContext; @@ -68,7 +71,10 @@ import org.apache.causeway.viewer.wicket.viewer.integration.AuthenticatedWebSessionForCauseway; import org.apache.causeway.viewer.wicket.viewer.integration.CausewayResourceSettings; import org.apache.causeway.viewer.wicket.viewer.integration.ConverterForObjectAdapter; +import org.apache.causeway.viewer.wicket.viewer.integration.RequestCycle2; import org.apache.causeway.viewer.wicket.viewer.integration.RootRequestMapper; +import org.apache.causeway.viewer.wicket.viewer.integration.TelemetryStartHandler; +import org.apache.causeway.viewer.wicket.viewer.integration.TelemetryStopHandler; import org.apache.causeway.viewer.wicket.viewer.integration.WebRequestCycleForCauseway; import lombok.Getter; @@ -112,6 +118,10 @@ public static CausewayWicketApplication get() { @Inject private List applicationInitializers; @Inject private CausewaySystemEnvironment systemEnvironment; @Inject private CausewayConfiguration configuration; + @Inject private MetricsService metricService; + + @Qualifier("causeway-wicketviewer") + @Inject private CausewayObservationInternal observationInternal; @Getter(onMethod = @__(@Override)) @Inject private ComponentFactoryRegistry componentFactoryRegistry; @@ -138,9 +148,6 @@ protected void internalInit() { // settings before any other. setResourceSettings(new CausewayResourceSettings(this)); super.internalInit(); - //setRequestCycleProvider(RequestCycle2::new); - - setRootRequestMapper(new RootRequestMapper(this)); } private AjaxRequestTarget decorate(final AjaxRequestTarget ajaxRequestTarget) { @@ -202,10 +209,15 @@ protected void init() { .submit(_ConcurrentContext.sequential()) .await(); + setRequestCycleProvider(RequestCycle2::new); + setRootRequestMapper(new RootRequestMapper(this)); getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER); getResourceSettings().setParentFolderPlaceholder("$up$"); - getRequestCycleListeners().add(createWebRequestCycleListenerForCauseway()); + getRequestCycleListeners().add(new TelemetryStartHandler(Objects.requireNonNull(observationInternal) + .provider(TelemetryStartHandler.class))); + getRequestCycleListeners().add(new WebRequestCycleForCauseway(metaModelContext, getPageClassRegistry())); + getRequestCycleListeners().add(new TelemetryStopHandler(metricService)); getRequestCycleListeners().add(new PageRequestHandlerTracker()); //XXX CAUSEWAY-2530, don't recreate expired pages @@ -275,15 +287,6 @@ protected String defaultEncryptionKey() { // ////////////////////////////////////// - /** - * Factored out for easy (informal) pluggability. - */ - protected IRequestCycleListener createWebRequestCycleListenerForCauseway() { - var webRequestCycleForCauseway = new WebRequestCycleForCauseway(); - webRequestCycleForCauseway.setPageClassRegistry(getPageClassRegistry()); - return webRequestCycleForCauseway; - } - protected static final Function> getCssResourceReferences = (final ComponentFactory input) -> { final CssResourceReference cssResourceReference = input.getCssResourceReference(); From e1ee5c6a608023d63d6f2c7ef837b492a19a4182 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 08:32:20 +0100 Subject: [PATCH 06/31] CAUSEWAY-3975: proper observation scope closing --- .../viewer/integration/RequestCycle2.java | 25 ++++++++++++++++--- .../integration/TelemetryStartHandler.java | 9 +++---- .../integration/TelemetryStopHandler.java | 9 +++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java index 7efdc44585c..bc2263624c6 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -18,8 +18,11 @@ */ package org.apache.causeway.viewer.wicket.viewer.integration; +import java.util.function.Supplier; + import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.cycle.RequestCycleContext; +import org.jspecify.annotations.Nullable; import io.micrometer.observation.Observation; import io.micrometer.observation.Observation.Scope; @@ -27,8 +30,8 @@ public class RequestCycle2 extends RequestCycle { final long startTimeNanos; - Observation observation; - Scope scope; + private Observation observation; + private Scope scope; public RequestCycle2(final RequestCycleContext context) { super(context); @@ -39,9 +42,9 @@ long millisSinceStart() { return (System.nanoTime() - startTimeNanos)/1000_000; } - void observationStartAndOpenScope() { + void observationStartAndOpenScope(final Observation observation) { if(observation==null) return; - observation.start(); + this.observation = observation.start(); this.scope = observation.openScope(); } @@ -54,4 +57,18 @@ void observationCloseScopeAndStop() { observation.stop(); } + void onObservationError(final Exception ex) { + if(observation==null) return; + observation.error(ex); + } + + void observationTag(final String key, @Nullable final Supplier valueSupplier) { + if(observation==null || valueSupplier == null) return; + try { + observation.highCardinalityKeyValue(key, "" + valueSupplier.get()); + } catch (Exception e) { + observation.highCardinalityKeyValue(key, "EXCEPTION: " + e.getMessage()); + } + } + } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index b5eb4badd62..fdec3182d5d 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -34,16 +34,15 @@ public record TelemetryStartHandler( @Override public synchronized void onBeginRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { - requestCycle2.observation = observationProvider.get("Apache Wicket Request Cycle"); - requestCycle2.observationStartAndOpenScope(); + requestCycle2.observationStartAndOpenScope( + observationProvider.get("Apache Wicket Request Cycle")); } } @Override public IRequestHandler onException(final RequestCycle requestCycle, final Exception ex) { - if (requestCycle instanceof RequestCycle2 requestCycle2 - && requestCycle2.observation!=null) { - requestCycle2.observation.error(ex); + if (requestCycle instanceof RequestCycle2 requestCycle2) { + requestCycle2.onObservationError(ex); } return null; } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java index d33a11f1ff0..def31d79e24 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java @@ -32,15 +32,14 @@ public record TelemetryStopHandler( @Override public void onEndRequest(final RequestCycle requestCycle) { - if (requestCycle instanceof RequestCycle2 requestCycle2 - && requestCycle2.observation!=null) { + if (requestCycle instanceof RequestCycle2 requestCycle2) { if(requestCycle2.millisSinceStart() > 50) { // avoid clutter - requestCycle2.observation.highCardinalityKeyValue("numberEntitiesLoaded", ""+metricsService.numberEntitiesLoaded()); - requestCycle2.observation.highCardinalityKeyValue("numberEntitiesDirtied", ""+metricsService.numberEntitiesDirtied()); + requestCycle2.observationTag("numberEntitiesLoaded", metricsService::numberEntitiesLoaded); + requestCycle2.observationTag("numberEntitiesDirtied", metricsService::numberEntitiesDirtied); } - requestCycle2.observation.stop(); + requestCycle2.observationCloseScopeAndStop(); } } From 88c5a0bf4cd724e1f10617d943c47570d2f1463a Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 08:41:09 +0100 Subject: [PATCH 07/31] CAUSEWAY-3975: factors out RehydrationHandler --- .../integration/RehydrationHandler.java | 43 +++++++++++++++++++ .../viewer/integration/RootRequestMapper.java | 3 ++ .../wicketapp/CausewayWicketApplication.java | 5 ++- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RehydrationHandler.java diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RehydrationHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RehydrationHandler.java new file mode 100644 index 00000000000..69069bf629f --- /dev/null +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RehydrationHandler.java @@ -0,0 +1,43 @@ +/* + * 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.viewer.wicket.viewer.integration; + +import org.apache.wicket.core.request.handler.ListenerRequestHandler; +import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.cycle.IRequestCycleListener; +import org.apache.wicket.request.cycle.RequestCycle; + +import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; + +/** + * EXPERIMENTAL: intercept requests and reload view-models so any detached entities are re-fetched + * + * @since 2.0 (refactored for v4) + */ +public record RehydrationHandler() implements IRequestCycleListener { + + @Override + public void onRequestHandlerResolved(final RequestCycle cycle, final IRequestHandler handler) { + if (handler instanceof ListenerRequestHandler listenerRequestHandler + && listenerRequestHandler.getPage() instanceof PageAbstract pageAbstract) { + pageAbstract.onNewRequestCycle(); + } + } + +} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java index 7e6b7cfd9c7..965c6d76b0d 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java @@ -28,13 +28,16 @@ import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; +@Deprecated // was replaced by RehydrationHandler public final class RootRequestMapper extends SystemMapper implements IRequestMapper { + @Deprecated public RootRequestMapper(final Application application) { super(application); } // intercept AJAX requests and reload view-models so any detached entities are re-fetched + @Deprecated @Override protected IRequestMapper newPageInstanceMapper() { return new PageInstanceMapper() { diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 61d9db93014..1cc27a57d4b 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -71,8 +71,8 @@ import org.apache.causeway.viewer.wicket.viewer.integration.AuthenticatedWebSessionForCauseway; import org.apache.causeway.viewer.wicket.viewer.integration.CausewayResourceSettings; import org.apache.causeway.viewer.wicket.viewer.integration.ConverterForObjectAdapter; +import org.apache.causeway.viewer.wicket.viewer.integration.RehydrationHandler; import org.apache.causeway.viewer.wicket.viewer.integration.RequestCycle2; -import org.apache.causeway.viewer.wicket.viewer.integration.RootRequestMapper; import org.apache.causeway.viewer.wicket.viewer.integration.TelemetryStartHandler; import org.apache.causeway.viewer.wicket.viewer.integration.TelemetryStopHandler; import org.apache.causeway.viewer.wicket.viewer.integration.WebRequestCycleForCauseway; @@ -210,7 +210,7 @@ protected void init() { .await(); setRequestCycleProvider(RequestCycle2::new); - setRootRequestMapper(new RootRequestMapper(this)); + //setRootRequestMapper(new RootRequestMapper(this)); getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER); getResourceSettings().setParentFolderPlaceholder("$up$"); @@ -218,6 +218,7 @@ protected void init() { .provider(TelemetryStartHandler.class))); getRequestCycleListeners().add(new WebRequestCycleForCauseway(metaModelContext, getPageClassRegistry())); getRequestCycleListeners().add(new TelemetryStopHandler(metricService)); + getRequestCycleListeners().add(new RehydrationHandler()); getRequestCycleListeners().add(new PageRequestHandlerTracker()); //XXX CAUSEWAY-2530, don't recreate expired pages From b5a170b62fde78da16adcbea1d4987074ef22008 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 09:32:40 +0100 Subject: [PATCH 08/31] CAUSEWAY-3975: adds observation 'Causeway Layered Interaction' properly spanning till interaction closes --- .../services/iactnlayer/InteractionLayer.java | 16 +++++- .../iactnlayer/InteractionLayerStack.java | 18 +++++-- .../CausewayObservationInternal.java | 50 +++++++++++++++++++ .../session/InteractionServiceDefault.java | 25 ++++------ .../InteractionService_forTesting.java | 4 +- .../viewer/integration/RequestCycle2.java | 30 +++-------- 6 files changed, 99 insertions(+), 44 deletions(-) diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java index 13da3e007fb..c4475490ab3 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java @@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; /** * Binds an {@link Interaction} ("what" is being executed) with @@ -46,7 +47,8 @@ public record InteractionLayer( * WHO is performing this {@link #getInteraction()}, also * WHEN and WHERE. */ - InteractionContext interactionContext) { + InteractionContext interactionContext, + ObservationClosure observationClosure) implements AutoCloseable { public boolean isRoot() { return parent==null; @@ -68,4 +70,16 @@ public InteractionLayer rootLayer() { : this; } + @Override + public void close() { + observationClosure.close(); + } + + public void closeAll() { + close(); + if(parent!=null) { + parent.closeAll(); + } + } + } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java index 3791d22d494..9a1b7b69fdb 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java @@ -24,6 +24,9 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; + +import io.micrometer.observation.Observation; public final class InteractionLayerStack { @@ -38,14 +41,17 @@ public Optional currentLayer() { public InteractionLayer push( final Interaction interaction, - final InteractionContext interactionContext) { + final InteractionContext interactionContext, + final Observation observation) { var parent = currentLayer().orElse(null); - var newLayer = new InteractionLayer(parent, interaction, interactionContext); + @SuppressWarnings("resource") + var newLayer = new InteractionLayer(parent, interaction, interactionContext, new ObservationClosure().startAndOpenScope(observation)); threadLocalLayer.set(newLayer); return newLayer; } public void clear() { + currentLayer().ifPresent(InteractionLayer::closeAll); threadLocalLayer.remove(); } @@ -67,9 +73,11 @@ public InteractionLayer peek() { @Nullable public InteractionLayer pop() { var current = threadLocalLayer.get(); - return set(current != null - ? current.parent() - : null); + if(current==null) return null; + + var newTop = current.parent(); + current.close(); + return set(newTop); } public void popWhile(final Predicate condition) { diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java index f4387ed04d9..2d7ea230a6b 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java @@ -19,10 +19,17 @@ package org.apache.causeway.commons.internal.observation; import java.util.Optional; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; +import lombok.Data; +import lombok.experimental.Accessors; + import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Scope; import io.micrometer.observation.ObservationRegistry; /** @@ -73,4 +80,47 @@ public ObservationProvider provider(final Class bean) { return name->createNotStarted(bean, name); } + /** + * Helps if start and stop of an {@link Observation} happen in different code locations. + */ + @Data @Accessors(fluent = true) + public static class ObservationClosure implements AutoCloseable { + + private Observation observation; + private Scope scope; + + public ObservationClosure startAndOpenScope(final Observation observation) { + if(observation==null) return this; + this.observation = observation.start(); + this.scope = observation.openScope(); + return this; + } + + @Override + public void close() { + if(observation==null) return; + if(scope!=null) { + this.scope.close(); + this.scope = null; + } + observation.stop(); + } + + public void onError(final Exception ex) { + if(observation==null) return; + observation.error(ex); + } + + public ObservationClosure tag(final String key, @Nullable final Supplier valueSupplier) { + if(observation==null || valueSupplier == null) return this; + try { + observation.highCardinalityKeyValue(key, "" + valueSupplier.get()); + } catch (Exception e) { + observation.highCardinalityKeyValue(key, "EXCEPTION: " + e.getMessage()); + } + return this; + } + + } + } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 7f2f854fa20..5930eaa18a0 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -209,23 +209,20 @@ private InteractionLayer openInteraction(final @NonNull InteractionContext inter // we are done, just return the stack's top return currentInteractionLayerElseFail(); - var newInteractionLayer = observationProvider.get("New Interaction Layer") - .highCardinalityKeyValue("stackSize", ""+getInteractionLayerCount()) - .observe(()->{ + var causewayInteraction = currentInteractionLayer() + .map(InteractionLayer::interaction) + .map(it->(CausewayInteraction)it) + .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId())); - var causewayInteraction = currentInteractionLayer() - .map(InteractionLayer::interaction) - .map(it->(CausewayInteraction)it) - .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId())); - var interactionLayer = layerStack.push(causewayInteraction, interactionContextToUse); + var obs = observationProvider.get("Causeway Layered Interaction") + .highCardinalityKeyValue("stackSize", ""+getInteractionLayerCount()); - if(isAtTopLevel()) { - transactionServiceSpring.onOpen(causewayInteraction); - interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); - } + var newInteractionLayer = layerStack.push(causewayInteraction, interactionContextToUse, obs); - return interactionLayer; - }); + if(isAtTopLevel()) { + transactionServiceSpring.onOpen(causewayInteraction); + interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); + } if(log.isDebugEnabled()) { log.debug("new interaction layer created (interactionId={}, total-layers-on-stack={}, {})", diff --git a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java index 95f18fcb0f2..54cf6b2baba 100644 --- a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java +++ b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java @@ -38,6 +38,8 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import io.micrometer.observation.Observation; + /** * A pass-through implementation, free of side-effects, * in support of simple JUnit tests. @@ -56,7 +58,7 @@ private InteractionLayer openInteraction() { // @Override private InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) { final Interaction interaction = new Interaction_forTesting(); - return layerStack.push(interaction, interactionContext); + return layerStack.push(interaction, interactionContext, Observation.NOOP); } @Override diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java index bc2263624c6..8dedb9ed985 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -24,14 +24,14 @@ import org.apache.wicket.request.cycle.RequestCycleContext; import org.jspecify.annotations.Nullable; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; + import io.micrometer.observation.Observation; -import io.micrometer.observation.Observation.Scope; public class RequestCycle2 extends RequestCycle { final long startTimeNanos; - private Observation observation; - private Scope scope; + final ObservationClosure observationClosure = new ObservationClosure(); public RequestCycle2(final RequestCycleContext context) { super(context); @@ -43,32 +43,16 @@ long millisSinceStart() { } void observationStartAndOpenScope(final Observation observation) { - if(observation==null) return; - this.observation = observation.start(); - this.scope = observation.openScope(); + observationClosure.startAndOpenScope(observation); } - void observationCloseScopeAndStop() { - if(observation==null) return; - if(scope!=null) { - this.scope.close(); - this.scope = null; - } - observation.stop(); + observationClosure.close(); } - void onObservationError(final Exception ex) { - if(observation==null) return; - observation.error(ex); + observationClosure.onError(ex); } - void observationTag(final String key, @Nullable final Supplier valueSupplier) { - if(observation==null || valueSupplier == null) return; - try { - observation.highCardinalityKeyValue(key, "" + valueSupplier.get()); - } catch (Exception e) { - observation.highCardinalityKeyValue(key, "EXCEPTION: " + e.getMessage()); - } + observationClosure.tag(key, valueSupplier); } } From 7258e3d6b4fe2a4c30371744168f8eb561a773cc Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 09:41:26 +0100 Subject: [PATCH 09/31] CAUSEWAY-3975: cleaning up --- .../conf/Configuration_usingWicket.java | 4 +- .../viewer/integration/RequestCycle2.java | 20 +------ .../viewer/integration/RootRequestMapper.java | 56 ------------------- .../integration/TelemetryStartHandler.java | 4 +- .../integration/TelemetryStopHandler.java | 6 +- .../wicketapp/CausewayWicketApplication.java | 1 - 6 files changed, 8 insertions(+), 83 deletions(-) delete mode 100644 viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java index e8c488746d9..c01c6cf8cc4 100644 --- a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/conf/Configuration_usingWicket.java @@ -61,7 +61,7 @@ import org.apache.causeway.viewer.wicket.ui.pages.PageClassRegistry; import org.apache.causeway.viewer.wicket.ui.pages.obj.DomainObjectPage; import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer; -import org.apache.causeway.viewer.wicket.viewer.integration.RootRequestMapper; +import org.apache.causeway.viewer.wicket.viewer.integration.RehydrationHandler; import lombok.AccessLevel; import lombok.Getter; @@ -349,7 +349,7 @@ protected IPageFactory newPageFactory() { @Override protected void internalInit() { super.internalInit(); - setRootRequestMapper(new RootRequestMapper(this)); + getRequestCycleListeners().add(new RehydrationHandler()); } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java index 8dedb9ed985..7bda8be00ba 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -18,20 +18,15 @@ */ package org.apache.causeway.viewer.wicket.viewer.integration; -import java.util.function.Supplier; - import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.cycle.RequestCycleContext; -import org.jspecify.annotations.Nullable; import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; -import io.micrometer.observation.Observation; - public class RequestCycle2 extends RequestCycle { final long startTimeNanos; - final ObservationClosure observationClosure = new ObservationClosure(); + public final ObservationClosure observationClosure = new ObservationClosure(); public RequestCycle2(final RequestCycleContext context) { super(context); @@ -42,17 +37,4 @@ long millisSinceStart() { return (System.nanoTime() - startTimeNanos)/1000_000; } - void observationStartAndOpenScope(final Observation observation) { - observationClosure.startAndOpenScope(observation); - } - void observationCloseScopeAndStop() { - observationClosure.close(); - } - void onObservationError(final Exception ex) { - observationClosure.onError(ex); - } - void observationTag(final String key, @Nullable final Supplier valueSupplier) { - observationClosure.tag(key, valueSupplier); - } - } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java deleted file mode 100644 index 965c6d76b0d..00000000000 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RootRequestMapper.java +++ /dev/null @@ -1,56 +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.viewer.wicket.viewer.integration; - -import org.apache.wicket.Application; -import org.apache.wicket.SystemMapper; -import org.apache.wicket.core.request.handler.ListenerRequestHandler; -import org.apache.wicket.core.request.mapper.PageInstanceMapper; -import org.apache.wicket.request.IRequestHandler; -import org.apache.wicket.request.IRequestMapper; -import org.apache.wicket.request.Request; - -import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract; - -@Deprecated // was replaced by RehydrationHandler -public final class RootRequestMapper extends SystemMapper implements IRequestMapper { - - @Deprecated - public RootRequestMapper(final Application application) { - super(application); - } - - // intercept AJAX requests and reload view-models so any detached entities are re-fetched - @Deprecated - @Override - protected IRequestMapper newPageInstanceMapper() { - return new PageInstanceMapper() { - @Override - public IRequestHandler mapRequest(final Request request) { - var handler = super.mapRequest(request); - if (handler instanceof ListenerRequestHandler listenerRequestHandler - && listenerRequestHandler.getPage() instanceof PageAbstract pageAbstract) { - pageAbstract.onNewRequestCycle(); - } - return handler; - } - }; - } - -} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index fdec3182d5d..daeef157a43 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -34,7 +34,7 @@ public record TelemetryStartHandler( @Override public synchronized void onBeginRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { - requestCycle2.observationStartAndOpenScope( + requestCycle2.observationClosure.startAndOpenScope( observationProvider.get("Apache Wicket Request Cycle")); } } @@ -42,7 +42,7 @@ public synchronized void onBeginRequest(final RequestCycle requestCycle) { @Override public IRequestHandler onException(final RequestCycle requestCycle, final Exception ex) { if (requestCycle instanceof RequestCycle2 requestCycle2) { - requestCycle2.onObservationError(ex); + requestCycle2.observationClosure.onError(ex); } return null; } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java index def31d79e24..646bad2c5b3 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java @@ -35,11 +35,11 @@ public void onEndRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { if(requestCycle2.millisSinceStart() > 50) { // avoid clutter - requestCycle2.observationTag("numberEntitiesLoaded", metricsService::numberEntitiesLoaded); - requestCycle2.observationTag("numberEntitiesDirtied", metricsService::numberEntitiesDirtied); + requestCycle2.observationClosure.tag("numberEntitiesLoaded", metricsService::numberEntitiesLoaded); + requestCycle2.observationClosure.tag("numberEntitiesDirtied", metricsService::numberEntitiesDirtied); } - requestCycle2.observationCloseScopeAndStop(); + requestCycle2.observationClosure.close(); } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 1cc27a57d4b..597e9548e82 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -210,7 +210,6 @@ protected void init() { .await(); setRequestCycleProvider(RequestCycle2::new); - //setRootRequestMapper(new RootRequestMapper(this)); getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER); getResourceSettings().setParentFolderPlaceholder("$up$"); From ee866125f69d9a580051b93b11fa7a231445217a Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 10:07:50 +0100 Subject: [PATCH 10/31] CAUSEWAY-3975: separation of concerns (mm init) --- .../CausewayModuleCoreMetamodel.java | 6 +- .../services/init/MetamodelInitializer.java | 108 ++++++++++++++++++ .../session/InteractionServiceDefault.java | 67 +---------- 3 files changed, 114 insertions(+), 67 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index 4f6b1a6b210..4b251c0e9cc 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -67,6 +67,7 @@ import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault; import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault; import org.apache.causeway.core.metamodel.services.idstringifier.IdStringifierLookupService; +import org.apache.causeway.core.metamodel.services.init.MetamodelInitializer; import org.apache.causeway.core.metamodel.services.inject.ServiceInjectorDefault; import org.apache.causeway.core.metamodel.services.layout.LayoutServiceDefault; import org.apache.causeway.core.metamodel.services.metamodel.MetaModelServiceDefault; @@ -206,7 +207,10 @@ LogicalTypeMalformedValidator.class, // menubar contributions - MetamodelInspectMenu.class + MetamodelInspectMenu.class, + + //last + MetamodelInitializer.class }) public class CausewayModuleCoreMetamodel { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java new file mode 100644 index 00000000000..c9d817a96cf --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java @@ -0,0 +1,108 @@ +/* + * 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.metamodel.services.init; + +import java.io.File; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import org.apache.causeway.applib.events.metamodel.MetamodelEvent; +import org.apache.causeway.applib.services.eventbus.EventBusService; +import org.apache.causeway.applib.util.schema.ChangesDtoUtils; +import org.apache.causeway.applib.util.schema.CommandDtoUtils; +import org.apache.causeway.applib.util.schema.InteractionDtoUtils; +import org.apache.causeway.applib.util.schema.InteractionsDtoUtils; +import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; +import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public record MetamodelInitializer( + EventBusService eventBusService, + Provider specificationLoaderProvider, + ObservationProvider observationProvider) { + + @Inject + public MetamodelInitializer( + final EventBusService eventBusService, + final Provider specificationLoaderProvider, + @Qualifier("causeway-metamodel") + final CausewayObservationInternal observation) { + this(eventBusService, specificationLoaderProvider, observation.provider(MetamodelInitializer.class)); + } + + @EventListener + public void init(final ContextRefreshedEvent event) { + log.info("Initialising Causeway System"); + log.info("working directory: {}", new File(".").getAbsolutePath()); + + observationProvider.get("Initialising Causeway System").observe(() -> { + observationProvider.get("Notify BEFORE_METAMODEL_LOADING Listeners").observe(() -> { + eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING); + }); + + observationProvider.get("Initialising Causeway Metamodel").observe(() -> { + initMetamodel(specificationLoaderProvider.get()); + }); + + observationProvider.get("Notify AFTER_METAMODEL_LOADED Listeners").observe(() -> { + eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED); + }); + }); + } + + private void initMetamodel(final SpecificationLoader specificationLoader) { + + var taskList = _ConcurrentTaskList.named("CausewayInteractionFactoryDefault Init") + .addRunnable("SpecificationLoader::createMetaModel", specificationLoader::createMetaModel) + .addRunnable("ChangesDtoUtils::init", ChangesDtoUtils::init) + .addRunnable("InteractionDtoUtils::init", InteractionDtoUtils::init) + .addRunnable("InteractionsDtoUtils::init", InteractionsDtoUtils::init) + .addRunnable("CommandDtoUtils::init", CommandDtoUtils::init) + ; + + taskList.submit(_ConcurrentContext.forkJoin()); + taskList.await(); + + { // log any validation failures, experimental code however, not sure how to best propagate failures + var validationResult = specificationLoader.getOrAssessValidationResult(); + if(validationResult.getNumberOfFailures()==0) { + log.info("Validation PASSED"); + } else { + log.error("### Validation FAILED, failure count: {}", validationResult.getNumberOfFailures()); + validationResult.forEach(failure->{ + log.error("# " + failure.message()); + }); + //throw _Exceptions.unrecoverable("Validation FAILED"); + } + } + } + +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 5930eaa18a0..61738dd9d12 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -18,7 +18,6 @@ */ package org.apache.causeway.core.runtimeservices.session; -import java.io.File; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -34,13 +33,10 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.annotation.Programmatic; -import org.apache.causeway.applib.events.metamodel.MetamodelEvent; import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.command.Command; import org.apache.causeway.applib.services.eventbus.EventBusService; @@ -51,13 +47,7 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.inject.ServiceInjector; -import org.apache.causeway.applib.util.schema.ChangesDtoUtils; -import org.apache.causeway.applib.util.schema.CommandDtoUtils; -import org.apache.causeway.applib.util.schema.InteractionDtoUtils; -import org.apache.causeway.applib.util.schema.InteractionsDtoUtils; import org.apache.causeway.commons.functional.ThrowingRunnable; -import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; -import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; @@ -97,9 +87,7 @@ public class InteractionServiceDefault final InteractionLayerStack layerStack = new InteractionLayerStack(); - final EventBusService eventBusService; final ObservationProvider observationProvider; - final Provider specificationLoaderProvider; final ServiceInjector serviceInjector; final ClockService clockService; @@ -111,6 +99,7 @@ public class InteractionServiceDefault final InteractionIdGenerator interactionIdGenerator; + @SuppressWarnings("exports") @Inject public InteractionServiceDefault( final EventBusService eventBusService, @@ -123,9 +112,7 @@ public InteractionServiceDefault( final Provider commandPublisherProvider, final ConfigurableBeanFactory beanFactory, final InteractionIdGenerator interactionIdGenerator) { - this.eventBusService = eventBusService; this.observationProvider = observation.provider(getClass()); - this.specificationLoaderProvider = specificationLoaderProvider; this.serviceInjector = serviceInjector; this.transactionServiceSpring = transactionServiceSpring; this.clockService = clockService; @@ -135,58 +122,6 @@ public InteractionServiceDefault( this.interactionScopeLifecycleHandler = InteractionScopeBeanFactoryPostProcessor.lookupScope(beanFactory); } - @EventListener - public void init(final ContextRefreshedEvent event) { - log.info("Initialising Causeway System"); - log.info("working directory: {}", new File(".").getAbsolutePath()); - - observationProvider.get("Initialising Causeway System") - .observe(()->{ - observationProvider.get("Notify BEFORE_METAMODEL_LOADING Listeners") - .observe(()->{ - eventBusService.post(MetamodelEvent.BEFORE_METAMODEL_LOADING); - }); - - observationProvider.get("Initialising Causeway Metamodel") - .observe(()->{ - initMetamodel(specificationLoaderProvider.get()); - }); - - observationProvider.get("Notify AFTER_METAMODEL_LOADED Listeners") - .observe(()->{ - eventBusService.post(MetamodelEvent.AFTER_METAMODEL_LOADED); - }); - }); - } - - //TODO this is a metamodel concern, why is it in runtime services? - private void initMetamodel(final SpecificationLoader specificationLoader) { - - var taskList = _ConcurrentTaskList.named("CausewayInteractionFactoryDefault Init") - .addRunnable("SpecificationLoader::createMetaModel", specificationLoader::createMetaModel) - .addRunnable("ChangesDtoUtils::init", ChangesDtoUtils::init) - .addRunnable("InteractionDtoUtils::init", InteractionDtoUtils::init) - .addRunnable("InteractionsDtoUtils::init", InteractionsDtoUtils::init) - .addRunnable("CommandDtoUtils::init", CommandDtoUtils::init) - ; - - taskList.submit(_ConcurrentContext.forkJoin()); - taskList.await(); - - { // log any validation failures, experimental code however, not sure how to best propagate failures - var validationResult = specificationLoader.getOrAssessValidationResult(); - if(validationResult.getNumberOfFailures()==0) { - log.info("Validation PASSED"); - } else { - log.error("### Validation FAILED, failure count: {}", validationResult.getNumberOfFailures()); - validationResult.forEach(failure->{ - log.error("# " + failure.message()); - }); - //throw _Exceptions.unrecoverable("Validation FAILED"); - } - } - } - @Override public int getInteractionLayerCount() { return layerStack.size(); From 292dcd19ae82b8891fa827fc61b59613bb198cb6 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 10:18:33 +0100 Subject: [PATCH 11/31] CAUSEWAY-3975: revert intro of TestSupport (1) --- .../iactnlayer/InteractionService.java | 70 ++++++++++--------- .../session/InteractionServiceDefault.java | 14 ++-- .../InteractionService_forTesting.java | 8 +-- .../CommandLog_IntegTestAbstract.java | 2 +- .../ExecutionLog_IntegTestAbstract.java | 2 +- .../ExecutionOutbox_IntegTestAbstract.java | 18 ++--- .../applib/CausewayInteractionHandler.java | 8 +-- .../applib/NoPermissionChecks.java | 2 +- .../applib/UserMementoRefiners.java | 2 +- .../WebRequestCycleForCauseway.java | 2 +- 10 files changed, 62 insertions(+), 66 deletions(-) diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java index 60845096603..ea7a306bd12 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java @@ -56,6 +56,42 @@ */ public interface InteractionService extends InteractionLayerTracker { + /** + * If present, reuses the current top level {@link InteractionLayer}, otherwise creates a new + * anonymous one. + * + * @see #openInteraction(InteractionContext) + */ + InteractionLayer openInteraction(); + + /** + * Returns a new or reused {@link InteractionLayer} that is a holder of the {@link InteractionContext} + * on top of the current thread's interaction layer stack. + * + *

+ * If available reuses an existing {@link InteractionContext}, otherwise creates a new one. + *

+ * + *

+ * The {@link InteractionLayer} represents a user's span of activities interacting with + * the application. These can be stacked (usually temporarily), for example for a sudo + * session or to mock the clock. The stack is later closed using {@link #closeInteractionLayers()}. + *

+ * + * @param interactionContext + * + * @apiNote if the current {@link InteractionLayer} (if any) has an {@link InteractionContext} that + * equals that of the given one, as an optimization, no new layer is pushed onto the stack; + * instead the current one is returned + */ + InteractionLayer openInteraction( + @NonNull InteractionContext interactionContext); + + /** + * closes all open {@link InteractionLayer}(s) as stacked on the current thread + */ + void closeInteractionLayers(); + /** * @return whether the calling thread is within the context of an open {@link InteractionLayer} */ @@ -169,11 +205,6 @@ default Try runAnonymousAndCatch( return callAnonymousAndCatch(runnable.toCallable()); } - /** - * closes all open {@link InteractionLayer}(s) as stacked on the current thread - */ - void closeInteractionLayers(); - public interface TestSupport { T model(); /** @@ -198,35 +229,6 @@ public interface TestSupport { * @see #nextInteraction() */ void nextInteraction(final InteractionContext interactionContext, final Consumer callback); - /** - * If present, reuses the current top level {@link InteractionLayer}, otherwise creates a new - * anonymous one. - * - * @see #openInteraction(InteractionContext) - */ - InteractionLayer openInteraction(); - /** - * Returns a new or reused {@link InteractionLayer} that is a holder of the {@link InteractionContext} - * on top of the current thread's interaction layer stack. - * - *

- * If available reuses an existing {@link InteractionContext}, otherwise creates a new one. - *

- * - *

- * The {@link InteractionLayer} represents a user's span of activities interacting with - * the application. These can be stacked (usually temporarily), for example for a sudo - * session or to mock the clock. The stack is later closed using {@link #closeInteractionLayers()}. - *

- * - * @param interactionContext - * - * @apiNote if the current {@link InteractionLayer} (if any) has an {@link InteractionContext} that - * equals that of the given one, as an optimization, no new layer is pushed onto the stack; - * instead the current one is returned - */ - InteractionLayer openInteraction( - @NonNull InteractionContext interactionContext); } TestSupport testSupport(T model); diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 61738dd9d12..45e0c081f60 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -127,13 +127,15 @@ public int getInteractionLayerCount() { return layerStack.size(); } - private InteractionLayer openInteraction() { + @Override + public InteractionLayer openInteraction() { return currentInteractionLayer() // or else create an anonymous authentication layer .orElseGet(()->openInteraction(InteractionContextFactory.anonymous())); } - private InteractionLayer openInteraction(final @NonNull InteractionContext interactionContextToUse) { + @Override + public InteractionLayer openInteraction(final @NonNull InteractionContext interactionContextToUse) { // check whether we should reuse any current interactionLayer, // that is, if current authentication and authToUse are equal @@ -465,14 +467,6 @@ public void nextInteraction(final InteractionContext interactionContext, final C interactionService.openInteraction(interactionContext); callback.accept(model); } - @Override - public InteractionLayer openInteraction() { - return interactionService.openInteraction(); - } - @Override - public InteractionLayer openInteraction(@NonNull final InteractionContext interactionContext) { - return interactionService.openInteraction(interactionContext); - } } @Override diff --git a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java index 54cf6b2baba..c71da8c8faa 100644 --- a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java +++ b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java @@ -49,14 +49,14 @@ public class InteractionService_forTesting final InteractionLayerStack layerStack = new InteractionLayerStack(); -// @Override - private InteractionLayer openInteraction() { + @Override + public InteractionLayer openInteraction() { final UserMemento userMemento = UserMemento.system(); return openInteraction(InteractionContext.ofUserWithSystemDefaults(userMemento)); } -// @Override - private InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) { + @Override + public InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) { final Interaction interaction = new Interaction_forTesting(); return layerStack.push(interaction, interactionContext, Observation.NOOP); } 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 8d29e0e4052..8057f15c7ee 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 @@ -345,7 +345,7 @@ class Model { } ); }); - testSupport.openInteraction(); + interactionService.openInteraction(); // given (same user, different target, same day) counter2 = counterRepository.findByName("counter-2"); diff --git a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java index 5558698bd76..77e4b7c623a 100644 --- a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java +++ b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java @@ -310,7 +310,7 @@ class Model { }); }); - testSupport.openInteraction(); + interactionService.openInteraction(); // given (same user, different target, same day) counter2 = counterRepository.findByName("counter-2"); diff --git a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java index c98204dd148..9455cc3d7a9 100644 --- a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java +++ b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java @@ -89,7 +89,7 @@ void invoke_mixin() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -120,7 +120,7 @@ void invoke_direct() { wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -151,7 +151,7 @@ void invoke_mixin_disabled() { wrapperFactory.wrapMixin(Counter_bumpUsingMixinWithExecutionPublishingDisabled.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -165,7 +165,7 @@ void invoke_direct_disabled() { wrapperFactory.wrap(counter1).bumpUsingDeclaredActionWithExecutionPublishingDisabled(); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -179,7 +179,7 @@ void edit() { wrapperFactory.wrap(counter1).setNum(99L); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -209,7 +209,7 @@ void edit_disabled() { wrapperFactory.wrap(counter1).setNum2(99L); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // then List all = executionOutboxEntryRepository.findOldest(); @@ -223,7 +223,7 @@ void roundtrip_EOE_bookmarks() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); List all = executionOutboxEntryRepository.findOldest(); ExecutionOutboxEntry executionLogEntry = all.get(0); @@ -241,7 +241,7 @@ void roundtrip_EOE_bookmarks() { // when we start a new session and lookup from the bookmark interactionService.closeInteractionLayers(); - testSupport.openInteraction(); + interactionService.openInteraction(); Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); assertThat(cle2IfAny).isPresent(); @@ -260,7 +260,7 @@ void test_all_the_repository_methods() { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); }); interactionService.closeInteractionLayers(); // to flush - testSupport.openInteraction(); + interactionService.openInteraction(); // when List executionTarget1User1IfAny = executionOutboxEntryRepository.findOldest(); diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java index 51f4fbea87b..cd1a1ea1c66 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java @@ -47,12 +47,12 @@ public void beforeEach(final ExtensionContext extensionContext) throws Exception _Helper .getInteractionFactory(extensionContext) - .map(InteractionService::testSupport) - .ifPresent(testSupport-> + .ifPresent(interactionService-> _Helper .getCustomInteractionContext(extensionContext) - .map(testSupport::openInteraction) - .orElseGet(testSupport::openInteraction)); + .ifPresentOrElse( + interactionService::openInteraction, + interactionService::openInteraction)); } @Override diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java index ba0ff65fbf9..00a9d0dec0d 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/NoPermissionChecks.java @@ -49,7 +49,7 @@ public void beforeEach(final ExtensionContext extensionContext) { interactionService.currentInteractionContext().ifPresent( currentInteractionContext -> { var sudoUser = currentInteractionContext.getUser().withRoleAdded(SudoService.ACCESS_ALL_ROLE.name()); - interactionService.testSupport().openInteraction(currentInteractionContext.withUser(sudoUser)); + interactionService.openInteraction(currentInteractionContext.withUser(sudoUser)); } ) ); diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java index 1a97dbb1774..59237a96771 100644 --- a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java +++ b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/UserMementoRefiners.java @@ -53,7 +53,7 @@ public void beforeEach(final ExtensionContext extensionContext) { for (UserMementoRefiner userMementoRefiner : serviceRegistry.select(UserMementoRefiner.class)) { user = userMementoRefiner.refine(user); } - interactionService.testSupport().openInteraction(currentInteractionContext.withUser(user)); + interactionService.openInteraction(currentInteractionContext.withUser(user)); } ) ) diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java index b2f1650080e..d38b0743f6b 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java @@ -146,7 +146,7 @@ public void onBeginRequest(final RequestCycle requestCycle) { } sessionAuthenticator.determineInteractionContext() - .ifPresent(interactionService.testSupport()::openInteraction); + .ifPresent(interactionService::openInteraction); } @Override From 708bbcb86d0e7a30f1bb7ef6e6165cdc301cb73e Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 21 Mar 2026 10:25:19 +0100 Subject: [PATCH 12/31] CAUSEWAY-3975: revert intro of TestSupport (2) --- .../iactnlayer/InteractionService.java | 55 +- .../session/InteractionServiceDefault.java | 28 - .../InteractionService_forTesting.java | 5 - .../BackgroundService_IntegTestAbstract.java | 47 +- .../CommandLog_IntegTestAbstract.java | 598 +++++++++--------- .../ExecutionLog_IntegTestAbstract.java | 233 ++++--- .../ExecutionOutbox_IntegTestAbstract.java | 3 - .../AuditTrail_IntegTestAbstract.java | 246 ++++--- ...CmdExecAuditSessLog_IntegTestAbstract.java | 208 +++--- .../integtest/Layout_Counter_IntegTest.java | 14 +- 10 files changed, 660 insertions(+), 777 deletions(-) diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java index ea7a306bd12..8778a90480e 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionService.java @@ -19,7 +19,6 @@ package org.apache.causeway.applib.services.iactnlayer; import java.util.concurrent.Callable; -import java.util.function.Consumer; import org.jspecify.annotations.NonNull; @@ -205,36 +204,34 @@ default Try runAnonymousAndCatch( return callAnonymousAndCatch(runnable.toCallable()); } - public interface TestSupport { - T model(); - /** - * Primarily for testing, closes the current interaction and opens a new one. - * - *

- * In tests, this is a good way to simulate multiple interactions within a scenario. If you use the popular - * given/when/then structure, consider using at the end of each "given" or "when" block. - *

- * - * @see #closeInteractionLayers() - * @see #openInteraction() - * @see #nextInteraction(InteractionContext) - */ - void nextInteraction(final Consumer callback); - /** - * Primarily for testing, closes the current interaction and opens a new one with the specified - * {@link InteractionContext}. - * - * @see #closeInteractionLayers() - * @see #openInteraction(InteractionContext) - * @see #nextInteraction() - */ - void nextInteraction(final InteractionContext interactionContext, final Consumer callback); + /** + * Primarily for testing, closes the current interaction and opens a new one. + * + *

+ * In tests, this is a good way to simulate multiple interactions within a scenario. If you use the popular + * given/when/then structure, consider using at the end of each "given" or "when" block. + *

+ * + * @see #closeInteractionLayers() + * @see #openInteraction() + * @see #nextInteraction(InteractionContext) + */ + default InteractionLayer nextInteraction() { + closeInteractionLayers(); + return openInteraction(); } - TestSupport testSupport(T model); - - default TestSupport testSupport() { - return testSupport(null); + /** + * Primarily for testing, closes the current interaction and opens a new one with the specified + * {@link InteractionContext}. + * + * @see #closeInteractionLayers() + * @see #openInteraction(InteractionContext) + * @see #nextInteraction() + */ + default InteractionLayer nextInteraction(final InteractionContext interactionContext) { + closeInteractionLayers(); + return openInteraction(interactionContext); } } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 45e0c081f60..841f5f582db 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -22,7 +22,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.function.Consumer; import jakarta.annotation.Priority; import jakarta.inject.Inject; @@ -62,10 +61,7 @@ import org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring; import org.apache.causeway.core.security.authentication.InteractionContextFactory; -import lombok.Getter; import lombok.SneakyThrows; -import lombok.Value; -import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; /** @@ -450,28 +446,4 @@ public void completeAndPublishCurrentCommand() { interaction.clear(); } - @Value - static final class TestSupportImpl implements TestSupport { - @Getter @Accessors(fluent = true) final T model; - final InteractionServiceDefault interactionService; - - @Override - public void nextInteraction(final Consumer callback) { - interactionService.closeInteractionLayers(); - interactionService.openInteraction(); - callback.accept(model); - } - @Override - public void nextInteraction(final InteractionContext interactionContext, final Consumer callback) { - interactionService.closeInteractionLayers(); - interactionService.openInteraction(interactionContext); - callback.accept(model); - } - } - - @Override - public TestSupport testSupport(final T model) { - return new TestSupportImpl<>(model, this); - } - } diff --git a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java index c71da8c8faa..18c0d0909d9 100644 --- a/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java +++ b/core/security/src/main/java/org/apache/causeway/core/security/_testing/InteractionService_forTesting.java @@ -140,9 +140,4 @@ static class Interaction_forTesting implements Interaction { @Override public Execution getPriorExecution() { return null; } } - @Override - public TestSupport testSupport(final T model) { - throw new UnsupportedOperationException(); - } - } 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 49124ebbc3a..cb09a0b956a 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 @@ -38,7 +38,6 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy; import org.apache.causeway.applib.services.xactn.TransactionService; @@ -70,7 +69,6 @@ public abstract class BackgroundService_IntegTestAbstract extends CausewayIntegr JobExecutionContext mockQuartzJobExecutionContext = Mockito.mock(JobExecutionContext.class); Bookmark bookmark; - private TestSupport testSupport; protected abstract T newCounter(String name); @@ -89,7 +87,6 @@ static void reset_environment() { @BeforeEach void setup_counter() { - this.testSupport = interactionService.testSupport(); transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { counterRepository.removeAll(); @@ -214,29 +211,27 @@ void using_background_service() { // when (simulate quartz running in the background) runBackgroundCommandsJob.execute(mockQuartzJobExecutionContext); - testSupport.nextInteraction(ia->{ - - // then bumped - transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { - var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow(); - assertThat(counter.getNum()).isEqualTo(1L); - }).ifFailureFail(); - - // and marked as started and completed - transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { - var after = commandLogEntryRepository.findAll(); - assertThat(after).hasSize(1); - CommandLogEntry commandLogEntryAfter = after.get(0); - - assertThat(commandLogEntryAfter) - .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getCompletedAt()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getResult()).isNotNull()) // changed - .satisfies(x -> assertThat(x.getResultSummary()).isNotNull()) // changed - ; - }).ifFailureFail(); - - }); + interactionService.nextInteraction(); + + // then bumped + transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { + var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow(); + assertThat(counter.getNum()).isEqualTo(1L); + }).ifFailureFail(); + + // and marked as started and completed + transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { + var after = commandLogEntryRepository.findAll(); + assertThat(after).hasSize(1); + CommandLogEntry commandLogEntryAfter = after.get(0); + + assertThat(commandLogEntryAfter) + .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getCompletedAt()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getResult()).isNotNull()) // changed + .satisfies(x -> assertThat(x.getResultSummary()).isNotNull()) // changed + ; + }).ifFailureFail(); } 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 8057f15c7ee..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 @@ -39,7 +39,6 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -59,17 +58,6 @@ public abstract class CommandLog_IntegTestAbstract extends CausewayIntegrationTestAbstract { - @Inject CommandLogEntryRepository commandLogEntryRepository; - @Inject SudoService sudoService; - @Inject ClockService clockService; - @Inject InteractionService interactionService; - @Inject InteractionLayerTracker interactionLayerTracker; - @Inject CounterRepository counterRepository; - @Inject WrapperFactory wrapperFactory; - @Inject BookmarkService bookmarkService; - @Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry; - - @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -77,25 +65,23 @@ static void beforeAll() { Counter counter1; Counter counter2; - private TestSupport testSupport; @BeforeEach void beforeEach() { - this.testSupport = interactionService.testSupport(); - testSupport.nextInteraction(ia->{ - counterRepository.removeAll(); - commandLogEntryRepository.removeAll(); + interactionService.nextInteraction(); - assertThat(counterRepository.find()).isEmpty(); + counterRepository.removeAll(); + commandLogEntryRepository.removeAll(); - counter1 = counterRepository.persist(newCounter("counter-1")); - counter2 = counterRepository.persist(newCounter("counter-2")); + assertThat(counterRepository.find()).isEmpty(); - assertThat(counterRepository.find()).hasSize(2); + counter1 = counterRepository.persist(newCounter("counter-1")); + counter2 = counterRepository.persist(newCounter("counter-2")); - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); - }); + assertThat(counterRepository.find()).hasSize(2); + + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); } protected abstract T newCounter(String name); @@ -105,34 +91,33 @@ void invoke_mixin() { // when wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - testSupport.nextInteraction(ia->{ - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingMixin"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNotNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - assertThat(commandLogEntry.getCommandDto()).isNotNull(); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); - }); + interactionService.nextInteraction(); + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingMixin"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNotNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + assertThat(commandLogEntry.getCommandDto()).isNotNull(); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); } @Test @@ -140,56 +125,57 @@ void invoke_direct() { // when wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); - testSupport.nextInteraction(ia->{ - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNotNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - assertThat(commandLogEntry.getCommandDto()).isNotNull(); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); - }); + interactionService.nextInteraction(); + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNotNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + assertThat(commandLogEntry.getCommandDto()).isNotNull(); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(ActionDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); } @Test void invoke_mixin_disabled() { + // when wrapperFactory.wrapMixin(Counter_bumpUsingMixinWithCommandPublishingDisabled.class, counter1).act(); - testSupport.nextInteraction(ia->{ - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); - }); + interactionService.nextInteraction(); + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); } @Test void invoke_direct_disabled() { + // when wrapperFactory.wrap(counter1).bumpUsingDeclaredActionWithCommandPublishingDisabled(); - testSupport.nextInteraction(ia->{ - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); - }); + interactionService.nextInteraction(); + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); } @Test @@ -197,33 +183,32 @@ void edit() { // when wrapperFactory.wrap(counter1).setNum(99L); - testSupport.nextInteraction(ia->{ - - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isPresent(); - - CommandLogEntry commandLogEntry = mostRecentCompleted.get(); - - assertThat(commandLogEntry.getInteractionId()).isNotNull(); - assertThat(commandLogEntry.getCompletedAt()).isNotNull(); - assertThat(commandLogEntry.getDuration()).isNotNull(); - assertThat(commandLogEntry.getException()).isEqualTo(""); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); - assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#num"); - assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); - assertThat(commandLogEntry.getResult()).isNull(); - assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK (VOID)"); - assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); - assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); - assertThat(commandLogEntry.getTarget()).isNotNull(); - assertThat(commandLogEntry.getTimestamp()).isNotNull(); - assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); - CommandDto commandDto = commandLogEntry.getCommandDto(); - assertThat(commandDto).isNotNull(); - assertThat(commandDto.getMember()).isInstanceOf(PropertyDto.class); - assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); - }); + interactionService.nextInteraction(); + + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isPresent(); + + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + + assertThat(commandLogEntry.getInteractionId()).isNotNull(); + assertThat(commandLogEntry.getCompletedAt()).isNotNull(); + assertThat(commandLogEntry.getDuration()).isNotNull(); + assertThat(commandLogEntry.getException()).isEqualTo(""); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isNotNull(); + assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#num"); + assertThat(commandLogEntry.getUsername()).isEqualTo("__system"); + assertThat(commandLogEntry.getResult()).isNull(); + assertThat(commandLogEntry.getResultSummary()).isEqualTo("OK (VOID)"); + assertThat(commandLogEntry.getReplayState()).isEqualTo(ReplayState.UNDEFINED); + assertThat(commandLogEntry.getReplayStateFailureReason()).isNull(); + assertThat(commandLogEntry.getTarget()).isNotNull(); + assertThat(commandLogEntry.getTimestamp()).isNotNull(); + assertThat(commandLogEntry.getType()).isEqualTo(DomainChangeRecord.ChangeType.COMMAND); + CommandDto commandDto = commandLogEntry.getCommandDto(); + assertThat(commandDto).isNotNull(); + assertThat(commandDto.getMember()).isInstanceOf(PropertyDto.class); + assertThat(commandDto.getMember().getLogicalMemberIdentifier()).isEqualTo(commandLogEntry.getLogicalMemberIdentifier()); } @Test @@ -231,271 +216,262 @@ void edit_disabled() { // when wrapperFactory.wrap(counter1).setNum2(99L); - testSupport.nextInteraction(ia->{ + interactionService.closeInteractionLayers(); // to flush - // then - Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - assertThat(mostRecentCompleted).isEmpty(); + interactionService.openInteraction(); - }); + // then + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); + assertThat(mostRecentCompleted).isEmpty(); } @Test void roundtrip_CLE_bookmarks() { - class Model { - CommandDto commandDto; - Bookmark cleBookmark; - } - var testSupport = interactionService.testSupport(new Model()); - // given wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - testSupport.nextInteraction(model->{ + interactionService.nextInteraction(); - CommandLogEntry commandLogEntry = commandLogEntryRepository.findMostRecentCompleted().get(); - model.commandDto = commandLogEntry.getCommandDto(); + Optional mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted(); - // when - Optional cleBookmarkIfAny = bookmarkService.bookmarkFor(commandLogEntry); - - // then - assertThat(cleBookmarkIfAny).isPresent(); - model.cleBookmark = cleBookmarkIfAny.get(); - String identifier = model.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 - } else { - UUID.fromString(identifier); // should not fail, ie check the format is as we expect - } + CommandLogEntry commandLogEntry = mostRecentCompleted.get(); + CommandDto commandDto = commandLogEntry.getCommandDto(); - }); + // when + Optional cleBookmarkIfAny = bookmarkService.bookmarkFor(commandLogEntry); + + // then + assertThat(cleBookmarkIfAny).isPresent(); + Bookmark cleBookmark = cleBookmarkIfAny.get(); + 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 + } else { + UUID.fromString(identifier); // should not fail, ie check the format is as we expect + } // when we start a new session and lookup from the bookmark - testSupport.nextInteraction(model->{ + interactionService.nextInteraction(); - Optional cle2IfAny = bookmarkService.lookup(model.cleBookmark); - assertThat(cle2IfAny).isPresent(); + Optional cle2IfAny = bookmarkService.lookup(cleBookmarkIfAny.get()); + assertThat(cle2IfAny).isPresent(); - CommandLogEntry cle2 = (CommandLogEntry) cle2IfAny.get(); - CommandDto commandDto2 = cle2.getCommandDto(); + CommandLogEntry cle2 = (CommandLogEntry) cle2IfAny.get(); + CommandDto commandDto2 = cle2.getCommandDto(); - assertThat(commandDto2).isEqualTo(model.commandDto); - }); + assertThat(commandDto2).isEqualTo(commandDto); } @Test void test_all_the_repository_methods() { - class Model { - UUID commandTarget1User1Id; - UUID commandTarget1User2Id; - UUID commandTarget1User1YesterdayId; - } - var testSupport = interactionService.testSupport(new Model()); - // given sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); }); + interactionService.nextInteraction(); - testSupport.nextInteraction(model->{ - // when - Optional commandTarget1User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); + // when + Optional commandTarget1User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); + + // then + Assertions.assertThat(commandTarget1User1IfAny).isPresent(); + var commandTarget1User1 = commandTarget1User1IfAny.get(); + var commandTarget1User1Id = commandTarget1User1.getInteractionId(); + + // given (different user, same target, same day) + counter1 = counterRepository.findByName("counter-1"); + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-2").build()), + () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() + ); + interactionService.nextInteraction(); - // then - Assertions.assertThat(commandTarget1User1IfAny).isPresent(); - var commandTarget1User1 = commandTarget1User1IfAny.get(); - model.commandTarget1User1Id = commandTarget1User1.getInteractionId(); - - // given (different user, same target, same day) - counter1 = counterRepository.findByName("counter-1"); - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-2").build()), - () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() - ); + // when + Optional commandTarget1User2IfAny = commandLogEntryRepository.findMostRecentCompleted(); + + // then + Assertions.assertThat(commandTarget1User2IfAny).isPresent(); + var commandTarget1User2 = commandTarget1User2IfAny.get(); + var commandTarget1User2Id = commandTarget1User2.getInteractionId(); + + // given (same user, same target, yesterday) + counter1 = counterRepository.findByName("counter-1"); + final UUID[] commandTarget1User1YesterdayIdHolder = new UUID[1]; + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-1").build()), + () -> { + var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); + sudoService.run( + InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), + () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + commandTarget1User1YesterdayIdHolder[0] = interactionLayerTracker.currentInteraction().get().getInteractionId(); + interactionService.closeInteractionLayers(); // to flush within changed time... + } + ); + }); + interactionService.openInteraction(); + + // when, then + final UUID commandTarget1User1YesterdayId = commandTarget1User1YesterdayIdHolder[0]; + + // given (same user, different target, same day) + counter2 = counterRepository.findByName("counter-2"); + sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); }); + interactionService.nextInteraction(); - testSupport.nextInteraction(model->{ - // when - Optional commandTarget1User2IfAny = commandLogEntryRepository.findMostRecentCompleted(); + // when + Optional commandTarget2User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); - // then - Assertions.assertThat(commandTarget1User2IfAny).isPresent(); - var commandTarget1User2 = commandTarget1User2IfAny.get(); - model.commandTarget1User2Id = commandTarget1User2.getInteractionId(); - - // given (same user, same target, yesterday) - counter1 = counterRepository.findByName("counter-1"); - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-1").build()), - () -> { - var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); - sudoService.run( - InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), - () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - // when, then - model.commandTarget1User1YesterdayId = interactionLayerTracker.currentInteraction().get().getInteractionId(); - interactionService.closeInteractionLayers(); // to flush within changed time... - } - ); - }); - interactionService.openInteraction(); - - // given (same user, different target, same day) - counter2 = counterRepository.findByName("counter-2"); - sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); - }); - }); + // then + Assertions.assertThat(commandTarget2User1IfAny).isPresent(); + var commandTarget2User1 = commandTarget2User1IfAny.get(); + var commandTarget2User1Id = commandTarget2User1.getInteractionId(); + + // when + Optional commandTarget1User1ById = commandLogEntryRepository.findByInteractionId(commandTarget1User1Id); + Optional commandTarget1User2ById = commandLogEntryRepository.findByInteractionId(commandTarget1User2Id); + Optional commandTarget1User1YesterdayById = commandLogEntryRepository.findByInteractionId(commandTarget1User1YesterdayId); + Optional commandTarget2User1ById = commandLogEntryRepository.findByInteractionId(commandTarget2User1Id); + + // then + Assertions.assertThat(commandTarget1User1ById).isPresent(); + Assertions.assertThat(commandTarget1User2ById).isPresent(); + Assertions.assertThat(commandTarget1User1YesterdayById).isPresent(); + Assertions.assertThat(commandTarget2User1ById).isPresent(); + Assertions.assertThat(commandTarget2User1ById.get()).isSameAs(commandTarget2User1); - testSupport.nextInteraction(model->{ + // given + commandTarget1User1 = commandTarget1User1ById.get(); + commandTarget1User2 = commandTarget1User2ById.get(); + @SuppressWarnings("unused") + var commandTarget1User1Yesterday = commandTarget1User1YesterdayById.get(); + commandTarget2User1 = commandTarget2User1ById.get(); - var commandTarget1User1Id = model.commandTarget1User1Id; - var commandTarget1User2Id = model.commandTarget1User2Id; - var commandTarget1User1YesterdayId = model.commandTarget1User1YesterdayId; + var target1 = commandTarget1User1.getTarget(); + var username1 = commandTarget1User1.getUsername(); + var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate(); + var to = from.plusDays(1); - // when - Optional commandTarget2User1IfAny = commandLogEntryRepository.findMostRecentCompleted(); + // when + List notYetReplayed = commandLogEntryRepository.findNotYetReplayed(); - // then - Assertions.assertThat(commandTarget2User1IfAny).isPresent(); - var commandTarget2User1 = commandTarget2User1IfAny.get(); - var commandTarget2User1Id = commandTarget2User1.getInteractionId(); + // then + Assertions.assertThat(notYetReplayed).isEmpty(); - // when - Optional commandTarget1User1ById = commandLogEntryRepository.findByInteractionId(commandTarget1User1Id); - Optional commandTarget1User2ById = commandLogEntryRepository.findByInteractionId(commandTarget1User2Id); - Optional commandTarget1User1YesterdayById = commandLogEntryRepository.findByInteractionId(commandTarget1User1YesterdayId); - Optional commandTarget2User1ById = commandLogEntryRepository.findByInteractionId(commandTarget2User1Id); + if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { - // then - Assertions.assertThat(commandTarget1User1ById).isPresent(); - Assertions.assertThat(commandTarget1User2ById).isPresent(); - Assertions.assertThat(commandTarget1User1YesterdayById).isPresent(); - Assertions.assertThat(commandTarget2User1ById).isPresent(); - Assertions.assertThat(commandTarget2User1ById.get()).isSameAs(commandTarget2User1); + // fails in JPA; possibly need to get the agent working for dirty tracking. // given - var commandTarget1User1 = commandTarget1User1ById.get(); - var commandTarget1User2 = commandTarget1User2ById.get(); - @SuppressWarnings("unused") - var commandTarget1User1Yesterday = commandTarget1User1YesterdayById.get(); - commandTarget2User1 = commandTarget2User1ById.get(); - - var target1 = commandTarget1User1.getTarget(); - var username1 = commandTarget1User1.getUsername(); - var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate(); - var to = from.plusDays(1); + commandTarget1User1.setReplayState(ReplayState.PENDING); // when - List notYetReplayed = commandLogEntryRepository.findNotYetReplayed(); + List notYetReplayed2 = commandLogEntryRepository.findNotYetReplayed(); // then - Assertions.assertThat(notYetReplayed).isEmpty(); + Assertions.assertThat(notYetReplayed2).hasSize(1); + Assertions.assertThat(notYetReplayed2.get(0).getInteractionId()).isEqualTo(commandTarget1User1.getInteractionId()); + } - if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { + // when + List byFromAndTo = commandLogEntryRepository.findByFromAndTo(from, to); - // fails in JPA; possibly need to get the agent working for dirty tracking. + // then + Assertions.assertThat(byFromAndTo).hasSize(3); + Assertions.assertThat(byFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // given - commandTarget1User1.setReplayState(ReplayState.PENDING); + // when + List byTarget1AndFromAndTo = commandLogEntryRepository.findByTargetAndFromAndTo(target1, from, to); - // when - List notYetReplayed2 = commandLogEntryRepository.findNotYetReplayed(); + // then + Assertions.assertThat(byTarget1AndFromAndTo).hasSize(2); + Assertions.assertThat(byTarget1AndFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent - // then - Assertions.assertThat(notYetReplayed2).hasSize(1); - Assertions.assertThat(notYetReplayed2.get(0).getInteractionId()).isEqualTo(commandTarget1User1.getInteractionId()); - } + // when + List recentByTargetOfCommand1 = commandLogEntryRepository.findRecentByTarget(target1); - // when - List byFromAndTo = commandLogEntryRepository.findByFromAndTo(from, to); + // then + Assertions.assertThat(recentByTargetOfCommand1).hasSize(3); + Assertions.assertThat(recentByTargetOfCommand1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent - // then - Assertions.assertThat(byFromAndTo).hasSize(3); - Assertions.assertThat(byFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // when + List recentByUsername = commandLogEntryRepository.findRecentByUsername(username1); - // when - List byTarget1AndFromAndTo = commandLogEntryRepository.findByTargetAndFromAndTo(target1, from, to); + // then + Assertions.assertThat(recentByUsername).hasSize(3); + Assertions.assertThat(recentByUsername.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // then - Assertions.assertThat(byTarget1AndFromAndTo).hasSize(2); - Assertions.assertThat(byTarget1AndFromAndTo.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent + // when + List byParent = commandLogEntryRepository.findByParent(commandTarget1User1); - // when - List recentByTargetOfCommand1 = commandLogEntryRepository.findRecentByTarget(target1); + // then // TODO: would need nested executions for this to show up. + Assertions.assertThat(byParent).isEmpty(); - // then - Assertions.assertThat(recentByTargetOfCommand1).hasSize(3); - Assertions.assertThat(recentByTargetOfCommand1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // the more recent + // when + List completed = commandLogEntryRepository.findCompleted(); - // when - List recentByUsername = commandLogEntryRepository.findRecentByUsername(username1); + // then + Assertions.assertThat(completed).hasSize(4); + Assertions.assertThat(completed.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent - // then - Assertions.assertThat(recentByUsername).hasSize(3); - Assertions.assertThat(recentByUsername.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // when + List current = commandLogEntryRepository.findCurrent(); - // when - List byParent = commandLogEntryRepository.findByParent(commandTarget1User1); + // then // TODO: would need more sophistication in fixtures to test + Assertions.assertThat(current).isEmpty(); - // then // TODO: would need nested executions for this to show up. - Assertions.assertThat(byParent).isEmpty(); + // when + List since = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 3); - // when - List completed = commandLogEntryRepository.findCompleted(); + // then + Assertions.assertThat(since).hasSize(2); + Assertions.assertThat(since.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest first - // then - Assertions.assertThat(completed).hasSize(4); - Assertions.assertThat(completed.get(0).getInteractionId()).isEqualTo(commandTarget2User1.getInteractionId()); // the more recent + // when + List sinceWithBatchSize1 = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 1); - // when - List current = commandLogEntryRepository.findCurrent(); + // then + Assertions.assertThat(sinceWithBatchSize1).hasSize(1); + Assertions.assertThat(sinceWithBatchSize1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest fist - // then // TODO: would need more sophistication in fixtures to test - Assertions.assertThat(current).isEmpty(); + // when + Optional mostRecentReplayedIfAny = commandLogEntryRepository.findMostRecentReplayed(); - // when - List since = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 3); + // then + Assertions.assertThat(mostRecentReplayedIfAny).isEmpty(); - // then - Assertions.assertThat(since).hasSize(2); - Assertions.assertThat(since.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest first + if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { - // when - List sinceWithBatchSize1 = commandLogEntryRepository.findSince(commandTarget1User1.getInteractionId(), 1); + // fails in JPA; possibly need to get the agent working for dirty tracking. - // then - Assertions.assertThat(sinceWithBatchSize1).hasSize(1); - Assertions.assertThat(sinceWithBatchSize1.get(0).getInteractionId()).isEqualTo(commandTarget1User2.getInteractionId()); // oldest fist + // given + commandTarget1User1.setReplayState(ReplayState.OK); // when - Optional mostRecentReplayedIfAny = commandLogEntryRepository.findMostRecentReplayed(); + Optional mostRecentReplayedIfAny2 = commandLogEntryRepository.findMostRecentReplayed(); // then - Assertions.assertThat(mostRecentReplayedIfAny).isEmpty(); - - if (causewayBeanTypeRegistry.persistenceStack().isJdo()) { - - // fails in JPA; possibly need to get the agent working for dirty tracking. - - // given - commandTarget1User1.setReplayState(ReplayState.OK); - - // when - Optional mostRecentReplayedIfAny2 = commandLogEntryRepository.findMostRecentReplayed(); - - // then - Assertions.assertThat(mostRecentReplayedIfAny2).isPresent(); - Assertions.assertThat(mostRecentReplayedIfAny2.get().getInteractionId()).isEqualTo(commandTarget1User1Id); - } - }); + Assertions.assertThat(mostRecentReplayedIfAny2).isPresent(); + Assertions.assertThat(mostRecentReplayedIfAny2.get().getInteractionId()).isEqualTo(commandTarget1User1Id); + } } + @Inject CommandLogEntryRepository commandLogEntryRepository; + @Inject SudoService sudoService; + @Inject ClockService clockService; + @Inject InteractionService interactionService; + @Inject InteractionLayerTracker interactionLayerTracker; + @Inject CounterRepository counterRepository; + @Inject WrapperFactory wrapperFactory; + @Inject BookmarkService bookmarkService; + @Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry; + } diff --git a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java index 77e4b7c623a..f235a176dfc 100644 --- a/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java +++ b/extensions/core/executionlog/applib/src/test/java/org/apache/causeway/extensions/executionlog/applib/integtest/ExecutionLog_IntegTestAbstract.java @@ -39,7 +39,6 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -57,15 +56,6 @@ public abstract class ExecutionLog_IntegTestAbstract extends CausewayIntegrationTestAbstract { - @Inject ExecutionLogEntryRepository executionLogEntryRepository; - @Inject SudoService sudoService; - @Inject ClockService clockService; - @Inject InteractionService interactionService; - @Inject InteractionLayerTracker interactionLayerTracker; - @Inject CounterRepository counterRepository; - @Inject WrapperFactory wrapperFactory; - @Inject BookmarkService bookmarkService; - @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -73,11 +63,10 @@ static void beforeAll() { Counter counter1; Counter counter2; - private TestSupport testSupport; @BeforeEach void beforeEach() { - this.testSupport = interactionService.testSupport(); + counterRepository.removeAll(); executionLogEntryRepository.removeAll(); @@ -234,29 +223,21 @@ void roundtrip_ELE_bookmarks() { Integer.parseInt(identifier.substring(identifier.indexOf("_")+1)); // should not fail, ie check the format is as we expect // when we start a new session and lookup from the bookmark - testSupport.nextInteraction(model->{ + interactionService.nextInteraction(); - Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); - assertThat(cle2IfAny).isPresent(); + Optional cle2IfAny = bookmarkService.lookup(eleBookmarkIfAny.get()); + assertThat(cle2IfAny).isPresent(); - ExecutionLogEntry ele2 = (ExecutionLogEntry) cle2IfAny.get(); - InteractionDto interactionDto2 = ele2.getInteractionDto(); + ExecutionLogEntry ele2 = (ExecutionLogEntry) cle2IfAny.get(); + InteractionDto interactionDto2 = ele2.getInteractionDto(); - assertThat(interactionDto2).isEqualTo(interactionDto); - }); + assertThat(interactionDto2).isEqualTo(interactionDto); } @Test void test_all_the_repository_methods() { - class Model { - UUID executionTarget1User1Id; - UUID executionTarget1User2Id; - UUID executionTarget1User1YesterdayId; - } - var testSupport = interactionService.testSupport(new Model()); - // given sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); @@ -267,10 +248,8 @@ class Model { // then assertThat(executionsForTarget1User1).hasSize(1); - { - var executionTarget1User1 = executionsForTarget1User1.get(0); - testSupport.model().executionTarget1User1Id = executionTarget1User1.getInteractionId(); - } + var executionTarget1User1 = executionsForTarget1User1.get(0); + var executionTarget1User1Id = executionTarget1User1.getInteractionId(); // given (different user, same target, same day) counter1 = counterRepository.findByName("counter-1"); @@ -279,134 +258,136 @@ class Model { UserMemento.builder("user-2").build()), () -> wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act() ); + interactionService.nextInteraction(); - testSupport.nextInteraction(model->{ - - // when - List executionsForTarget1User2 = executionLogEntryRepository.findMostRecent(1); - - // then - assertThat(executionsForTarget1User2).hasSize(1); - var executionTarget1User2 = executionsForTarget1User2.get(0); - model.executionTarget1User2Id = executionTarget1User2.getInteractionId(); - - // given (same user, same target, yesterday) - counter1 = counterRepository.findByName("counter-1"); - - sudoService.run( - InteractionContext.switchUser( - UserMemento.builder("user-1").build()), - () -> { - var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); - sudoService.run( - InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), - () -> { - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); - // when, then - model.executionTarget1User1YesterdayId = interactionLayerTracker.currentInteraction().get().getInteractionId(); - interactionService.closeInteractionLayers(); // to flush within changed time... - } - ); - }); - }); + // when + List executionsForTarget1User2 = executionLogEntryRepository.findMostRecent(1); + // then + assertThat(executionsForTarget1User2).hasSize(1); + var executionTarget1User2 = executionsForTarget1User2.get(0); + var executionTarget1User2Id = executionTarget1User2.getInteractionId(); + + // given (same user, same target, yesterday) + counter1 = counterRepository.findByName("counter-1"); + final UUID[] executionTarget1User1YesterdayIdHolder = new UUID[1]; + sudoService.run( + InteractionContext.switchUser( + UserMemento.builder("user-1").build()), + () -> { + var yesterday = clockService.getClock().nowAsLocalDateTime().minusDays(1); + sudoService.run( + InteractionContext.switchClock(VirtualClock.nowAt(yesterday)), + () -> { + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + executionTarget1User1YesterdayIdHolder[0] = interactionLayerTracker.currentInteraction().get().getInteractionId(); + interactionService.closeInteractionLayers(); // to flush within changed time... + } + ); + }); interactionService.openInteraction(); + // when, then + final UUID executionTarget1User1YesterdayId = executionTarget1User1YesterdayIdHolder[0]; + // given (same user, different target, same day) counter2 = counterRepository.findByName("counter-2"); sudoService.run(InteractionContext.switchUser(UserMemento.builder("user-1").build()), () -> { wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); }); + interactionService.nextInteraction(); - testSupport.nextInteraction(model->{ - - var executionTarget1User1Id = model.executionTarget1User1Id; - var executionTarget1User2Id = model.executionTarget1User2Id; - var executionTarget1User1YesterdayId = model.executionTarget1User1YesterdayId; - - // when - List executionTarget2User1IfAny = executionLogEntryRepository.findMostRecent(1); + // when + List executionTarget2User1IfAny = executionLogEntryRepository.findMostRecent(1); - // then - assertThat(executionTarget2User1IfAny).hasSize(1); - var executionTarget2User1 = executionTarget2User1IfAny.get(0); - var executionTarget2User1Id = executionTarget2User1.getInteractionId(); + // then + assertThat(executionTarget2User1IfAny).hasSize(1); + var executionTarget2User1 = executionTarget2User1IfAny.get(0); + var executionTarget2User1Id = executionTarget2User1.getInteractionId(); - // when - Optional executionTarget1User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1Id, 0); - Optional executionTarget1User2ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User2Id, 0); - Optional executionTarget1User1YesterdayById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1YesterdayId, 0); - Optional executionTarget2User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget2User1Id, 0); + // when + Optional executionTarget1User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1Id, 0); + Optional executionTarget1User2ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User2Id, 0); + Optional executionTarget1User1YesterdayById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget1User1YesterdayId, 0); + Optional executionTarget2User1ById = executionLogEntryRepository.findByInteractionIdAndSequence(executionTarget2User1Id, 0); - // then - assertThat(executionTarget1User1ById).isPresent(); - assertThat(executionTarget1User2ById).isPresent(); - assertThat(executionTarget1User1YesterdayById).isPresent(); - assertThat(executionTarget2User1ById).isPresent(); - assertThat(executionTarget2User1ById.get()).isSameAs(executionTarget2User1); + // then + assertThat(executionTarget1User1ById).isPresent(); + assertThat(executionTarget1User2ById).isPresent(); + assertThat(executionTarget1User1YesterdayById).isPresent(); + assertThat(executionTarget2User1ById).isPresent(); + assertThat(executionTarget2User1ById.get()).isSameAs(executionTarget2User1); - // given - counter1 = counterRepository.findByName("counter-1"); - var executionTarget1User1 = executionTarget1User1ById.get(); - var executionTarget1User2 = executionTarget1User2ById.get(); - var executionTarget1User1Yesterday = executionTarget1User1YesterdayById.get(); - executionTarget2User1 = executionTarget2User1ById.get(); + // given + counter1 = counterRepository.findByName("counter-1"); + executionTarget1User1 = executionTarget1User1ById.get(); + executionTarget1User2 = executionTarget1User2ById.get(); + var executionTarget1User1Yesterday = executionTarget1User1YesterdayById.get(); + executionTarget2User1 = executionTarget2User1ById.get(); - var target1 = executionTarget1User1.getTarget(); - var username1 = executionTarget1User1.getUsername(); - Timestamp from1 = executionTarget1User1.getStartedAt(); - Timestamp to1 = Timestamp.valueOf(from1.toLocalDateTime().plusDays(1)); - var bookmark1 = bookmarkService.bookmarkForElseFail(counter1); + var target1 = executionTarget1User1.getTarget(); + var username1 = executionTarget1User1.getUsername(); + Timestamp from1 = executionTarget1User1.getStartedAt(); + Timestamp to1 = Timestamp.valueOf(from1.toLocalDateTime().plusDays(1)); + var bookmark1 = bookmarkService.bookmarkForElseFail(counter1); - // when - List recentByTarget = executionLogEntryRepository.findRecentByTarget(bookmark1); + // when + List recentByTarget = executionLogEntryRepository.findRecentByTarget(bookmark1); - // then - assertThat(recentByTarget).hasSize(3); + // then + assertThat(recentByTarget).hasSize(3); - // when - List byTargetAndTimestampBefore = executionLogEntryRepository.findByTargetAndTimestampBefore(bookmark1, from1); + // when + List byTargetAndTimestampBefore = executionLogEntryRepository.findByTargetAndTimestampBefore(bookmark1, from1); - // then - assertThat(byTargetAndTimestampBefore).hasSize(2); // yesterday, plus cmd1 + // then + assertThat(byTargetAndTimestampBefore).hasSize(2); // yesterday, plus cmd1 - // when - List byTargetAndTimestampAfter = executionLogEntryRepository.findByTargetAndTimestampAfter(bookmark1, from1); + // when + List byTargetAndTimestampAfter = executionLogEntryRepository.findByTargetAndTimestampAfter(bookmark1, from1); - // then - assertThat(byTargetAndTimestampAfter).hasSize(2); // cmd1, 2nd + // then + assertThat(byTargetAndTimestampAfter).hasSize(2); // cmd1, 2nd - // when - List byTargetAndTimestampBetween = executionLogEntryRepository.findByTargetAndTimestampBetween(bookmark1, from1, to1); + // when + List byTargetAndTimestampBetween = executionLogEntryRepository.findByTargetAndTimestampBetween(bookmark1, from1, to1); - // then - assertThat(byTargetAndTimestampBetween).hasSize(2); // 1st and 2nd for this target + // then + assertThat(byTargetAndTimestampBetween).hasSize(2); // 1st and 2nd for this target - // when - List byTimestampBefore = executionLogEntryRepository.findByTimestampBefore(from1); + // when + List byTimestampBefore = executionLogEntryRepository.findByTimestampBefore(from1); - // then - assertThat(byTimestampBefore).hasSize(2); // cmd1 plus yesterday + // then + assertThat(byTimestampBefore).hasSize(2); // cmd1 plus yesterday - // when - List byTimestampAfter = executionLogEntryRepository.findByTimestampAfter(from1); + // when + List byTimestampAfter = executionLogEntryRepository.findByTimestampAfter(from1); - // then - assertThat(byTimestampAfter).hasSize(3); // cmd1, 2nd, and for other target + // then + assertThat(byTimestampAfter).hasSize(3); // cmd1, 2nd, and for other target - // when - List byTimestampBetween = executionLogEntryRepository.findByTimestampBetween(from1, to1); + // when + List byTimestampBetween = executionLogEntryRepository.findByTimestampBetween(from1, to1); - // then - assertThat(byTimestampBetween).hasSize(3); // 1st and 2nd for this target, and other target + // then + assertThat(byTimestampBetween).hasSize(3); // 1st and 2nd for this target, and other target - // when - List byUsername = executionLogEntryRepository.findRecentByUsername(username1); + // when + List byUsername = executionLogEntryRepository.findRecentByUsername(username1); - // then - assertThat(byUsername).hasSize(3); - }); + // then + assertThat(byUsername).hasSize(3); } + @Inject ExecutionLogEntryRepository executionLogEntryRepository; + @Inject SudoService sudoService; + @Inject ClockService clockService; + @Inject InteractionService interactionService; + @Inject InteractionLayerTracker interactionLayerTracker; + @Inject CounterRepository counterRepository; + @Inject WrapperFactory wrapperFactory; + @Inject BookmarkService bookmarkService; + } diff --git a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java index 9455cc3d7a9..c35fe6211d9 100644 --- a/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java +++ b/extensions/core/executionoutbox/applib/src/test/java/org/apache/causeway/extensions/executionoutbox/applib/integtest/ExecutionOutbox_IntegTestAbstract.java @@ -36,7 +36,6 @@ import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.sudo.SudoService; import org.apache.causeway.applib.services.user.UserMemento; import org.apache.causeway.applib.services.wrapper.WrapperFactory; @@ -61,11 +60,9 @@ static void beforeAll() { Counter counter1; Counter counter2; - private TestSupport testSupport; @BeforeEach void beforeEach() { - this.testSupport = interactionService.testSupport(); counterRepository.removeAll(); executionOutboxEntryRepository.removeAll(); diff --git a/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java b/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java index 54042cafebc..d182ff274b2 100644 --- a/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java +++ b/extensions/security/audittrail/applib/src/test/java/org/apache/causeway/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java @@ -31,7 +31,6 @@ import org.apache.causeway.applib.mixins.system.DomainChangeRecord; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.core.config.presets.CausewayPresets; import org.apache.causeway.extensions.audittrail.applib.dom.AuditTrailEntry; @@ -43,8 +42,6 @@ public abstract class AuditTrail_IntegTestAbstract extends CausewayIntegrationTestAbstract { - private TestSupport testSupport; - @BeforeAll static void beforeAll() { CausewayPresets.forcePrototyping(); @@ -52,15 +49,14 @@ static void beforeAll() { @BeforeEach void setUp() { - this.testSupport = interactionService.testSupport(); counterRepository.removeAll(); - testSupport.nextInteraction(model->{ - auditTrailEntryRepository.removeAll(); - }); - testSupport.nextInteraction(model->{ - assertThat(counterRepository.find()).isEmpty(); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); - }); + interactionService.nextInteraction(); + + auditTrailEntryRepository.removeAll(); + interactionService.nextInteraction(); + + assertThat(counterRepository.find()).isEmpty(); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); } protected abstract Counter newCounter(String name); @@ -71,33 +67,32 @@ void created() { // when var counter1 = counterRepository.persist(newCounter("counter-1")); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - testSupport.nextInteraction(model->{ - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).contains("name", "num", "num2"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("counter-1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); - assertThat(entriesById.get("num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); - }); + interactionService.nextInteraction(); + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).contains("name", "num", "num2"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("counter-1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); + assertThat(entriesById.get("num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull()); } @Test @@ -106,62 +101,55 @@ void updated_using_mixin() { // given var counter1 = counterRepository.persist(newCounter("counter-1")); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - testSupport.nextInteraction(ia->{ - auditTrailEntryRepository.removeAll(); - }); - - testSupport.nextInteraction(ia->{ - assertThat(counterRepository.find()).hasSize(1); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); - - // when - var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); - }); - - testSupport.nextInteraction(ia->{ - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - - // given - auditTrailEntryRepository.removeAll(); - - }); - - testSupport.nextInteraction(ia->{ - - // when bump again - var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter2).act(); - }); - - testSupport.nextInteraction(ia->{ - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("2")); - }); + interactionService.nextInteraction(); + + auditTrailEntryRepository.removeAll(); + interactionService.nextInteraction(); + + assertThat(counterRepository.find()).hasSize(1); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); + + // when + counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + interactionService.nextInteraction(); + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + + // given + auditTrailEntryRepository.removeAll(); + interactionService.nextInteraction(); + + // when bump again + counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act(); + interactionService.nextInteraction(); + + // then + entries = auditTrailEntryRepository.findAll(); + propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("2")); + } @Test @@ -172,44 +160,40 @@ void deleted() { counter1.setNum(1L); counter1.setNum2(2L); var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); + interactionService.nextInteraction(); + + auditTrailEntryRepository.removeAll(); + interactionService.nextInteraction(); - testSupport.nextInteraction(ia->{ - auditTrailEntryRepository.removeAll(); - }); - - testSupport.nextInteraction(ia->{ - // when - var counter2 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - counterRepository.remove(counter2); - }); - - testSupport.nextInteraction(ia->{ - - // then - var entries = auditTrailEntryRepository.findAll(); - var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).contains("name", "num", "num2"); - - var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("counter-1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); - assertThat(entriesById.get("num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("2")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); - }); + // when + counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); + counterRepository.remove(counter1); + interactionService.nextInteraction(); + + // then + var entries = auditTrailEntryRepository.findAll(); + var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).contains("name", "num", "num2"); + + var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("counter-1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); + assertThat(entriesById.get("num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("2")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]")); } diff --git a/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java b/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java index 14ca623834e..3b24b9bc08f 100644 --- a/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java +++ b/regressiontests/cmdexecauditsess/generic/src/main/java/org/apache/causeway/regressiontests/cmdexecauditsess/generic/integtest/CmdExecAuditSessLog_IntegTestAbstract.java @@ -33,7 +33,6 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.wrapper.WrapperFactory; import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; import org.apache.causeway.core.config.presets.CausewayPresets; @@ -60,35 +59,32 @@ static void beforeAll() { } Bookmark target1; - private TestSupport testSupport; @BeforeEach void beforeEach() { - this.testSupport = interactionService.testSupport(); - testSupport.nextInteraction(ia->{ - counterRepository.removeAll(); - - assertThat(counterRepository.find()).isEmpty(); - - var counter1 = counterRepository.persist(newCounter("counter-1")); - target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); - - assertThat(counterRepository.find()).hasSize(1); - }); - - testSupport.nextInteraction(ia->{ - commandLogEntryRepository.removeAll(); - executionLogEntryRepository.removeAll(); - executionOutboxEntryRepository.removeAll(); - auditTrailEntryRepository.removeAll(); - }); - - testSupport.nextInteraction(ia->{ - assertThat(commandLogEntryRepository.findAll()).isEmpty(); - assertThat(executionLogEntryRepository.findAll()).isEmpty(); - assertThat(executionOutboxEntryRepository.findAll()).isEmpty(); - assertThat(auditTrailEntryRepository.findAll()).isEmpty(); - }); + interactionService.nextInteraction(); + + counterRepository.removeAll(); + + assertThat(counterRepository.find()).isEmpty(); + + var counter1 = counterRepository.persist(newCounter("counter-1")); + target1 = bookmarkService.bookmarkFor(counter1).orElseThrow(); + + assertThat(counterRepository.find()).hasSize(1); + + interactionService.nextInteraction(); + commandLogEntryRepository.removeAll(); + executionLogEntryRepository.removeAll(); + executionOutboxEntryRepository.removeAll(); + auditTrailEntryRepository.removeAll(); + + interactionService.nextInteraction(); + + assertThat(commandLogEntryRepository.findAll()).isEmpty(); + assertThat(executionLogEntryRepository.findAll()).isEmpty(); + assertThat(executionOutboxEntryRepository.findAll()).isEmpty(); + assertThat(auditTrailEntryRepository.findAll()).isEmpty(); } protected abstract Counter newCounter(String name); @@ -97,7 +93,7 @@ void beforeEach() { protected void assertEntityPublishingDisabledFor(final Class entityClass) { var objectSpecification = specificationLoader.loadSpecification(entityClass); - EntityChangePublishingFacet facet = objectSpecification.lookupFacet(EntityChangePublishingFacet.class).orElse(null); + EntityChangePublishingFacet facet = objectSpecification.getFacet(EntityChangePublishingFacet.class); Assertions.assertThat(facet) .satisfies(f -> assertThat(f).isNotNull()) .satisfies(f -> assertThat(f.isEnabled()).isFalse()) @@ -111,8 +107,6 @@ void invoke_mixin() { var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); var interaction = interactionService.currentInteraction().orElseThrow(); - { - // when wrapperFactory.wrapMixinT(Counter_bumpUsingMixin.class, counter1).act(); @@ -196,46 +190,42 @@ void invoke_mixin() { // ... and audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); - } // when - testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries - - // then - // ... command entry now marked as complete - var commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNotNull()) - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - var auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); - - var propertyIds = auditTrailEntries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); - assertThat(propertyIds).containsExactly("num"); - - var entriesById = auditTrailEntries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); - assertThat(entriesById.get("num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("cmdexecauditsess.test.Counter#num")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isEqualTo(interaction.getInteractionId())) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) - .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); - } - - }); + interactionService.nextInteraction(); // flushes the command and audit trail entries + // then + // ... command entry now marked as complete + commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNotNull()) + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + + var propertyIds = auditTrailEntries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList()); + assertThat(propertyIds).containsExactly("num"); + + var entriesById = auditTrailEntries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x)); + assertThat(entriesById.get("num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("cmdexecauditsess.test.Counter#num")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1")) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isEqualTo(interaction.getInteractionId())) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull()) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY)) + .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system")); + } } @Test @@ -243,7 +233,6 @@ void invoke_direct() { // given var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - { // when wrapperFactory.wrap(counter1).bumpUsingDeclaredAction(); @@ -270,29 +259,27 @@ void invoke_direct() { // ... but audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); - } // when - testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries - - // then - // ... command entry now marked as complete - var commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNotNull()) - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - var auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); - } - }); + interactionService.nextInteraction(); // flushes the command and audit trail entries + + // then + // ... command entry now marked as complete + commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNotNull()) + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + } } @@ -301,7 +288,7 @@ void edit() { // given var counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow(); - { + // when wrapperFactory.wrap(counter1).setNum(99L); @@ -324,28 +311,27 @@ void edit() { // ... and audit entries not yet generated var auditTrailEntries = auditTrailEntryRepository.findAll(); assertThat(auditTrailEntries).isEmpty(); - } + // when - testSupport.nextInteraction(ia->{ // flushes the command and audit trail entries - - // then - // ... command entry now marked as complete - var commandLogEntries = commandLogEntryRepository.findAll(); - assertThat(commandLogEntries).hasSize(1); - var commandLogEntryAfter = commandLogEntries.get(0); - assertThat(commandLogEntryAfter) - .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) - .satisfies(e -> assertThat(e.getDuration()).isNotNull()) - .satisfies(e -> assertThat(e.getResult()).isNull()) // property edits are effectively void actions - .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK (VOID)")); - - if(!isJpa()) { - // and then - // ... audit trail entry created - var auditTrailEntries = auditTrailEntryRepository.findAll(); - assertThat(auditTrailEntries).hasSize(1); - } - }); + interactionService.nextInteraction(); // flushes the command and audit trail entries + + // then + // ... command entry now marked as complete + commandLogEntries = commandLogEntryRepository.findAll(); + assertThat(commandLogEntries).hasSize(1); + var commandLogEntryAfter = commandLogEntries.get(0); + assertThat(commandLogEntryAfter) + .satisfies(e -> assertThat(e.getCompletedAt()).isNotNull()) + .satisfies(e -> assertThat(e.getDuration()).isNotNull()) + .satisfies(e -> assertThat(e.getResult()).isNull()) // property edits are effectively void actions + .satisfies(e -> assertThat(e.getResultSummary()).isEqualTo("OK (VOID)")); + + if(!isJpa()) { + // and then + // ... audit trail entry created + auditTrailEntries = auditTrailEntryRepository.findAll(); + assertThat(auditTrailEntries).hasSize(1); + } } diff --git a/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java b/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java index eb26dc54a5b..17f1fdbac40 100644 --- a/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java +++ b/regressiontests/layouts/src/test/java/org/apache/causeway/regressiontests/layouts/integtest/Layout_Counter_IntegTest.java @@ -52,7 +52,6 @@ import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.iactnlayer.InteractionService; -import org.apache.causeway.applib.services.iactnlayer.InteractionService.TestSupport; import org.apache.causeway.applib.services.metamodel.MetaModelService; import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; import org.apache.causeway.core.config.presets.CausewayPresets; @@ -124,15 +123,16 @@ static void beforeAll() { } Bookmark target1; - private TestSupport testSupport; @BeforeEach void beforeEach() { - this.testSupport = interactionService.testSupport(); - testSupport.nextInteraction(model->{ - Optional bookmark = bookmarkService.bookmarkFor(newCounter("counter-1")); - target1 = bookmark.orElseThrow(); - }); + interactionService.nextInteraction(); + + Optional bookmark = bookmarkService.bookmarkFor(newCounter("counter-1")); + target1 = bookmark.orElseThrow(); + + interactionService.nextInteraction(); + } protected Counter newCounter(final String name) { From 1479476220934109745fe0ccb8da94b4a25bba0b Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 22 Mar 2026 10:36:56 +0100 Subject: [PATCH 13/31] CAUSEWAY-3975: adds default OpenTelemetryServerRequestObservationConvention --- commons/src/main/java/module-info.java | 1 + .../CausewayObservationInternal.java | 11 +++++-- .../session/InteractionServiceDefault.java | 9 ++++-- .../core/webapp/CausewayModuleCoreWebapp.java | 30 +++++++++++++++++-- .../CausewayModuleViewerWicketViewer.java | 4 +-- .../integration/TelemetryStartHandler.java | 8 ++++- .../wicketapp/CausewayWicketApplication.java | 4 +-- 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java index a5d7963e405..47a24ab13b0 100644 --- a/commons/src/main/java/module-info.java +++ b/commons/src/main/java/module-info.java @@ -68,6 +68,7 @@ requires transitive tools.jackson.core; requires transitive tools.jackson.databind; requires transitive tools.jackson.module.jakarta.xmlbind; + requires transitive micrometer.commons; requires transitive micrometer.observation; requires transitive org.jdom2; requires transitive org.jspecify; diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java index 2d7ea230a6b..7248a116165 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java @@ -28,6 +28,7 @@ import lombok.Data; import lombok.experimental.Accessors; +import io.micrometer.common.KeyValue; import io.micrometer.observation.Observation; import io.micrometer.observation.Observation.Scope; import io.micrometer.observation.ObservationRegistry; @@ -68,7 +69,7 @@ public boolean isNoop() { public Observation createNotStarted(final Class bean, final String name) { return Observation.createNotStarted(name, observationRegistry) .lowCardinalityKeyValue("module", module) - .highCardinalityKeyValue("bean", bean.getSimpleName()); + .lowCardinalityKeyValue("bean", bean.getSimpleName()); } @FunctionalInterface @@ -84,7 +85,7 @@ public ObservationProvider provider(final Class bean) { * Helps if start and stop of an {@link Observation} happen in different code locations. */ @Data @Accessors(fluent = true) - public static class ObservationClosure implements AutoCloseable { + public static final class ObservationClosure implements AutoCloseable { private Observation observation; private Scope scope; @@ -123,4 +124,10 @@ public ObservationClosure tag(final String key, @Nullable final Supplier } + public static KeyValue currentThreadId() { + var ct = Thread.currentThread(); + return KeyValue.of("threadId", "%d [%s]".formatted(ct.getId(), ct.getName())); + + } + } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 841f5f582db..ca90c91139a 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -147,11 +147,16 @@ public InteractionLayer openInteraction(final @NonNull InteractionContext intera .map(it->(CausewayInteraction)it) .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId())); - var obs = observationProvider.get("Causeway Layered Interaction") - .highCardinalityKeyValue("stackSize", ""+getInteractionLayerCount()); + var obs = observationProvider.get(getInteractionLayerCount()==0 + ? "Causeway Root Interaction" + : "Causeway Nested Interaction"); var newInteractionLayer = layerStack.push(causewayInteraction, interactionContextToUse, obs); + if(getInteractionLayerCount()>0) { + obs.highCardinalityKeyValue("stackedLayers", ""+getInteractionLayerCount()); + } + if(isAtTopLevel()) { transactionServiceSpring.onOpen(causewayInteraction); interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); diff --git a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java index fa99d8e5de6..6a16a0b7739 100644 --- a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java +++ b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java @@ -23,9 +23,12 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationContext; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextListener; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.core.interaction.session.MessageBrokerImpl; import org.apache.causeway.core.metamodel.services.message.MessageBroker; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; @@ -36,7 +39,9 @@ import org.apache.causeway.core.webapp.modules.templresources.WebModuleTemplateResources; import org.apache.causeway.core.webapp.webappctx.CausewayWebAppContextInitializer; -@Configuration +import io.micrometer.common.KeyValues; + +@Configuration(proxyBeanMethods = false) @Import({ // Modules CausewayModuleCoreRuntime.class, @@ -61,7 +66,7 @@ public class CausewayModuleCoreWebapp { @Scope( value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) - public MessageBroker sessionScopedMessageBroker() { + MessageBroker sessionScopedMessageBroker() { return new MessageBrokerImpl(); } @@ -73,8 +78,27 @@ public MessageBroker sessionScopedMessageBroker() { * @see https://stackoverflow.com/a/61431621/56880 */ @Bean - public RequestContextListener requestContextListener() { + RequestContextListener requestContextListener() { return new RequestContextListener(); } + /** + * https://docs.spring.io/spring-boot/reference/actuator/observability.html + */ + @Bean + OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() { + return new OpenTelemetryServerRequestObservationConvention() { + @Override + public String getContextualName(final ServerRequestObservationContext context) { + return super.getContextualName(context) + " {TODO}"; + } + @Override + public KeyValues getHighCardinalityKeyValues(final ServerRequestObservationContext context) { + // Make sure that KeyValues entries are already sorted by name for better performance + return KeyValues.of(methodOriginal(context), httpUrl(context), + CausewayObservationInternal.currentThreadId()); + } + }; + } + } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/CausewayModuleViewerWicketViewer.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/CausewayModuleViewerWicketViewer.java index 8746f572e7c..b3482c5feb3 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/CausewayModuleViewerWicketViewer.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/CausewayModuleViewerWicketViewer.java @@ -43,7 +43,7 @@ /** * @since 1.x {@index} */ -@Configuration +@Configuration(proxyBeanMethods = false) @Import({ // Modules CausewayModuleViewerWicketUi.class, @@ -69,9 +69,9 @@ PageClassRegistryDefault.AutoConfiguration.class, PageNavigationServiceDefault.AutoConfiguration.class, HintStoreUsingWicketSession.AutoConfiguration.class, - }) public class CausewayModuleViewerWicketViewer { public static final String NAMESPACE = "causeway.viewer.wicket"; + } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index daeef157a43..41504896e40 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -22,6 +22,7 @@ import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; /** @@ -31,11 +32,16 @@ public record TelemetryStartHandler( ObservationProvider observationProvider) implements IRequestCycleListener { + public TelemetryStartHandler(final CausewayObservationInternal observationInternal) { + this(observationInternal.provider(TelemetryStartHandler.class)); + } + @Override public synchronized void onBeginRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { requestCycle2.observationClosure.startAndOpenScope( - observationProvider.get("Apache Wicket Request Cycle")); + observationProvider.get("Apache Wicket Request Cycle") + .lowCardinalityKeyValue("ck2", "test2")); } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 597e9548e82..36985ca8661 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -21,7 +21,6 @@ import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.function.Function; @@ -213,8 +212,7 @@ protected void init() { getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER); getResourceSettings().setParentFolderPlaceholder("$up$"); - getRequestCycleListeners().add(new TelemetryStartHandler(Objects.requireNonNull(observationInternal) - .provider(TelemetryStartHandler.class))); + getRequestCycleListeners().add(new TelemetryStartHandler(observationInternal)); getRequestCycleListeners().add(new WebRequestCycleForCauseway(metaModelContext, getPageClassRegistry())); getRequestCycleListeners().add(new TelemetryStopHandler(metricService)); getRequestCycleListeners().add(new RehydrationHandler()); From a6a24debcc5f09f270a1e65152ccbab56fce98f7 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 22 Mar 2026 11:16:22 +0100 Subject: [PATCH 14/31] CAUSEWAY-3975: adds request path to top level observation name --- .../causeway/core/webapp/CausewayModuleCoreWebapp.java | 5 ++++- .../wicket/viewer/integration/TelemetryStartHandler.java | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java index 6a16a0b7739..f9e61895a32 100644 --- a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java +++ b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java @@ -28,6 +28,7 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextListener; +import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; import org.apache.causeway.core.interaction.session.MessageBrokerImpl; import org.apache.causeway.core.metamodel.services.message.MessageBroker; @@ -90,7 +91,9 @@ OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObserv return new OpenTelemetryServerRequestObservationConvention() { @Override public String getContextualName(final ServerRequestObservationContext context) { - return super.getContextualName(context) + " {TODO}"; + return "%s (%s)".formatted( + super.getContextualName(context), + _Strings.ellipsifyAtEnd(context.getCarrier().getRequestURI(), 80, "...")); } @Override public KeyValues getHighCardinalityKeyValues(final ServerRequestObservationContext context) { diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index 41504896e40..1d5cda6ada8 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -40,8 +40,7 @@ public TelemetryStartHandler(final CausewayObservationInternal observationIntern public synchronized void onBeginRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { requestCycle2.observationClosure.startAndOpenScope( - observationProvider.get("Apache Wicket Request Cycle") - .lowCardinalityKeyValue("ck2", "test2")); + observationProvider.get("Apache Wicket Request Cycle")); } } From 125820db58f3776d6ab2b6bc1c9134062dc6afd9 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Mon, 23 Mar 2026 09:26:39 +0100 Subject: [PATCH 15/31] CAUSEWAY-3975: on interaction start provide user info --- .../causeway/commons/having/HasTypeSpecificAttributes.java | 5 +++++ .../core/interaction/session/CausewayInteraction.java | 4 ++-- .../runtimeservices/session/InteractionServiceDefault.java | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/having/HasTypeSpecificAttributes.java b/commons/src/main/java/org/apache/causeway/commons/having/HasTypeSpecificAttributes.java index 5e4216112d3..c4563b5445a 100644 --- a/commons/src/main/java/org/apache/causeway/commons/having/HasTypeSpecificAttributes.java +++ b/commons/src/main/java/org/apache/causeway/commons/having/HasTypeSpecificAttributes.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.commons.having; +import java.util.Optional; import java.util.function.Function; public interface HasTypeSpecificAttributes { @@ -31,6 +32,10 @@ public interface HasTypeSpecificAttributes { /** get type specific attribute */ T getAttribute(Class type); + default Optional lookupAttribute(final Class type) { + return Optional.ofNullable(getAttribute(type)); + } + /** remove type specific attribute */ void removeAttribute(Class type); diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java index 12b91d874ef..f1c3148f307 100644 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java @@ -37,6 +37,7 @@ import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.execution.InteractionInternal; + import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -212,9 +213,8 @@ private Execution popAndComplete( final ClockService clockService, final MetricsService metricsService) { - if(currentExecution == null) { + if(currentExecution == null) throw new IllegalStateException("No current execution to pop"); - } final Execution popped = currentExecution; var completedAt = clockService.getClock().nowAsJavaSqlTimestamp(); diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index ca90c91139a..dbd88e2f468 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -47,6 +47,7 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.inject.ServiceInjector; import org.apache.causeway.commons.functional.ThrowingRunnable; +import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; @@ -153,6 +154,11 @@ public InteractionLayer openInteraction(final @NonNull InteractionContext intera : "Causeway Nested Interaction"); var newInteractionLayer = layerStack.push(causewayInteraction, interactionContextToUse, obs); + obs.highCardinalityKeyValue("user.isImpersonating", "" + interactionContextToUse.getUser().isImpersonating()); + _Strings.nonEmpty(interactionContextToUse.getUser().multiTenancyToken()) + .ifPresent(value->obs.highCardinalityKeyValue("user.multiTenancyToken", value)); + _Strings.nonEmpty(interactionContextToUse.getUser().name()) + .ifPresent(value->obs.highCardinalityKeyValue("user.name", value)); if(getInteractionLayerCount()>0) { obs.highCardinalityKeyValue("stackedLayers", ""+getInteractionLayerCount()); } From 22c2176173774b60efb2399af1f3e08a569dc454 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 26 Mar 2026 06:29:25 +0100 Subject: [PATCH 16/31] CAUSEWAY-3975: renaming integration --- .../applib/services/iactnlayer/InteractionLayer.java | 2 +- .../services/iactnlayer/InteractionLayerStack.java | 2 +- ...ernal.java => CausewayObservationIntegration.java} | 11 ++++++++--- .../core/metamodel/CausewayModuleCoreMetamodel.java | 6 +++--- .../metamodel/services/init/MetamodelInitializer.java | 8 ++++---- .../core/runtime/CausewayModuleCoreRuntime.java | 2 +- .../CausewayModuleCoreRuntimeServices.java | 6 +++--- .../session/InteractionServiceDefault.java | 8 ++++---- .../core/webapp/CausewayModuleCoreWebapp.java | 4 ++-- .../wicket/model/CausewayModuleViewerWicketModel.java | 6 +++--- .../wicket/viewer/integration/RequestCycle2.java | 2 +- .../viewer/integration/TelemetryStartHandler.java | 8 ++++---- .../viewer/wicketapp/CausewayWicketApplication.java | 6 +++--- 13 files changed, 38 insertions(+), 33 deletions(-) rename commons/src/main/java/org/apache/causeway/commons/internal/observation/{CausewayObservationInternal.java => CausewayObservationIntegration.java} (93%) diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java index c4475490ab3..3f65a55eb20 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java @@ -21,7 +21,7 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; /** * Binds an {@link Interaction} ("what" is being executed) with diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java index 9a1b7b69fdb..96faddac1c8 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java @@ -24,7 +24,7 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; import io.micrometer.observation.Observation; diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java similarity index 93% rename from commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java rename to commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 7248a116165..210926d8a7d 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -45,17 +45,17 @@ * } * */ -public record CausewayObservationInternal( +public record CausewayObservationIntegration( ObservationRegistry observationRegistry, String module) { - public CausewayObservationInternal( + public CausewayObservationIntegration( final Optional observationRegistryOpt, final String module) { this(observationRegistryOpt.orElse(ObservationRegistry.NOOP), module); } - public CausewayObservationInternal { + public CausewayObservationIntegration { observationRegistry = observationRegistry!=null ? observationRegistry : ObservationRegistry.NOOP; @@ -109,6 +109,11 @@ public void close() { public void onError(final Exception ex) { if(observation==null) return; + // scope lifecycle terminates before exception handling + if(scope!=null) { + this.scope.close(); + this.scope = null; + } observation.error(ex); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index 4b251c0e9cc..1ab28d0cddd 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -43,7 +43,7 @@ import org.apache.causeway.commons.functional.Either; import org.apache.causeway.commons.functional.Railway; import org.apache.causeway.commons.functional.Try; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.commons.semantics.CollectionSemantics; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.CausewayModuleCoreConfig; @@ -270,9 +270,9 @@ public ValueCodec valueCodec( } @Bean("causeway-metamodel") - public CausewayObservationInternal causewayObservationInternal( + public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationInternal(observationRegistryOpt, "causeway-metamodel"); + return new CausewayObservationIntegration(observationRegistryOpt, "causeway-metamodel"); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java index c9d817a96cf..3220c879aa2 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java @@ -36,8 +36,8 @@ import org.apache.causeway.applib.util.schema.InteractionsDtoUtils; import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import lombok.extern.slf4j.Slf4j; @@ -54,8 +54,8 @@ public MetamodelInitializer( final EventBusService eventBusService, final Provider specificationLoaderProvider, @Qualifier("causeway-metamodel") - final CausewayObservationInternal observation) { - this(eventBusService, specificationLoaderProvider, observation.provider(MetamodelInitializer.class)); + final CausewayObservationIntegration observationIntegration) { + this(eventBusService, specificationLoaderProvider, observationIntegration.provider(MetamodelInitializer.class)); } @EventListener diff --git a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java index 220ff51c8c3..61177be5fd1 100644 --- a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java +++ b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java @@ -33,7 +33,7 @@ CausewayModuleCoreInteraction.class, CausewayModuleCoreTransaction.class, - // @Service's + // @Service XrayInitializerService.class, }) public class CausewayModuleCoreRuntime { diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java index ae003667556..a468caf7e15 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java @@ -31,7 +31,7 @@ import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.bookmark.HmacAuthority; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault; @@ -157,9 +157,9 @@ public HmacAuthority fallbackHmacAuthority() { } @Bean("causeway-runtimeservices") - public CausewayObservationInternal causewayObservationInternal( + public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationInternal(observationRegistryOpt, "causeway-runtimeservices"); + return new CausewayObservationIntegration(observationRegistryOpt, "causeway-runtimeservices"); } } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index dbd88e2f468..9a838fde6fc 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -51,8 +51,8 @@ import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor; import org.apache.causeway.core.interaction.scope.InteractionScopeLifecycleHandler; import org.apache.causeway.core.interaction.session.CausewayInteraction; @@ -101,7 +101,7 @@ public class InteractionServiceDefault public InteractionServiceDefault( final EventBusService eventBusService, @Qualifier("causeway-runtimeservices") - final CausewayObservationInternal observation, + final CausewayObservationIntegration observationIntegration, final Provider specificationLoaderProvider, final ServiceInjector serviceInjector, final TransactionServiceSpring transactionServiceSpring, @@ -109,7 +109,7 @@ public InteractionServiceDefault( final Provider commandPublisherProvider, final ConfigurableBeanFactory beanFactory, final InteractionIdGenerator interactionIdGenerator) { - this.observationProvider = observation.provider(getClass()); + this.observationProvider = observationIntegration.provider(getClass()); this.serviceInjector = serviceInjector; this.transactionServiceSpring = transactionServiceSpring; this.clockService = clockService; diff --git a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java index f9e61895a32..2ab1bee6e54 100644 --- a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java +++ b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java @@ -29,7 +29,7 @@ import org.springframework.web.context.request.RequestContextListener; import org.apache.causeway.commons.internal.base._Strings; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.interaction.session.MessageBrokerImpl; import org.apache.causeway.core.metamodel.services.message.MessageBroker; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; @@ -99,7 +99,7 @@ public String getContextualName(final ServerRequestObservationContext context) { public KeyValues getHighCardinalityKeyValues(final ServerRequestObservationContext context) { // Make sure that KeyValues entries are already sorted by name for better performance return KeyValues.of(methodOriginal(context), httpUrl(context), - CausewayObservationInternal.currentThreadId()); + CausewayObservationIntegration.currentThreadId()); } }; } diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java index 7e3bca0fbd0..f9b1bfd97fc 100644 --- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java +++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java @@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.webapp.CausewayModuleCoreWebapp; import io.micrometer.observation.ObservationRegistry; @@ -40,8 +40,8 @@ public class CausewayModuleViewerWicketModel { @Bean("causeway-wicketviewer") - public CausewayObservationInternal causewayObservationInternal( + public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationInternal(observationRegistryOpt, "causeway-wicketviewer"); + return new CausewayObservationIntegration(observationRegistryOpt, "causeway-wicketviewer"); } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java index 7bda8be00ba..1fdc60754d7 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java @@ -21,7 +21,7 @@ import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.cycle.RequestCycleContext; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; public class RequestCycle2 extends RequestCycle { diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index 1d5cda6ada8..10b4274ab4a 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -22,8 +22,8 @@ import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; /** * @since 4.0 @@ -32,8 +32,8 @@ public record TelemetryStartHandler( ObservationProvider observationProvider) implements IRequestCycleListener { - public TelemetryStartHandler(final CausewayObservationInternal observationInternal) { - this(observationInternal.provider(TelemetryStartHandler.class)); + public TelemetryStartHandler(final CausewayObservationIntegration observationIntegration) { + this(observationIntegration.provider(TelemetryStartHandler.class)); } @Override diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 36985ca8661..0c56901a428 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -53,7 +53,7 @@ import org.apache.causeway.applib.services.metrics.MetricsService; import org.apache.causeway.commons.internal.concurrent._ConcurrentContext; import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; -import org.apache.causeway.commons.internal.observation.CausewayObservationInternal; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.environment.CausewaySystemEnvironment; import org.apache.causeway.core.metamodel.context.MetaModelContext; @@ -120,7 +120,7 @@ public static CausewayWicketApplication get() { @Inject private MetricsService metricService; @Qualifier("causeway-wicketviewer") - @Inject private CausewayObservationInternal observationInternal; + @Inject private CausewayObservationIntegration observationIntegration; @Getter(onMethod = @__(@Override)) @Inject private ComponentFactoryRegistry componentFactoryRegistry; @@ -212,7 +212,7 @@ protected void init() { getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER); getResourceSettings().setParentFolderPlaceholder("$up$"); - getRequestCycleListeners().add(new TelemetryStartHandler(observationInternal)); + getRequestCycleListeners().add(new TelemetryStartHandler(observationIntegration)); getRequestCycleListeners().add(new WebRequestCycleForCauseway(metaModelContext, getPageClassRegistry())); getRequestCycleListeners().add(new TelemetryStopHandler(metricService)); getRequestCycleListeners().add(new RehydrationHandler()); From 0dfb7f43e6bd1090c5ea93d825375cb7fab93548 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 26 Mar 2026 12:48:21 +0100 Subject: [PATCH 17/31] CAUSEWAY-3975: adds transaction observation --- .../CausewayObservationIntegration.java | 4 +- .../TransactionServiceDevNotes.adoc | 37 +++++ .../transaction/TransactionServiceSpring.java | 105 ++++++------ ...NoopTransactionSynchronizationService.java | 2 +- .../scope/StackedTransactionScope.java | 155 +++++++++--------- ...nsactionScopeBeanFactoryPostProcessor.java | 3 +- 6 files changed, 173 insertions(+), 133 deletions(-) create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 210926d8a7d..110f9a7b672 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -25,7 +25,7 @@ import org.springframework.util.StringUtils; -import lombok.Data; +import lombok.Getter; import lombok.experimental.Accessors; import io.micrometer.common.KeyValue; @@ -84,7 +84,7 @@ public ObservationProvider provider(final Class bean) { /** * Helps if start and stop of an {@link Observation} happen in different code locations. */ - @Data @Accessors(fluent = true) + @Getter @Accessors(fluent = true) public static final class ObservationClosure implements AutoCloseable { private Observation observation; diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc new file mode 100644 index 00000000000..808b2d56bcd --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc @@ -0,0 +1,37 @@ += Transaction Service + +:Notice: 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. + +[plantuml,fig-transaction-flow,svg] +.Transactional Code Flow +---- +@startuml + +boundary RequestCycle +participant InteractionService +participant TransactionService +boundary JpaTransactionManager as "JpaTransactionManager\n(Spring)" + +RequestCycle -> InteractionService: open (root) Interaction Layer + +InteractionService -> TransactionService: onOpen - sets up the initial\n\ +transaction against (all available) \nPlatformTransactionManager(s);\n\ +also installs ObservationClosure + +TransactionService -> JpaTransactionManager: getTransaction(txDefn) +TransactionService <-- JpaTransactionManager: new or existing Transaction +InteractionService <-- TransactionService: Interaction opened + +RequestCycle -> InteractionService: closeInteractionLayers() + +InteractionService -> TransactionService: onClose +TransactionService -> JpaTransactionManager: commit(txStatus) or rollback(txStatus) +TransactionService <-- JpaTransactionManager: Transaction completed + +InteractionService <-- TransactionService : Observations closed\n\ +Interaction closed + +RequestCycle <-- InteractionService + +@enduml +---- diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java index 1c91bf6736b..cd19888eb3a 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.core.runtimeservices.transaction; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; @@ -53,15 +54,16 @@ import org.apache.causeway.commons.functional.ThrowingRunnable; import org.apache.causeway.commons.functional.Try; import org.apache.causeway.commons.internal.base._NullSafe; -import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.interaction.session.CausewayInteraction; import org.apache.causeway.core.runtime.flushmgmt.FlushMgmt; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.core.transaction.events.TransactionCompletionStatus; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -87,13 +89,16 @@ public class TransactionServiceSpring private final Provider interactionLayerTrackerProvider; private final Can persistenceExceptionTranslators; private final ConfigurableListableBeanFactory configurableListableBeanFactory; + private final ObservationProvider observationProvider; @Inject public TransactionServiceSpring( final List platformTransactionManagers, final List persistenceExceptionTranslators, final Provider interactionLayerTrackerProvider, - final ConfigurableListableBeanFactory configurableListableBeanFactory + final ConfigurableListableBeanFactory configurableListableBeanFactory, + @Qualifier("causeway-runtimeservices") + final CausewayObservationIntegration observationIntegration ) { this.platformTransactionManagers = Can.ofCollection(platformTransactionManagers); @@ -105,6 +110,8 @@ public TransactionServiceSpring( log.info("PersistenceExceptionTranslators: {}", persistenceExceptionTranslators); this.interactionLayerTrackerProvider = interactionLayerTrackerProvider; + + this.observationProvider = observationIntegration.provider(getClass()); } // -- API @@ -118,7 +125,7 @@ public Try callTransactional(final TransactionDefinition def, final Calla try { TransactionStatus txStatus = platformTransactionManager.getTransaction(def); - registerTransactionSynchronizations(txStatus); + registerTransactionSynchronizations(); result = Try.call(() -> { @@ -145,9 +152,8 @@ public Try callTransactional(final TransactionDefinition def, final Calla // return the original failure cause (originating from calling the callable) // (so we don't shadow the original failure) // return the failure we just caught - if (result != null && result.isFailure()) { + if (result != null && result.isFailure()) return result; - } // otherwise, we thought we had a success, but now we have an exception thrown by either , // the call to rollback or commit above. We don't need to do anything though; if either of @@ -159,7 +165,7 @@ public Try callTransactional(final TransactionDefinition def, final Calla return result; } - private void registerTransactionSynchronizations(final TransactionStatus txStatus) { + private void registerTransactionSynchronizations() { if (TransactionSynchronizationManager.isSynchronizationActive()) { configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class) .values() @@ -184,9 +190,8 @@ public void flushTransaction() { var translatedEx = translateExceptionIfPossible(ex, txManager); - if(translatedEx instanceof RuntimeException) { + if(translatedEx instanceof RuntimeException) throw ex; - } throw new RuntimeException(ex); @@ -209,11 +214,10 @@ public TransactionState currentTransactionState() { return currentTransactionStatus() .map(txStatus->{ - if(txStatus.isCompleted()) { + if(txStatus.isCompleted()) return txStatus.isRollbackOnly() ? TransactionState.ABORTED : TransactionState.COMMITTED; - } return txStatus.isRollbackOnly() ? TransactionState.MUST_ABORT @@ -235,9 +239,8 @@ public TransactionState currentTransactionState() { private PlatformTransactionManager transactionManagerForElseFail(final TransactionDefinition def) { if(def instanceof TransactionTemplate) { var txManager = ((TransactionTemplate)def).getTransactionManager(); - if(txManager!=null) { + if(txManager!=null) return txManager; - } } return platformTransactionManagers.getSingleton() .orElseThrow(()-> @@ -264,9 +267,8 @@ private Optional currentTransactionStatus() { txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); // not strictly required, but to prevent stack-trace creation later on - if(!TransactionSynchronizationManager.isActualTransactionActive()) { + if(!TransactionSynchronizationManager.isActualTransactionActive()) return Optional.empty(); - } // get current transaction else throw an exception return Try.call(()-> @@ -278,9 +280,8 @@ private Optional currentTransactionStatus() { private Throwable translateExceptionIfPossible(final Throwable ex, final PlatformTransactionManager txManager) { - if(ex instanceof DataAccessException) { + if(ex instanceof DataAccessException) return ex; // nothing to do, already translated - } if(ex instanceof RuntimeException) { @@ -291,9 +292,8 @@ private Throwable translateExceptionIfPossible(final Throwable ex, final Platfor .findFirst() .orElse(null); - if(translatedEx!=null) { + if(translatedEx!=null) return translatedEx; - } } @@ -309,36 +309,43 @@ private Throwable translateExceptionIfPossible(final Throwable ex, final Platfor public void onOpen(final @NonNull CausewayInteraction interaction) { txCounter.get().reset(); + if (platformTransactionManagers.isEmpty()) return; if (log.isDebugEnabled()) { log.debug("opening on {}", _Probe.currentThreadId()); } - if (!platformTransactionManagers.isEmpty()) { - var onCloseTasks = _Lists.newArrayList(platformTransactionManagers.size()); + var onCloseHandle = new OnCloseHandle(new ArrayList<>(platformTransactionManagers.size()), new ObservationClosure()); + interaction.putAttribute(OnCloseHandle.class, onCloseHandle); - interaction.putAttribute(OnCloseHandle.class, new OnCloseHandle(onCloseTasks)); + platformTransactionManagers.forEach(txManager -> { - platformTransactionManagers.forEach(txManager -> { + var txDefn = new TransactionTemplate(txManager); // specify the txManager in question + txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - var txDefn = new TransactionTemplate(txManager); // specify the txManager in question - txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + var obs = onCloseHandle.observationClosure().startAndOpenScope(observationProvider.get("Transaction")) + .observation() + .highCardinalityKeyValue("txManager", txManager.getClass().getName()); - // either participate in existing or create new transaction - TransactionStatus txStatus = txManager.getTransaction(txDefn); + // either participate in existing or create new transaction + TransactionStatus txStatus = observationProvider.get("Transaction Creation") + .observe(()->txManager.getTransaction(txDefn)); + if(!txStatus.isNewTransaction()) { + // discard telemetry data when participating in existing transaction + obs.getContext().put("micrometer.discard", true); + // we are participating in an exiting transaction (or testing), nothing to do + return; + } - if(!txStatus.isNewTransaction()) { - // we are participating in an exiting transaction (or testing), nothing to do - return; - } - registerTransactionSynchronizations(txStatus); - - // we have created a new transaction, so need to provide a CloseTask - onCloseTasks.add( - new CloseTask( - txStatus, - txManager.getClass().getName(), // info to be used for display in case of errors - () -> { + registerTransactionSynchronizations(); + + // we have created a new transaction, so need to provide a CloseTask + onCloseHandle.onCloseTasks().add( + new CloseTask( + txStatus, + txManager.getClass().getName(), // info to be used for display in case of errors + ()->observationProvider.get("Transaction Completion") + .observe(() -> { _Xray.txBeforeCompletion(interactionLayerTrackerProvider.get(), "tx: beforeCompletion"); final TransactionCompletionStatus event; if (txStatus.isRollbackOnly()) { @@ -349,13 +356,12 @@ public void onOpen(final @NonNull CausewayInteraction interaction) { event = TransactionCompletionStatus.COMMITTED; } _Xray.txAfterCompletion(interactionLayerTrackerProvider.get(), String.format("tx: afterCompletion (%s)", event.name())); - txCounter.get().increment(); - } - ) - ); - }); - } + }) + ) + ); + }); + } /** @@ -397,9 +403,10 @@ private record CloseTask( @NonNull ThrowingRunnable runnable) { } - @RequiredArgsConstructor - private static class OnCloseHandle { - private final @NonNull List onCloseTasks; + private record OnCloseHandle( + List onCloseTasks, + ObservationClosure observationClosure) { + void requestRollback() { onCloseTasks.forEach(onCloseTask->{ onCloseTask.txStatus.setRollbackOnly(); @@ -407,7 +414,6 @@ void requestRollback() { } void runOnCloseTasks() { onCloseTasks.forEach(onCloseTask->{ - try { onCloseTask.runnable().run(); } catch(final Throwable ex) { @@ -419,6 +425,7 @@ void runOnCloseTasks() { ex); } }); + observationClosure.close(); } } } diff --git a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java index 01769b5f51a..14cd95cbee8 100644 --- a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java +++ b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java @@ -22,7 +22,7 @@ /** * This service, which does nothing in and of itself, exists in order to ensure that the {@link StackedTransactionScope} - * is always initialized, findinag at least one {@link TransactionScope transaction-scope}d service. + * is always initialized, finding at least one {@link TransactionScope transaction-scope}d service. */ @Service @TransactionScope diff --git a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java index f3dd41b30a5..61dd09a93bd 100644 --- a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java +++ b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java @@ -21,42 +21,27 @@ import java.util.Stack; import java.util.UUID; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; -import org.jspecify.annotations.Nullable; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.apache.causeway.commons.internal.base._Refs; + public class StackedTransactionScope implements Scope { @Override public Object get(final String name, final ObjectFactory objectFactory) { - var transactionNestingLevelForThisThread = currentTransactionNestingLevelForThisThread(); - - ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(currentTransactionNestingLevelForThisThread()); + var scopedObjects = currentScopedObjectsHolder(); if (scopedObjects == null) { - scopedObjects = new ScopedObjectsHolder(transactionNestingLevelForThisThread); - if (TransactionSynchronizationManager.isSynchronizationActive()) { - // this happen when TransactionSynchronization#afterCompletion is called. - // it's a catch-22 : we use TransactionSynchronization as a resource to hold the scoped objects, - // but those scoped objects can only be interacted with during the transaction, not after it. - // - // see the 'else' clause below for the handling if we encounter the ScopedObjectsHolder after the - // transaction was completed. - registerWithTransitionSynchronizationManager(scopedObjects); - } else { - scopedObjects.registered = false; - } - TransactionSynchronizationManager.bindResource(transactionNestingLevelForThisThread, scopedObjects); + scopedObjects = createAndBindScopedObjectsHolder(); } else { - if (TransactionSynchronizationManager.isSynchronizationActive()) { - // it's possible that this already-existing scopedObject was added when a synchronization wasn't active - // (see the 'if' block above) and so wouldn't be registered to TSM. If that's the case, we register it now. - if (!scopedObjects.registered) { - registerWithTransitionSynchronizationManager(scopedObjects); - } - } + // it's possible that this already-existing scopedObject was added when a synchronization wasn't active + // (see the 'if' block above) and so wouldn't be registered to TSM. If that's the case, we register it now. + registerWithTransactionSynchronizationManagerIfNotAlready(scopedObjects); } // NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details, // see https://github.com/spring-projects/spring-framework/issues/25801. @@ -68,32 +53,52 @@ public Object get(final String name, final ObjectFactory objectFactory) { return scopedObject; } - private void registerWithTransitionSynchronizationManager(final ScopedObjectsHolder scopedObjects) { - TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization(scopedObjects)); - scopedObjects.registered = true; + private void registerWithTransactionSynchronizationManagerIfNotAlready(final ScopedObjectsHolder scopedObjects) { + if (scopedObjects.registered.isTrue() + || !TransactionSynchronizationManager.isSynchronizationActive()) return; + TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization(this, scopedObjects)); + scopedObjects.registered.setValue(true); } @Override @Nullable public Object remove(final String name) { - var currentTransactionNestingLevel = currentTransactionNestingLevelForThisThread(); - ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(currentTransactionNestingLevel); + var scopedObjects = currentScopedObjectsHolder(); if (scopedObjects != null) { scopedObjects.destructionCallbacks.remove(name); return scopedObjects.scopedInstances.remove(name); - } else { + } else return null; - } } @Override public void registerDestructionCallback(final String name, final Runnable callback) { - ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(currentTransactionNestingLevelForThisThread()); + var scopedObjects = currentScopedObjectsHolder(); if (scopedObjects != null) { scopedObjects.destructionCallbacks.put(name, callback); } } + @Nullable + private ScopedObjectsHolder currentScopedObjectsHolder() { + return (ScopedObjectsHolder) TransactionSynchronizationManager + .getResource(currentTransactionNestingLevelForThisThread()); + } + + private ScopedObjectsHolder createAndBindScopedObjectsHolder() { + final UUID transactionNestingLevelForThisThread = currentTransactionNestingLevelForThisThread(); + var scopedObjects = new ScopedObjectsHolder(transactionNestingLevelForThisThread); + // this happen when TransactionSynchronization#afterCompletion is called. + // it's a catch-22 : we use TransactionSynchronization as a resource to hold the scoped objects, + // but those scoped objects can only be interacted with during the transaction, not after it. + // + // see the 'else' clause below for the handling if we encounter the ScopedObjectsHolder after the + // transaction was completed. + registerWithTransactionSynchronizationManagerIfNotAlready(scopedObjects); + TransactionSynchronizationManager.bindResource(transactionNestingLevelForThisThread, scopedObjects); + return scopedObjects; + } + /** * Holds a unique id for each nested transaction within the current thread. * @@ -103,8 +108,8 @@ public void registerDestructionCallback(final String name, final Runnable callba * using an anonymous new Object(). *

*/ - private static final ThreadLocal> transactionNestingLevelThreadLocal = ThreadLocal.withInitial(() -> { - Stack stack = new Stack<>(); + private static final ThreadLocal> UUID_STACK = ThreadLocal.withInitial(() -> { + var stack = new Stack(); stack.push(UUID.randomUUID()); return stack; }); @@ -113,29 +118,26 @@ public void registerDestructionCallback(final String name, final Runnable callba * Maintains a stack of keys representing nested transactions, where the top-most is the key managed by * {@link TransactionSynchronizationManager} holding the {@link ScopedObjectsHolder} for the current transaction. * - *

- * The keys themselves are {@link UUID}s, having no meaning in themselves other than their identity as the key + *

The keys are {@link UUID}s, having no meaning in themselves other than their identity as the key * into a hashmap. * - *

- * If a transaction is suspended, then the {@link CleanupSynchronization#suspend() suspend} callback is used + *

If a transaction is suspended, then the {@link CleanupSynchronization#suspend() suspend} callback is used * to pop a new key onto the stack, unbinding the previous key's resources (in other words, the * {@link org.apache.causeway.applib.annotation.TransactionScope transaction-scope}d beans of the suspended - * transaction) from {@link TransactionSynchronizationManager}. As transaction-scoped beans are then resolved, + * transaction) from {@link TransactionSynchronizationManager}. As transaction-scoped beans are then resolved, * they will be associated with the new key. * - *

- * Conversely, when a transaction is resumed, then the process is reversed; the old key is popped, and the previous + *

Conversely, when a transaction is resumed, then the process is reversed; the old key is popped, and the previous * key is rebound to the {@link TransactionSynchronizationManager}, meaning that the previous transaction's * {@link org.apache.causeway.applib.annotation.TransactionScope transaction-scope}d beans are brought back. * * @see #currentTransactionNestingLevelForThisThread() * @see #pushToNewTransactionNestingLevelForThisThread() * @see #popToPreviousTransactionNestingLevelForThisThread() - * @see #transactionNestingLevelThreadLocal + * @see #UUID_STACK */ private static Stack transactionNestingLevelForThread() { - return transactionNestingLevelThreadLocal.get(); + return UUID_STACK.get(); } /** @@ -174,48 +176,41 @@ public String getConversationId() { /** * Holder for scoped objects. */ - static class ScopedObjectsHolder { - - private final UUID transactionUuid; - - ScopedObjectsHolder(UUID transactionUuid) { - this.transactionUuid = transactionUuid; + record ScopedObjectsHolder( + UUID transactionUuid, + Map scopedInstances, + Map destructionCallbacks, + /** + * Keeps track of whether these objects have been registered with {@link TransactionSynchronizationManager}. + * + *

This can only be done if + * {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}, which + * isn't the case for {@link ScopedObjectsHolder scoped objects} that are obtained as a result of the + * {@link TransactionSynchronization#afterCompletion(int)} callback. + * We use this flag to keep track in case they are reused in a subsequent transaction. + */ + _Refs.BooleanReference registered) { + + ScopedObjectsHolder( + final UUID transactionUuid) { + this(transactionUuid, new HashMap<>(), new LinkedHashMap<>(), new _Refs.BooleanReference(false)); } - final Map scopedInstances = new HashMap<>(); - final Map destructionCallbacks = new LinkedHashMap<>(); - - /** - * Keeps track of whether these objects have been registered with {@link TransactionSynchronizationManager}. - * - *

- * This can only be done if - * {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}, which - * isn't the case for {@link ScopedObjectsHolder scoped objects} that are obtained as a result of the - * {@link TransactionSynchronization#afterCompletion(int)} callback. We use this flag to keep track in - * case they are reused in a subsequent transaction. - *

- */ - private boolean registered = false; - + @Override public String toString() { return String.format( - "uuid: %s, registered: %s, scopedInstances.size(): %d, destructionCallbacks.size(): %d", - transactionUuid, registered, scopedInstances.size(), destructionCallbacks.size()); + "uuid: %s, registered: %b, scopedInstances.size(): %d, destructionCallbacks.size(): %d", + transactionUuid, registered.isTrue(), scopedInstances.size(), destructionCallbacks.size()); } } - private class CleanupSynchronization implements TransactionSynchronization { - - private final ScopedObjectsHolder scopedObjects; - - public CleanupSynchronization(final ScopedObjectsHolder scopedObjects) { - this.scopedObjects = scopedObjects; - } + private record CleanupSynchronization( + StackedTransactionScope scope, + ScopedObjectsHolder scopedObjects) implements TransactionSynchronization { @Override public void suspend() { - var transactionNestingLevelForThisThread = currentTransactionNestingLevelForThisThread(); + var transactionNestingLevelForThisThread = scope.currentTransactionNestingLevelForThisThread(); TransactionSynchronizationManager.unbindResource(transactionNestingLevelForThisThread); pushToNewTransactionNestingLevelForThisThread(); // subsequent calls to obtain a @TransactionScope'd bean will be against this key } @@ -223,17 +218,17 @@ public void suspend() { @Override public void resume() { popToPreviousTransactionNestingLevelForThisThread(); // the now-completed transaction's @TransactionScope'd beans are no longer required, and will be GC'd. - TransactionSynchronizationManager.bindResource(currentTransactionNestingLevelForThisThread(), this.scopedObjects); + TransactionSynchronizationManager.bindResource(scope.currentTransactionNestingLevelForThisThread(), scopedObjects); } @Override public void afterCompletion(final int status) { - TransactionSynchronizationManager.unbindResourceIfPossible(StackedTransactionScope.this.currentTransactionNestingLevelForThisThread()); - for (Runnable callback : this.scopedObjects.destructionCallbacks.values()) { + TransactionSynchronizationManager.unbindResourceIfPossible(scope.currentTransactionNestingLevelForThisThread()); + for (Runnable callback : scopedObjects.destructionCallbacks.values()) { callback.run(); } - this.scopedObjects.destructionCallbacks.clear(); - this.scopedObjects.scopedInstances.clear(); + scopedObjects.destructionCallbacks.clear(); + scopedObjects.scopedInstances.clear(); } } diff --git a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java index ebc022decde..42af05b351e 100644 --- a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java +++ b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java @@ -32,7 +32,8 @@ public class TransactionScopeBeanFactoryPostProcessor implements BeanFactoryPost public static final String SCOPE_NAME = org.apache.causeway.applib.annotation.TransactionScope.SCOPE_NAME; @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + public void postProcessBeanFactory(@SuppressWarnings("exports") final ConfigurableListableBeanFactory beanFactory) + throws BeansException { var transactionScope = new StackedTransactionScope(); beanFactory.registerScope(SCOPE_NAME, transactionScope); } From ed6a6606138ddb4cc0abb452f887b00dc8be5f8c Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 26 Mar 2026 13:01:15 +0100 Subject: [PATCH 18/31] CAUSEWAY-3975: use prefix for causeway tags --- .../internal/observation/CausewayObservationIntegration.java | 4 ++-- .../causeway/core/metamodel/CausewayModuleCoreMetamodel.java | 2 +- .../runtimeservices/CausewayModuleCoreRuntimeServices.java | 2 +- .../apache/causeway/core/webapp/CausewayModuleCoreWebapp.java | 4 ++-- .../viewer/wicket/model/CausewayModuleViewerWicketModel.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 110f9a7b672..1c2ce466e6d 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -68,8 +68,8 @@ public boolean isNoop() { public Observation createNotStarted(final Class bean, final String name) { return Observation.createNotStarted(name, observationRegistry) - .lowCardinalityKeyValue("module", module) - .lowCardinalityKeyValue("bean", bean.getSimpleName()); + .lowCardinalityKeyValue("causeway.module", module) + .lowCardinalityKeyValue("causeway.bean", bean.getSimpleName()); } @FunctionalInterface diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index 1ab28d0cddd..321fccb0880 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -272,7 +272,7 @@ public ValueCodec valueCodec( @Bean("causeway-metamodel") public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "causeway-metamodel"); + return new CausewayObservationIntegration(observationRegistryOpt, "metamodel"); } } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java index a468caf7e15..2454ddd5c36 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java @@ -159,7 +159,7 @@ public HmacAuthority fallbackHmacAuthority() { @Bean("causeway-runtimeservices") public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "causeway-runtimeservices"); + return new CausewayObservationIntegration(observationRegistryOpt, "runtimeservices"); } } diff --git a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java index 2ab1bee6e54..7dedb2d727d 100644 --- a/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java +++ b/core/webapp/src/main/java/org/apache/causeway/core/webapp/CausewayModuleCoreWebapp.java @@ -98,8 +98,8 @@ public String getContextualName(final ServerRequestObservationContext context) { @Override public KeyValues getHighCardinalityKeyValues(final ServerRequestObservationContext context) { // Make sure that KeyValues entries are already sorted by name for better performance - return KeyValues.of(methodOriginal(context), httpUrl(context), - CausewayObservationIntegration.currentThreadId()); + return super.getHighCardinalityKeyValues(context) + .and(CausewayObservationIntegration.currentThreadId()); } }; } diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java index f9b1bfd97fc..99cba8e3c65 100644 --- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java +++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java @@ -42,6 +42,6 @@ public class CausewayModuleViewerWicketModel { @Bean("causeway-wicketviewer") public CausewayObservationIntegration causewayObservationIntegration( final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "causeway-wicketviewer"); + return new CausewayObservationIntegration(observationRegistryOpt, "wicketviewer"); } } From 430e2582e5febd14139dd066a8c1ad3e25771a60 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 10:33:02 +0100 Subject: [PATCH 19/31] CAUSEWAY-3975: adds telemetries for JpaEntityFacet TODO don't record observations quicker than 2 ms say With Micrometer Observation API, it seems there is no way to discard already started Observations. Perhaps we can prevent those from being sent over the wire later. --- .../CausewayObservationIntegration.java | 74 +++++++++++++++ .../transaction/TransactionServiceSpring.java | 2 +- ...usewayModulePersistenceJpaIntegration.java | 21 ++++- .../integration/entity/JpaEntityFacet.java | 92 ++++++++++++------- 4 files changed, 150 insertions(+), 39 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 1c2ce466e6d..36e1d0fde22 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -18,8 +18,10 @@ */ package org.apache.causeway.commons.internal.observation; +import java.time.Duration; import java.util.Optional; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; @@ -31,6 +33,7 @@ import io.micrometer.common.KeyValue; import io.micrometer.observation.Observation; import io.micrometer.observation.Observation.Scope; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; /** @@ -81,6 +84,10 @@ public ObservationProvider provider(final Class bean) { return name->createNotStarted(bean, name); } + public ObservationProvider provider(final Class bean, final UnaryOperator customizer) { + return name->customizer.apply(createNotStarted(bean, name)); + } + /** * Helps if start and stop of an {@link Observation} happen in different code locations. */ @@ -132,7 +139,74 @@ public ObservationClosure tag(final String key, @Nullable final Supplier public static KeyValue currentThreadId() { var ct = Thread.currentThread(); return KeyValue.of("threadId", "%d [%s]".formatted(ct.getId(), ct.getName())); + } + + /** + * TODO Has no effect. Cannot filter based on name, when {@link Observation} is already started. + * With Micrometer Observation API, it seems there is no way to discard already started Observations. + * Perhaps we can prevent those from being sent over the wire later. + */ + public static void discard(@Nullable final Observation obs) { + if(obs == null) + return; + obs.contextualName("denied"); + } + public record ObservationWithTimeThreshold(Observation delegate, Duration threshold, Timer timer) implements Observation { + private static class Timer { + long startNanos; + void start() { this.startNanos = System.nanoTime(); } + long elapsedNanos() { return System.nanoTime() - startNanos; } + } + public ObservationWithTimeThreshold(final Observation delegate, final Duration threshold) { + this(delegate, threshold, new Timer()); + } + @Override public Observation contextualName(@Nullable final String contextualName) { + return delegate.contextualName(contextualName); + } + @Override public Observation parentObservation(@Nullable final Observation parentObservation) { + return delegate.parentObservation(parentObservation); + } + @Override public Observation lowCardinalityKeyValue(final KeyValue keyValue) { + return delegate.lowCardinalityKeyValue(keyValue); + } + @Override public Observation lowCardinalityKeyValue(final String key, final String value) { + return delegate.lowCardinalityKeyValue(key, value); + } + @Override public Observation highCardinalityKeyValue(final KeyValue keyValue) { + return delegate.highCardinalityKeyValue(keyValue); + } + @Override public Observation highCardinalityKeyValue(final String key, final String value) { + return delegate.highCardinalityKeyValue(key, value); + } + @Override public Observation observationConvention(final ObservationConvention observationConvention) { + return delegate.observationConvention(observationConvention); + } + @Override public Observation error(final Throwable error) { + return delegate.error(error); + } + @Override public Observation event(final Event event) { + return delegate.event(event); + } + @Override public Observation start() { + timer.start(); + return delegate.start(); + } + @Override public Context getContext() { + return delegate.getContext(); + } + @Override public void stop() { + if(timer.elapsedNanos() < threshold.toNanos()) { + discard(delegate); + } + delegate.stop(); + } + @Override public Scope openScope() { + return delegate.openScope(); + } + @Override public String toString() { + return delegate.toString(); + } } } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java index cd19888eb3a..6f3d2ccda89 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java @@ -332,7 +332,7 @@ public void onOpen(final @NonNull CausewayInteraction interaction) { .observe(()->txManager.getTransaction(txDefn)); if(!txStatus.isNewTransaction()) { // discard telemetry data when participating in existing transaction - obs.getContext().put("micrometer.discard", true); + CausewayObservationIntegration.discard(obs); // we are participating in an exiting transaction (or testing), nothing to do return; } diff --git a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java index 34ae16da4db..a18788eb9e0 100644 --- a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java +++ b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.persistence.jpa.integration; +import java.util.Optional; import java.util.Set; import jakarta.persistence.EntityManager; @@ -30,6 +31,7 @@ import org.springframework.data.jpa.repository.JpaContext; import org.springframework.data.jpa.repository.support.DefaultJpaContext; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; import org.apache.causeway.persistence.commons.CausewayModulePersistenceCommons; import org.apache.causeway.persistence.jpa.integration.entity.JpaEntityIntegration; @@ -52,6 +54,8 @@ import org.apache.causeway.persistence.jpa.integration.typeconverters.schema.v2.CausewayOidDtoConverter; import org.apache.causeway.persistence.jpa.metamodel.CausewayModulePersistenceJpaMetamodel; +import io.micrometer.observation.ObservationRegistry; + @Configuration(proxyBeanMethods = false) @Import({ // modules @@ -84,16 +88,27 @@ JavaUtilUuidConverter.class, OffsetTimeConverterForJpa.class, OffsetDateTimeConverterForJpa.class, - ZonedDateTimeConverterForJpa.class - + ZonedDateTimeConverterForJpa.class, }) public class CausewayModulePersistenceJpaIntegration { //TODO close issue https://issues.apache.org/jira/browse/CAUSEWAY-3895 once this can be removed @Bean @Primary - public JpaContext defaultJpaContextWorkaround(final Set entityManagers) { + JpaContext defaultJpaContextWorkaround(final Set entityManagers) { var setOfOne = Set.of(entityManagers.iterator().next()); return new DefaultJpaContext(setOfOne); } + @Bean("causeway-persistence-jpa") + CausewayObservationIntegration causewayObservationIntegration( + final Optional observationRegistryOpt) { + // has no effect +// observationRegistryOpt +// .ifPresent(reg->{ +// reg.observationConfig().observationPredicate((name, context) -> !name.equals("DISCARD")); +// }); + + return new CausewayObservationIntegration(observationRegistryOpt, "persistence-jpa"); + } + } diff --git a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java index 325939710b9..d160187086b 100644 --- a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java +++ b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java @@ -19,7 +19,9 @@ package org.apache.causeway.persistence.jpa.integration.entity; import java.lang.reflect.Method; +import java.time.Duration; import java.util.Optional; +import java.util.function.Function; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; @@ -29,6 +31,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.jpa.repository.JpaContext; import org.apache.causeway.applib.query.AllInstancesQuery; @@ -39,6 +42,9 @@ import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.assertions._Assert; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationWithTimeThreshold; import org.apache.causeway.core.config.beans.CausewayBeanMetaData.PersistenceStack; import org.apache.causeway.core.metamodel.facetapi.FacetAbstract; import org.apache.causeway.core.metamodel.facetapi.FacetHolder; @@ -60,9 +66,12 @@ class JpaEntityFacet @Inject private JpaContext jpaContext; @Inject private IdStringifierLookupService idStringifierLookupService; @Inject private OrmMetadataProvider ormMetadataProvider; + @Qualifier("causeway-persistence-jpa") + @Inject private CausewayObservationIntegration observationIntegration; private final Class entityClass; - private PrimaryKeyType primaryKeyType; + private final PrimaryKeyType primaryKeyType; + private final ObservationProvider observationProvider; protected JpaEntityFacet( final FacetHolder holder, @@ -73,6 +82,9 @@ protected JpaEntityFacet( this.entityClass = entityClass; this.primaryKeyType = idStringifierLookupService .primaryKeyTypeFor(entityClass, getPrimaryKeyType()); + var timeThreshold = Duration.ofMillis(2); + this.observationProvider = observationIntegration + .provider(getClass(), obs->new ObservationWithTimeThreshold(obs, timeThreshold)); } // -- ENTITY FACET @@ -85,9 +97,8 @@ public PersistenceStack getPersistenceStack() { @Override public Optional identifierFor(final @Nullable Object pojo) { - if (!getEntityState(pojo).hasOid()) { + if (!getEntityState(pojo).hasOid()) return Optional.empty(); - } var entityManager = getEntityManager(); var persistenceUnitUtil = getPersistenceUnitUtil(entityManager); @@ -106,14 +117,12 @@ public Bookmark validateBookmark(final @NonNull Bookmark bookmark) { @Override public Optional fetchByBookmark(final @NonNull Bookmark bookmark) { - log.debug("fetchEntity; bookmark={}", bookmark); - - var primaryKey = primaryKeyType.destring(bookmark.identifier()); - - var entityManager = getEntityManager(); - var entityPojo = entityManager.find(entityClass, primaryKey); - return Optional.ofNullable(entityPojo); + return observationProvider.get("Fetch by Bookmark (%s)".formatted(bookmark)) + .observe(()->{ + var primaryKey = primaryKeyType.destring(bookmark.identifier()); + return Optional.ofNullable(getEntityManager().find(entityClass, primaryKey)); + }); } private Class getPrimaryKeyType() { @@ -125,8 +134,10 @@ public Can fetchByQuery(final Query query) { var range = query.getRange(); - if (query instanceof AllInstancesQuery) { + var entitySpec = getEntitySpecification(); + final Function adapter = entity -> ManagedObject.adaptSingular(entitySpec, entity); + if (query instanceof AllInstancesQuery) { var queryFindAllInstances = (AllInstancesQuery) query; var queryEntityType = queryFindAllInstances.getResultType(); @@ -141,13 +152,13 @@ public Can fetchByQuery(final Query query) { typedQuery.setMaxResults(range.getLimitAsInt()); } - var entitySpec = getEntitySpecification(); - return Can.ofStream( - typedQuery.getResultStream() - .map(entity -> ManagedObject.adaptSingular(entitySpec, entity))); + var obs = observationProvider.get("Fetch all Instances (%s)" + .formatted(queryFindAllInstances.getDescription())); - } else if (query instanceof NamedQuery) { + return obs.observe(()-> + Can.ofStream(typedQuery.getResultStream().map(adapter))); + } else if (query instanceof NamedQuery) { var applibNamedQuery = (NamedQuery) query; var queryResultType = applibNamedQuery.getResultType(); @@ -168,11 +179,10 @@ public Can fetchByQuery(final Query query) { .forEach((paramName, paramValue) -> namedQuery.setParameter(paramName, paramValue)); - var entitySpec = getEntitySpecification(); - return Can.ofStream( - namedQuery.getResultStream() - .map(entity -> ManagedObject.adaptSingular(entitySpec, entity))); - + var obs = observationProvider.get("Fetch all Instances (%s)" + .formatted(applibNamedQuery.getDescription())); + return obs.observe(()-> + Can.ofStream(namedQuery.getResultStream().map(adapter))); } throw _Exceptions.unsupportedOperation( @@ -181,9 +191,8 @@ public Can fetchByQuery(final Query query) { @Override public void persist(final Object pojo) { - if (pojo == null) { + if (pojo == null) return; // nothing to do - } // guard against misuse _Assert.assertNullableObjectIsInstanceOf(pojo, entityClass); @@ -192,7 +201,8 @@ public void persist(final Object pojo) { log.debug("about to persist entity {}", pojo); - entityManager.persist(pojo); + observationProvider.get("Persist (type=%s)".formatted(entityClass.getName())) + .observe(()->entityManager.persist(pojo)); } @Override @Nullable @@ -203,30 +213,33 @@ public T refresh(final @Nullable T pojo) { _Assert.assertNullableObjectIsInstanceOf(pojo, entityClass); var entityManager = getEntityManager(); - entityManager.refresh(pojo); + + observationProvider.get("Refresh (type=%s)".formatted(entityClass.getName())) + .observe(()->entityManager.refresh(pojo)); + return pojo; } @Override public void delete(final Object pojo) { - if (pojo == null) { + if (pojo == null) return; // nothing to do - } // guard against misuse _Assert.assertNullableObjectIsInstanceOf(pojo, entityClass); var entityManager = getEntityManager(); - entityManager.remove(pojo); + + observationProvider.get("Remove (type=%s)".formatted(entityClass.getName())) + .observe(()->entityManager.remove(pojo)); } @Override public EntityState getEntityState(final Object pojo) { if (pojo == null - || !entityClass.isAssignableFrom(pojo.getClass())) { + || !entityClass.isAssignableFrom(pojo.getClass())) return EntityState.NOT_PERSISTABLE; - } var entityManager = getEntityManager(); var persistenceUnitUtil = getPersistenceUnitUtil(entityManager); @@ -237,9 +250,8 @@ public EntityState getEntityState(final Object pojo) { @Override public Object versionOf(final Object pojo) { if (getEntityState(pojo).isAttached()) { - if (pojo instanceof HasVersion) { + if (pojo instanceof HasVersion) return ((HasVersion)pojo).getVersion(); - } } return null; } @@ -251,7 +263,17 @@ public boolean isProxyEnhancement(final Method method) { @Override public T detach(final T pojo) { - getEntityManager().detach(pojo); + if (pojo == null) + return null; + + // guard against misuse + _Assert.assertNullableObjectIsInstanceOf(pojo, entityClass); + + var entityManager = getEntityManager(); + + observationProvider.get("Detach (type=%s)".formatted(entityClass.getName())) + .observe(()->entityManager.detach(pojo)); + return pojo; } @@ -271,10 +293,10 @@ protected EntityManager getEntityManager() { protected PersistenceUnitUtil getPersistenceUnitUtil(final EntityManager entityManager) { return entityManager.getEntityManagerFactory().getPersistenceUnitUtil(); } - + // -- HELPER - private TypedQuery selectFrom(Class entityClass) { + private TypedQuery selectFrom(final Class entityClass) { var entityManager = getEntityManager(); var q = entityManager.getCriteriaBuilder().createQuery(entityClass); q.select(q.from(entityClass)); From 3b6508a89151fedee35ce2500facf28037902c89 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 14:11:20 +0100 Subject: [PATCH 20/31] CAUSEWAY-3975: proper span exporting filter --- commons/pom.xml | 5 ++ commons/src/main/java/module-info.java | 1 + .../CausewayObservationIntegration.java | 49 ++++++++++++++----- .../core/config/CausewayModuleCoreConfig.java | 35 +++++++++---- .../CausewayModuleCoreMetamodel.java | 10 ---- .../services/init/MetamodelInitializer.java | 7 +-- .../CausewayModuleCoreRuntimeServices.java | 11 ----- .../session/InteractionServiceDefault.java | 4 +- .../transaction/TransactionServiceSpring.java | 7 +-- ...usewayModulePersistenceJpaIntegration.java | 18 +------ .../integration/entity/JpaEntityFacet.java | 8 +-- .../CausewayModuleViewerWicketModel.java | 11 ----- .../integration/TelemetryStartHandler.java | 4 +- .../wicketapp/CausewayWicketApplication.java | 3 -- 14 files changed, 87 insertions(+), 86 deletions(-) diff --git a/commons/pom.xml b/commons/pom.xml index 18546295a83..d80521cdb59 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -144,6 +144,11 @@ org.springframework.boot spring-boot-starter + + + io.micrometer + micrometer-tracing + diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java index 47a24ab13b0..12dadbb4686 100644 --- a/commons/src/main/java/module-info.java +++ b/commons/src/main/java/module-info.java @@ -70,6 +70,7 @@ requires transitive tools.jackson.module.jakarta.xmlbind; requires transitive micrometer.commons; requires transitive micrometer.observation; + requires transitive micrometer.tracing; requires transitive org.jdom2; requires transitive org.jspecify; requires transitive org.jsoup; diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 36e1d0fde22..debeccaf92b 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -32,9 +33,12 @@ import io.micrometer.common.KeyValue; import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Context; import io.micrometer.observation.Observation.Scope; import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.exporter.SpanExportingPredicate; /** * Holder of {@link ObservationRegistry} which comes as a dependency of spring-context. @@ -49,20 +53,17 @@ * */ public record CausewayObservationIntegration( - ObservationRegistry observationRegistry, - String module) { + ObservationRegistry observationRegistry) { public CausewayObservationIntegration( - final Optional observationRegistryOpt, - final String module) { - this(observationRegistryOpt.orElse(ObservationRegistry.NOOP), module); + final Optional observationRegistryOpt) { + this(observationRegistryOpt.orElse(ObservationRegistry.NOOP)); } public CausewayObservationIntegration { observationRegistry = observationRegistry!=null ? observationRegistry : ObservationRegistry.NOOP; - module = StringUtils.hasText(module) ? module : "unknown_module"; } public boolean isNoop() { @@ -70,8 +71,7 @@ public boolean isNoop() { } public Observation createNotStarted(final Class bean, final String name) { - return Observation.createNotStarted(name, observationRegistry) - .lowCardinalityKeyValue("causeway.module", module) + return Observation.createNotStarted(name, Context::new, observationRegistry) .lowCardinalityKeyValue("causeway.bean", bean.getSimpleName()); } @@ -84,10 +84,16 @@ public ObservationProvider provider(final Class bean) { return name->createNotStarted(bean, name); } - public ObservationProvider provider(final Class bean, final UnaryOperator customizer) { + public ObservationProvider provider(final Class bean, final Function customizer) { return name->customizer.apply(createNotStarted(bean, name)); } + public static UnaryOperator withModuleName(final String moduleName){ + return obs->StringUtils.hasText(moduleName) + ? obs.lowCardinalityKeyValue("causeway.module", moduleName) + : obs; + } + /** * Helps if start and stop of an {@link Observation} happen in different code locations. */ @@ -134,6 +140,11 @@ public ObservationClosure tag(final String key, @Nullable final Supplier return this; } + public void discard() { + CausewayObservationIntegration.discard(this.observation); + close(); + } + } public static KeyValue currentThreadId() { @@ -141,17 +152,29 @@ public static KeyValue currentThreadId() { return KeyValue.of("threadId", "%d [%s]".formatted(ct.getId(), ct.getName())); } + private static final KeyValue DISCARD_KEY = KeyValue.of("causeway.discard", ""); + /** - * TODO Has no effect. Cannot filter based on name, when {@link Observation} is already started. - * With Micrometer Observation API, it seems there is no way to discard already started Observations. - * Perhaps we can prevent those from being sent over the wire later. + * Denies span export, in collaboration with a Spring registered {@link DiscardedSpanExportingPredicate}. */ public static void discard(@Nullable final Observation obs) { if(obs == null) return; - obs.contextualName("denied"); + obs.lowCardinalityKeyValue(DISCARD_KEY); + } + + /** + * Does not allow discarded spans to be exported. Register with Spring (before auto configuration is running). + */ + public record DiscardedSpanExportingPredicate() implements SpanExportingPredicate { + @Override + public boolean isExportable(final FinishedSpan span) { + return !span.getTags().containsKey(DISCARD_KEY.getKey()); + } } + //TODO perhaps threshold should not be hardcoded at call site; what we really want is to report Observations + // that are way off a base-line; this would require some profiling to establish base-lines public record ObservationWithTimeThreshold(Observation delegate, Duration threshold, Timer timer) implements Observation { private static class Timer { long startNanos; diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java index 5e89e825129..18124a36da6 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayModuleCoreConfig.java @@ -18,12 +18,17 @@ */ package org.apache.causeway.core.config; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; + import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.DiscardedSpanExportingPredicate; import org.apache.causeway.core.config.applib.RestfulPathProvider; import org.apache.causeway.core.config.beans.CausewayBeanFactoryPostProcessor; import org.apache.causeway.core.config.converters.PatternsConverter; @@ -34,9 +39,15 @@ import org.apache.causeway.core.config.validators.PatternOptionalStringConstraintValidator; import org.apache.causeway.core.config.viewer.web.WebAppContextPath; +import io.micrometer.observation.ObservationRegistry; + @Configuration(proxyBeanMethods = false) @Import({ + // Observation configuration + // needs to happen early, at least before auto configuration gets to run + DiscardedSpanExportingPredicate.class, + // @Component CausewayConfiguration.class, PatternsConverter.class, @@ -64,15 +75,15 @@ public class CausewayModuleCoreConfig { public static final String NAMESPACE = "causeway.config"; @Bean - public EmailConfiguration emailConfiguration( - CausewayConfiguration conf, - @Value("#{systemProperties['spring.mail.username']}") String senderEmailUsername, - @Value("#{systemProperties['spring.mail.password']}") String senderEmailPassword, - @Value("#{systemProperties['spring.mail.host']}") String senderEmailHostName, - @Value("#{systemProperties['spring.mail.port']}") Integer senderEmailPort, - @Value("#{systemProperties['spring.mail.javamail.properties.mail.smtp.starttls.enable']}") Boolean senderEmailTlsEnabled, - @Value("#{systemProperties['spring.mail.properties.mail.smtp.timeout']}") Integer smtpTimeout, - @Value("#{systemProperties['spring.mail.properties.mail.smtp.connectiontimeout']}") Integer smtpConnectionTimeout) { + EmailConfiguration emailConfiguration( + final CausewayConfiguration conf, + @Value("#{systemProperties['spring.mail.username']}") final String senderEmailUsername, + @Value("#{systemProperties['spring.mail.password']}") final String senderEmailPassword, + @Value("#{systemProperties['spring.mail.host']}") final String senderEmailHostName, + @Value("#{systemProperties['spring.mail.port']}") final Integer senderEmailPort, + @Value("#{systemProperties['spring.mail.javamail.properties.mail.smtp.starttls.enable']}") final Boolean senderEmailTlsEnabled, + @Value("#{systemProperties['spring.mail.properties.mail.smtp.timeout']}") final Integer smtpTimeout, + @Value("#{systemProperties['spring.mail.properties.mail.smtp.connectiontimeout']}") final Integer smtpConnectionTimeout) { var emailConfiguration = conf.core().runtimeServices().email(); @@ -91,4 +102,10 @@ public EmailConfiguration emailConfiguration( _Strings.emptyToNull(emailConfiguration.override().bcc())); } + @Bean + CausewayObservationIntegration causewayObservationIntegration( + final Optional observationRegistryOpt) { + return new CausewayObservationIntegration(observationRegistryOpt); + } + } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java index 321fccb0880..243b1520076 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.stream.Stream; import jakarta.inject.Provider; @@ -43,7 +42,6 @@ import org.apache.causeway.commons.functional.Either; import org.apache.causeway.commons.functional.Railway; import org.apache.causeway.commons.functional.Try; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.commons.semantics.CollectionSemantics; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.CausewayModuleCoreConfig; @@ -119,8 +117,6 @@ import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault; import org.apache.causeway.core.security.CausewayModuleCoreSecurity; -import io.micrometer.observation.ObservationRegistry; - @Configuration(proxyBeanMethods = false) @Import({ // Modules @@ -269,10 +265,4 @@ public ValueCodec valueCodec( return new ValueCodec(bookmarkService, valueSemanticsResolverProvider); } - @Bean("causeway-metamodel") - public CausewayObservationIntegration causewayObservationIntegration( - final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "metamodel"); - } - } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java index 3220c879aa2..951cc481143 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java @@ -23,7 +23,6 @@ import jakarta.inject.Inject; import jakarta.inject.Provider; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -38,6 +37,7 @@ import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; +import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import lombok.extern.slf4j.Slf4j; @@ -53,9 +53,10 @@ public record MetamodelInitializer( public MetamodelInitializer( final EventBusService eventBusService, final Provider specificationLoaderProvider, - @Qualifier("causeway-metamodel") final CausewayObservationIntegration observationIntegration) { - this(eventBusService, specificationLoaderProvider, observationIntegration.provider(MetamodelInitializer.class)); + this(eventBusService, specificationLoaderProvider, observationIntegration + .provider(MetamodelInitializer.class, + CausewayObservationIntegration.withModuleName(CausewayModuleCoreMetamodel.NAMESPACE))); } @EventListener diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java index 2454ddd5c36..bee6e2db2ce 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java @@ -18,8 +18,6 @@ */ package org.apache.causeway.core.runtimeservices; -import java.util.Optional; - import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -31,7 +29,6 @@ import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.bookmark.HmacAuthority; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault; @@ -76,8 +73,6 @@ import org.apache.causeway.core.runtimeservices.xml.XmlServiceDefault; import org.apache.causeway.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefault; -import io.micrometer.observation.ObservationRegistry; - @Configuration(proxyBeanMethods = false) @Import({ // Modules @@ -156,10 +151,4 @@ public HmacAuthority fallbackHmacAuthority() { } } - @Bean("causeway-runtimeservices") - public CausewayObservationIntegration causewayObservationIntegration( - final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "runtimeservices"); - } - } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 9a838fde6fc..4e464582fae 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -100,7 +100,6 @@ public class InteractionServiceDefault @Inject public InteractionServiceDefault( final EventBusService eventBusService, - @Qualifier("causeway-runtimeservices") final CausewayObservationIntegration observationIntegration, final Provider specificationLoaderProvider, final ServiceInjector serviceInjector, @@ -109,7 +108,8 @@ public InteractionServiceDefault( final Provider commandPublisherProvider, final ConfigurableBeanFactory beanFactory, final InteractionIdGenerator interactionIdGenerator) { - this.observationProvider = observationIntegration.provider(getClass()); + this.observationProvider = observationIntegration.provider(getClass(), + CausewayObservationIntegration.withModuleName(CausewayModuleCoreRuntimeServices.NAMESPACE)); this.serviceInjector = serviceInjector; this.transactionServiceSpring = transactionServiceSpring; this.clockService = clockService; diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java index 6f3d2ccda89..74e96c32ba4 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java @@ -60,6 +60,7 @@ import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.interaction.session.CausewayInteraction; +import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; import org.apache.causeway.core.runtime.flushmgmt.FlushMgmt; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.core.transaction.events.TransactionCompletionStatus; @@ -97,7 +98,6 @@ public TransactionServiceSpring( final List persistenceExceptionTranslators, final Provider interactionLayerTrackerProvider, final ConfigurableListableBeanFactory configurableListableBeanFactory, - @Qualifier("causeway-runtimeservices") final CausewayObservationIntegration observationIntegration ) { @@ -111,7 +111,8 @@ public TransactionServiceSpring( this.interactionLayerTrackerProvider = interactionLayerTrackerProvider; - this.observationProvider = observationIntegration.provider(getClass()); + this.observationProvider = observationIntegration.provider(getClass(), + CausewayObservationIntegration.withModuleName(CausewayModuleCoreMetamodel.NAMESPACE)); } // -- API @@ -332,7 +333,7 @@ public void onOpen(final @NonNull CausewayInteraction interaction) { .observe(()->txManager.getTransaction(txDefn)); if(!txStatus.isNewTransaction()) { // discard telemetry data when participating in existing transaction - CausewayObservationIntegration.discard(obs); + onCloseHandle.observationClosure().discard(); // we are participating in an exiting transaction (or testing), nothing to do return; } diff --git a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java index a18788eb9e0..c401ca76fe0 100644 --- a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java +++ b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/CausewayModulePersistenceJpaIntegration.java @@ -18,7 +18,6 @@ */ package org.apache.causeway.persistence.jpa.integration; -import java.util.Optional; import java.util.Set; import jakarta.persistence.EntityManager; @@ -31,7 +30,6 @@ import org.springframework.data.jpa.repository.JpaContext; import org.springframework.data.jpa.repository.support.DefaultJpaContext; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime; import org.apache.causeway.persistence.commons.CausewayModulePersistenceCommons; import org.apache.causeway.persistence.jpa.integration.entity.JpaEntityIntegration; @@ -54,8 +52,6 @@ import org.apache.causeway.persistence.jpa.integration.typeconverters.schema.v2.CausewayOidDtoConverter; import org.apache.causeway.persistence.jpa.metamodel.CausewayModulePersistenceJpaMetamodel; -import io.micrometer.observation.ObservationRegistry; - @Configuration(proxyBeanMethods = false) @Import({ // modules @@ -92,6 +88,8 @@ }) public class CausewayModulePersistenceJpaIntegration { + public static final String NAMESPACE = "causeway.persistence.jpa.integration"; + //TODO close issue https://issues.apache.org/jira/browse/CAUSEWAY-3895 once this can be removed @Bean @Primary JpaContext defaultJpaContextWorkaround(final Set entityManagers) { @@ -99,16 +97,4 @@ JpaContext defaultJpaContextWorkaround(final Set entityManagers) return new DefaultJpaContext(setOfOne); } - @Bean("causeway-persistence-jpa") - CausewayObservationIntegration causewayObservationIntegration( - final Optional observationRegistryOpt) { - // has no effect -// observationRegistryOpt -// .ifPresent(reg->{ -// reg.observationConfig().observationPredicate((name, context) -> !name.equals("DISCARD")); -// }); - - return new CausewayObservationIntegration(observationRegistryOpt, "persistence-jpa"); - } - } diff --git a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java index d160187086b..7ba1410fcfa 100644 --- a/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java +++ b/persistence/jpa/integration/src/main/java/org/apache/causeway/persistence/jpa/integration/entity/JpaEntityFacet.java @@ -31,7 +31,6 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.jpa.repository.JpaContext; import org.apache.causeway.applib.query.AllInstancesQuery; @@ -53,6 +52,7 @@ import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.services.idstringifier.IdStringifierLookupService; import org.apache.causeway.persistence.jpa.applib.integration.HasVersion; +import org.apache.causeway.persistence.jpa.integration.CausewayModulePersistenceJpaIntegration; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -66,7 +66,6 @@ class JpaEntityFacet @Inject private JpaContext jpaContext; @Inject private IdStringifierLookupService idStringifierLookupService; @Inject private OrmMetadataProvider ormMetadataProvider; - @Qualifier("causeway-persistence-jpa") @Inject private CausewayObservationIntegration observationIntegration; private final Class entityClass; @@ -83,8 +82,9 @@ protected JpaEntityFacet( this.primaryKeyType = idStringifierLookupService .primaryKeyTypeFor(entityClass, getPrimaryKeyType()); var timeThreshold = Duration.ofMillis(2); - this.observationProvider = observationIntegration - .provider(getClass(), obs->new ObservationWithTimeThreshold(obs, timeThreshold)); + this.observationProvider = observationIntegration.provider(getClass(), + CausewayObservationIntegration.withModuleName(CausewayModulePersistenceJpaIntegration.NAMESPACE) + .andThen(obs->new ObservationWithTimeThreshold(obs, timeThreshold))); } // -- ENTITY FACET diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java index 99cba8e3c65..fe4c67376ed 100644 --- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java +++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/CausewayModuleViewerWicketModel.java @@ -18,17 +18,11 @@ */ package org.apache.causeway.viewer.wicket.model; -import java.util.Optional; - -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.core.webapp.CausewayModuleCoreWebapp; -import io.micrometer.observation.ObservationRegistry; - /** * @since 1.x {@index} */ @@ -39,9 +33,4 @@ }) public class CausewayModuleViewerWicketModel { - @Bean("causeway-wicketviewer") - public CausewayObservationIntegration causewayObservationIntegration( - final Optional observationRegistryOpt) { - return new CausewayObservationIntegration(observationRegistryOpt, "wicketviewer"); - } } diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index 10b4274ab4a..0800f5dddc9 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -24,6 +24,7 @@ import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; +import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer; /** * @since 4.0 @@ -33,7 +34,8 @@ public record TelemetryStartHandler( implements IRequestCycleListener { public TelemetryStartHandler(final CausewayObservationIntegration observationIntegration) { - this(observationIntegration.provider(TelemetryStartHandler.class)); + this(observationIntegration.provider(TelemetryStartHandler.class, + CausewayObservationIntegration.withModuleName(CausewayModuleViewerWicketViewer.NAMESPACE))); } @Override diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java index 0c56901a428..41c0e681f68 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketApplication.java @@ -46,7 +46,6 @@ import org.apache.wicket.settings.RequestCycleSettings; import org.apache.wicket.spring.injection.annot.SpringComponentInjector; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.apache.causeway.applib.services.inject.ServiceInjector; @@ -118,8 +117,6 @@ public static CausewayWicketApplication get() { @Inject private CausewaySystemEnvironment systemEnvironment; @Inject private CausewayConfiguration configuration; @Inject private MetricsService metricService; - - @Qualifier("causeway-wicketviewer") @Inject private CausewayObservationIntegration observationIntegration; @Getter(onMethod = @__(@Override)) From 7fd5e40f0f9c784da327c79cbd2ff2c50fc56a29 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 17:32:39 +0100 Subject: [PATCH 21/31] CAUSEWAY-3975: ajax detector --- .../CausewayObservationIntegration.java | 39 +++++++++++-------- .../integration/TelemetryStartHandler.java | 16 +++++++- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index debeccaf92b..73b61a98307 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -42,15 +42,6 @@ /** * Holder of {@link ObservationRegistry} which comes as a dependency of spring-context. - * - * @apiNote each Causeway module can have its own, using qualifiers and bean factory methods, e.g.: - *
- * @Bean("causeway-metamodel")
- * public CausewayObservationInternal causewayObservationInternal(
- *   Optional observationRegistryOpt) {
- *   return new CausewayObservationInternal(observationRegistryOpt, "causeway-metamodel");
- * }
- *  
*/ public record CausewayObservationIntegration( ObservationRegistry observationRegistry) { @@ -62,8 +53,8 @@ public CausewayObservationIntegration( public CausewayObservationIntegration { observationRegistry = observationRegistry!=null - ? observationRegistry - : ObservationRegistry.NOOP; + ? observationRegistry + : ObservationRegistry.NOOP; } public boolean isNoop() { @@ -75,6 +66,8 @@ public Observation createNotStarted(final Class bean, final String name) { .lowCardinalityKeyValue("causeway.bean", bean.getSimpleName()); } + // -- OBSERVATION PROVIDER + @FunctionalInterface public interface ObservationProvider { Observation get(String name); @@ -90,10 +83,27 @@ public ObservationProvider provider(final Class bean, final Function withModuleName(final String moduleName){ return obs->StringUtils.hasText(moduleName) - ? obs.lowCardinalityKeyValue("causeway.module", moduleName) + ? obs.lowCardinalityKeyValue(moduleName(moduleName)) : obs; } + // -- COMMON KEY-VALUES + + public static KeyValue currentThreadId() { + var ct = Thread.currentThread(); + return KeyValue.of("causeway.threadId", "%d [%s]".formatted(ct.getId(), ct.getName())); + } + + public static KeyValue moduleName(final String moduleName) { + return KeyValue.of("causeway.module", + moduleName.startsWith("causeway.") + || moduleName.startsWith("causeway-") + ? moduleName.substring(9) + : moduleName); + } + + // -- OBSERVATION CLOSURE + /** * Helps if start and stop of an {@link Observation} happen in different code locations. */ @@ -147,10 +157,7 @@ public void discard() { } - public static KeyValue currentThreadId() { - var ct = Thread.currentThread(); - return KeyValue.of("threadId", "%d [%s]".formatted(ct.getId(), ct.getName())); - } + // -- SPAN EXPORT DISCARDING SUPPORT private static final KeyValue DISCARD_KEY = KeyValue.of("causeway.discard", ""); diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java index 0800f5dddc9..1765b202a49 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java @@ -19,8 +19,10 @@ package org.apache.causeway.viewer.wicket.viewer.integration; import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.http.WebRequest; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; @@ -41,8 +43,12 @@ public TelemetryStartHandler(final CausewayObservationIntegration observationInt @Override public synchronized void onBeginRequest(final RequestCycle requestCycle) { if (requestCycle instanceof RequestCycle2 requestCycle2) { + var name = isAjax(requestCycle.getRequest()) + ? "Apache Wicket Request Cycle (AJAX)" + : "Apache Wicket Request Cycle"; + requestCycle2.observationClosure.startAndOpenScope( - observationProvider.get("Apache Wicket Request Cycle")); + observationProvider.get(name)); } } @@ -54,4 +60,12 @@ public IRequestHandler onException(final RequestCycle requestCycle, final Except return null; } + // -- HELPER + + private boolean isAjax(final Request request) { + return request instanceof WebRequest webRequest + ? webRequest.isAjax() + : false; + } + } From a33c792c781ef36da98ac41c2dd306be1226c4a7 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 18:32:43 +0100 Subject: [PATCH 22/31] CAUSEWAY-3975: report interaction details (locale, clock, ..) --- .../CausewayObservationIntegration.java | 37 +++++++++++ .../session/InteractionServiceDefault.java | 19 ++---- .../runtimeservices/session/_Observation.java | 63 +++++++++++++++++++ 3 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Observation.java diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java index 73b61a98307..7e90105f308 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java @@ -19,6 +19,10 @@ package org.apache.causeway.commons.internal.observation; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -28,6 +32,8 @@ import org.springframework.util.StringUtils; +import org.apache.causeway.commons.internal.base._Strings; + import lombok.Getter; import lombok.experimental.Accessors; @@ -102,6 +108,37 @@ public static KeyValue moduleName(final String moduleName) { : moduleName); } + /** + * UTC ISO format + */ + public static KeyValue interactionClock(final Instant instant) { + return KeyValue.of("causeway.interaction.clock", DateTimeFormatter.ISO_INSTANT.format(instant)); + } + public static KeyValue interactionDepth(final int value) { + return KeyValue.of("causeway.interaction.depth", "" + value); + } + public static KeyValue interactionLanguage(final Locale locale) { + return KeyValue.of("causeway.interaction.language", locale.toString()); + } + public static KeyValue interactionNumberFormat(final Locale locale) { + return KeyValue.of("causeway.interaction.numberformat", locale.toString()); + } + public static KeyValue interactionTimeFormat(final Locale locale) { + return KeyValue.of("causeway.interaction.timeformat", locale.toString()); + } + public static KeyValue interactionTimezone(final ZoneId zone) { + return KeyValue.of("causeway.interaction.timezone", zone.getId()); + } + public static KeyValue userName(final @Nullable String value) { + return KeyValue.of("causeway.user.name", _Strings.nullToEmpty(value)); + } + public static KeyValue userImpersonating(final boolean value) { + return KeyValue.of("causeway.user.impersonating", "" + value); + } + public static KeyValue userMultiTenancyToken(final @Nullable String value) { + return KeyValue.of("causeway.user.multiTenancyToken", _Strings.nullToEmpty(value)); + } + // -- OBSERVATION CLOSURE /** diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java index 4e464582fae..f2f3e341027 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java @@ -47,7 +47,6 @@ import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.inject.ServiceInjector; import org.apache.causeway.commons.functional.ThrowingRunnable; -import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; @@ -148,22 +147,16 @@ public InteractionLayer openInteraction(final @NonNull InteractionContext intera .map(it->(CausewayInteraction)it) .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId())); + final int depth = getInteractionLayerCount(); - var obs = observationProvider.get(getInteractionLayerCount()==0 + var obs = observationProvider.get(depth == 0 ? "Causeway Root Interaction" : "Causeway Nested Interaction"); var newInteractionLayer = layerStack.push(causewayInteraction, interactionContextToUse, obs); - obs.highCardinalityKeyValue("user.isImpersonating", "" + interactionContextToUse.getUser().isImpersonating()); - _Strings.nonEmpty(interactionContextToUse.getUser().multiTenancyToken()) - .ifPresent(value->obs.highCardinalityKeyValue("user.multiTenancyToken", value)); - _Strings.nonEmpty(interactionContextToUse.getUser().name()) - .ifPresent(value->obs.highCardinalityKeyValue("user.name", value)); - if(getInteractionLayerCount()>0) { - obs.highCardinalityKeyValue("stackedLayers", ""+getInteractionLayerCount()); - } + _Observation.addTags(obs, interactionContextToUse, depth); - if(isAtTopLevel()) { + if(depth == 0) { transactionServiceSpring.onOpen(causewayInteraction); interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); } @@ -320,7 +313,7 @@ private void requestRollback(final Throwable cause) { transactionServiceSpring.requestRollback(interaction); } - private boolean isAtTopLevel() { + private boolean isAtRootLevel() { return layerStack.size()==1; } @@ -398,7 +391,7 @@ private void closeInteractionLayerStackDownToStackSize(final int downToStackSize try { layerStack.popWhile(currentLayer->{ if(!(layerStack.size()>downToStackSize)) return false; - if(isAtTopLevel()) { + if(isAtRootLevel()) { // keep the stack unmodified yet, to allow for callbacks to properly operate preInteractionClosed((CausewayInteraction)currentLayer.interaction()); } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Observation.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Observation.java new file mode 100644 index 00000000000..92cfd935a46 --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/_Observation.java @@ -0,0 +1,63 @@ +/* + * 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.session; + +import org.apache.causeway.applib.services.iactnlayer.InteractionContext; +import org.apache.causeway.commons.internal.base._Strings; + +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionClock; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionDepth; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionLanguage; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionNumberFormat; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionTimeFormat; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.interactionTimezone; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.userImpersonating; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.userMultiTenancyToken; +import static org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.userName; + +import lombok.experimental.UtilityClass; + +import io.micrometer.observation.Observation; + +@UtilityClass +class _Observation { + + void addTags(final Observation obs, final InteractionContext ic, final int depth) { + if(depth>0) { + obs.lowCardinalityKeyValue(interactionDepth(depth)); + } + + obs.highCardinalityKeyValue(interactionClock(ic.getClock().nowAsInstant())); + obs.lowCardinalityKeyValue(interactionLanguage(ic.getLocale().languageLocale())); + obs.lowCardinalityKeyValue(interactionNumberFormat(ic.getLocale().numberFormatLocale())); + obs.lowCardinalityKeyValue(interactionTimeFormat(ic.getLocale().timeFormatLocale())); + obs.lowCardinalityKeyValue(interactionTimezone(ic.getTimeZone())); + + obs.lowCardinalityKeyValue(userImpersonating(ic.getUser().isImpersonating())); + + _Strings.nonEmpty(ic.getUser().multiTenancyToken()) + .ifPresent(value->obs.highCardinalityKeyValue(userMultiTenancyToken(value))); + + _Strings.nonEmpty(ic.getUser().name()) + .ifPresent(value->obs.highCardinalityKeyValue(userName(value))); + + + } + +} From ce13999c8c034d6f3b4f7e8e9b31afefc70b293b Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 19:11:10 +0100 Subject: [PATCH 23/31] CAUSEWAY-3975: observe member execution (act invoke, prop change) --- .../execution/MemberExecutorService.java | 8 +- .../MemberExecutorServiceDefault.java | 126 +++++++++++------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/MemberExecutorService.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/MemberExecutorService.java index 03a5534d839..0d6a52e1928 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/MemberExecutorService.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/MemberExecutorService.java @@ -20,6 +20,8 @@ import java.util.Optional; +import org.jspecify.annotations.NonNull; + import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; @@ -34,12 +36,10 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; -import org.jspecify.annotations.NonNull; - /** * Used by ActionInvocationFacets and PropertySetterOrClearFacets to submit their executions. - *

- * That is, invoke a domain action or edit a domain property. + * + *

That is, invoke a domain action or edit a domain property. * * @since 2.0 */ diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java index 616808c4ad8..aa3b687d8e4 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java @@ -18,10 +18,18 @@ */ package org.apache.causeway.core.runtimeservices.executor; -import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled; - import java.util.Optional; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; + +import org.jspecify.annotations.NonNull; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.command.Command; @@ -29,14 +37,14 @@ import org.apache.causeway.applib.services.iactn.Execution; import org.apache.causeway.applib.services.iactn.PropertyEdit; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; -import org.apache.causeway.applib.services.inject.ServiceInjector; import org.apache.causeway.applib.services.metrics.MetricsService; -import org.apache.causeway.applib.services.repository.RepositoryService; import org.apache.causeway.applib.services.xactn.TransactionService; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.functional.Try; import org.apache.causeway.commons.internal.assertions._Assert; import org.apache.causeway.commons.internal.collections._Lists; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate; @@ -57,7 +65,6 @@ import org.apache.causeway.core.metamodel.object.PackedManagedObject; import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer; -import org.apache.causeway.core.metamodel.services.events.MetamodelEventService; import org.apache.causeway.core.metamodel.services.ixn.InteractionDtoFactory; import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher; import org.apache.causeway.core.metamodel.services.publishing.ExecutionPublisher; @@ -65,16 +72,9 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.schema.ixn.v2.ActionInvocationDto; -import org.jspecify.annotations.NonNull; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.inject.Provider; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled; + import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -87,24 +87,44 @@ @Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".MemberExecutorServiceDefault") @Priority(PriorityPrecedence.EARLY) @Qualifier("Default") -@RequiredArgsConstructor(onConstructor_ = {@Inject}) @Slf4j public class MemberExecutorServiceDefault implements MemberExecutorService { - private final @Getter InteractionLayerTracker interactionLayerTracker; - private final @Getter CausewayConfiguration configuration; - private final @Getter ObjectManager objectManager; - private final @Getter ClockService clockService; - private final @Getter DeadlockRecognizer deadlockRecognizer; - private final @Getter ServiceInjector serviceInjector; - private final @Getter Provider metricsServiceProvider; - private final @Getter InteractionDtoFactory interactionDtoFactory; - private final @Getter Provider executionPublisherProvider; - private final @Getter MetamodelEventService metamodelEventService; - private final @Getter TransactionService transactionService; - private final @Getter RepositoryService repositoryService; + private final InteractionLayerTracker interactionLayerTracker; + private final CausewayConfiguration configuration; + private final ObjectManager objectManager; + private final ClockService clockService; + private final DeadlockRecognizer deadlockRecognizer; + private final Provider metricsServiceProvider; + private final InteractionDtoFactory interactionDtoFactory; + private final Provider executionPublisherProvider; + private final TransactionService transactionService; private final Provider commandPublisherProvider; + private final ObservationProvider observationProvider; + + @Inject + MemberExecutorServiceDefault(final InteractionLayerTracker interactionLayerTracker, + final CausewayConfiguration configuration, final ObjectManager objectManager, final ClockService clockService, + final DeadlockRecognizer deadlockRecognizer, + final Provider metricsServiceProvider, final InteractionDtoFactory interactionDtoFactory, + final Provider executionPublisherProvider, + final TransactionService transactionService, + final Provider commandPublisherProvider, + final CausewayObservationIntegration observationIntegration) { + this.interactionLayerTracker = interactionLayerTracker; + this.configuration = configuration; + this.objectManager = objectManager; + this.clockService = clockService; + this.deadlockRecognizer = deadlockRecognizer; + this.metricsServiceProvider = metricsServiceProvider; + this.interactionDtoFactory = interactionDtoFactory; + this.executionPublisherProvider = executionPublisherProvider; + this.transactionService = transactionService; + this.commandPublisherProvider = commandPublisherProvider; + this.observationProvider = observationIntegration.provider(getClass(), + CausewayObservationIntegration.withModuleName(CausewayModuleCoreRuntimeServices.NAMESPACE)); + } private MetricsService metricsService() { return metricsServiceProvider.get(); @@ -124,11 +144,16 @@ public Optional getInteraction() { public ManagedObject invokeAction( final @NonNull ActionExecutor actionExecutor) { - var executionResult = actionExecutor.getInteractionInitiatedBy().isPassThrough() + var executionResult = observationProvider.get("Action Invocation (%s)" + .formatted(actionExecutor.getOwningAction().getFeatureIdentifier())) + //could also add action's args as tags (but potentially sensitive) + //(we do this with Xray, but that is local for debugging only) + .observe(()-> + actionExecutor.getInteractionInitiatedBy().isPassThrough() ? Try.call(()-> invokeActionInternally(actionExecutor)) - : getTransactionService().callWithinCurrentTransactionElseCreateNew(()-> - invokeActionInternally(actionExecutor)); + : transactionService.callWithinCurrentTransactionElseCreateNew(()-> + invokeActionInternally(actionExecutor))); return executionResult .valueAsNullableElseFail(); @@ -139,7 +164,7 @@ private ManagedObject invokeActionInternally( final ObjectAction owningAction = actionExecutor.getOwningAction(); final InteractionHead head = actionExecutor.getHead(); - + final Can argumentAdapters = actionExecutor.getArguments(); final InteractionInitiatedBy interactionInitiatedBy = actionExecutor.getInteractionInitiatedBy(); // final MethodFacade methodFacade, @@ -222,12 +247,17 @@ private ManagedObject invokeActionInternally( public ManagedObject setOrClearProperty( final @NonNull PropertyModifier propertyExecutor) { - var executionResult = propertyExecutor.getInteractionInitiatedBy().isPassThrough() + var executionResult = observationProvider.get("Property Update (%s)" + .formatted(propertyExecutor.getOwningProperty().getFeatureIdentifier())) + //could also add property's old and new value as tags (but potentially sensitive) + //(we do this with Xray, but that is local for debugging only) + .observe(()-> + propertyExecutor.getInteractionInitiatedBy().isPassThrough() ? Try.call(()-> setOrClearPropertyInternally(propertyExecutor)) - : getTransactionService() + : transactionService .callWithinCurrentTransactionElseCreateNew(() -> - setOrClearPropertyInternally(propertyExecutor)); + setOrClearPropertyInternally(propertyExecutor))); return executionResult .valueAsNullableElseFail(); @@ -275,18 +305,17 @@ private ManagedObject setOrClearPropertyInternally( // TODO: should also sync DTO's 'threw' attribute here...? var executionExceptionIfAny = priorExecution.getThrew(); - if(executionExceptionIfAny != null) { - throw executionExceptionIfAny instanceof RuntimeException - ? ((RuntimeException)executionExceptionIfAny) + if(executionExceptionIfAny != null) + throw executionExceptionIfAny instanceof RuntimeException r + ? r : new RuntimeException(executionExceptionIfAny); - } // publish (if not a contributed association, query-only mixin) if (ExecutionPublishingFacet.isPublishingEnabled(propertyModifier.getFacetHolder())) { executionPublisher().publishPropertyEdit(priorExecution); } - var result = getObjectManager().adapt(targetPojo); + var result = objectManager.adapt(targetPojo); _Xray.exitInvocation(xrayHandle); return result; } @@ -307,21 +336,18 @@ private Object invokeMethodPassThrough( private void setCommandResultIfEntity( final Command command, final ManagedObject resultAdapter) { - if(command.getResult() != null) { + if(command.getResult() != null) // don't trample over any existing result, eg subsequent mixins. return; - } - if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) { + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) return; - } var entityState = resultAdapter.getEntityState(); - if(!entityState.isPersistable()) { + if(!entityState.isPersistable()) return; - } if(entityState.isHollow() || entityState.isDetached()) { // ensure that any still-to-be-persisted adapters get persisted to DB. - getTransactionService().flushTransaction(); + transactionService.flushTransaction(); } // re-evaluate if(!resultAdapter.getEntityState().hasOid()) { @@ -337,14 +363,12 @@ private ManagedObject resultFilteredHonoringVisibility( final ManagedObject resultAdapter, final InteractionInitiatedBy interactionInitiatedBy) { - if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) { + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) return resultAdapter; - } - if (!getConfiguration().core().metaModel().filterVisibility() - || resultAdapter instanceof PackedManagedObject) { + if (!configuration.core().metaModel().filterVisibility() + || resultAdapter instanceof PackedManagedObject) return resultAdapter; - } return MmVisibilityUtils.isVisible(resultAdapter, interactionInitiatedBy) ? resultAdapter From 3f1232719daceb18806f196ef04395f7bac5cd92 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 27 Mar 2026 19:38:21 +0100 Subject: [PATCH 24/31] CAUSEWAY-3975: observe execution publishing --- .../MemberExecutorServiceDefault.java | 2 ++ .../publish/ExecutionPublisherDefault.java | 35 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java index aa3b687d8e4..6db5cc609c0 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java @@ -146,6 +146,7 @@ public ManagedObject invokeAction( var executionResult = observationProvider.get("Action Invocation (%s)" .formatted(actionExecutor.getOwningAction().getFeatureIdentifier())) + .lowCardinalityKeyValue("causeway.execution.initiatedBy", actionExecutor.getInteractionInitiatedBy().name()) //could also add action's args as tags (but potentially sensitive) //(we do this with Xray, but that is local for debugging only) .observe(()-> @@ -249,6 +250,7 @@ public ManagedObject setOrClearProperty( var executionResult = observationProvider.get("Property Update (%s)" .formatted(propertyExecutor.getOwningProperty().getFeatureIdentifier())) + .lowCardinalityKeyValue("causeway.execution.initiatedBy", propertyExecutor.getInteractionInitiatedBy().name()) //could also add property's old and new value as tags (but potentially sensitive) //(we do this with Xray, but that is local for debugging only) .observe(()-> diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ExecutionPublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ExecutionPublisherDefault.java index 2d45594d25c..c8eb95512d4 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ExecutionPublisherDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ExecutionPublisherDefault.java @@ -22,13 +22,13 @@ import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; -import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.inject.Named; -import org.springframework.beans.factory.annotation.Qualifier; import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.InteractionScope; @@ -38,11 +38,11 @@ import org.apache.causeway.applib.services.publishing.spi.ExecutionSubscriber; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.having.HasEnabling; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.metamodel.services.publishing.ExecutionPublisher; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; -import lombok.RequiredArgsConstructor; - /** * Default implementation of {@link ExecutionPublisher}. * @@ -53,23 +53,28 @@ @Priority(PriorityPrecedence.MIDPOINT) @Qualifier("Default") @InteractionScope -@RequiredArgsConstructor(onConstructor_ = {@Inject}) public class ExecutionPublisherDefault implements ExecutionPublisher { - private final List subscribers; + private final Can enabledSubscribers; private final InteractionLayerTracker iaTracker; + private final ObservationProvider observationProvider; - private Can enabledSubscribers = Can.empty(); /** * this is the reason that this service is @InteractionScope'd */ private final LongAdder suppressionRequestCounter = new LongAdder(); - @PostConstruct - public void init() { - enabledSubscribers = Can.ofCollection(subscribers) + @Inject + public ExecutionPublisherDefault( + final InteractionLayerTracker iaTracker, + final List subscribers, + final CausewayObservationIntegration observationIntegration) { + this.iaTracker = iaTracker; + this.enabledSubscribers = Can.ofCollection(subscribers) .filter(HasEnabling::isEnabled); + this.observationProvider = observationIntegration.provider(getClass(), + CausewayObservationIntegration.withModuleName(CausewayModuleCoreRuntimeServices.NAMESPACE)); } @Override @@ -108,9 +113,13 @@ private void notifySubscribers(final Execution execution) { this::getCannotPublishReason); if(canPublish()) { - for (var subscriber : enabledSubscribers) { - subscriber.onExecution(execution); - } + observationProvider.get("Execution Publishing (subscribers=%d)" + .formatted(enabledSubscribers.size())) + .observe(()->{ + for (var subscriber : enabledSubscribers) { + subscriber.onExecution(execution); + } + }); } _Xray.exitPublishing(handle); From 60d762738293afe1a99381758540d279c4c15f9c Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 28 Mar 2026 08:31:07 +0100 Subject: [PATCH 25/31] CAUSEWAY-3975: observing CausewayInteraction (wip) --- .../session/CausewayInteraction.java | 36 +++++++++++-------- .../execution/InteractionInternal.java | 19 ++++++---- .../MemberExecutorServiceDefault.java | 13 +++++-- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java index f1c3148f307..dae778943e2 100644 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java @@ -100,13 +100,16 @@ public Object execute( final ActionInvocation actionInvocation, final Context context) { - push(actionInvocation); - start(actionInvocation, context); - try { - return executeInternal(memberExecutor, actionInvocation, context); - } finally { - popAndComplete(context.clockService(), context.metricsService()); - } + return context.observationProvider().get("Execute Action Invocation") + .observe(()->{ + push(actionInvocation); + start(actionInvocation, context); + try { + return executeInternal(memberExecutor, actionInvocation, context); + } finally { + popAndComplete(context.clockService(), context.metricsService()); + } + }); } @Override @@ -115,13 +118,16 @@ public Object execute( final PropertyEdit propertyEdit, final Context context) { - push(propertyEdit); - start(propertyEdit, context); - try { - return executeInternal(memberExecutor, propertyEdit, context); - } finally { - popAndComplete(context.clockService(), context.metricsService()); - } + return context.observationProvider().get("Execute Property Edit") + .observe(()->{ + push(propertyEdit); + start(propertyEdit, context); + try { + return executeInternal(memberExecutor, propertyEdit, context); + } finally { + popAndComplete(context.clockService(), context.metricsService()); + } + }); } private > Object executeInternal( @@ -130,7 +136,7 @@ private > Object executeInternal( final Context context) { try { - Object result = memberExecutor.execute(execution); + Object result = context.observationProvider().get("Member Execution").observe(()->memberExecutor.execute(execution)); execution.setReturned(result); return result; } catch (Exception ex) { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java index d39c4a56827..399c8483b9c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java @@ -21,6 +21,8 @@ import java.util.concurrent.Callable; import java.util.concurrent.atomic.LongAdder; +import jakarta.inject.Provider; + import org.jspecify.annotations.NonNull; import org.apache.causeway.applib.services.clock.ClockService; @@ -30,6 +32,7 @@ import org.apache.causeway.applib.services.iactn.PropertyEdit; import org.apache.causeway.applib.services.metrics.MetricsService; import org.apache.causeway.applib.services.wrapper.WrapperFactory; +import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer; import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher; @@ -50,8 +53,13 @@ interface MemberExecutor> { record Context( ClockService clockService, MetricsService metricsService, - CommandPublisher commandPublisher, - DeadlockRecognizer deadlockRecognizer) { + Provider commandPublisherProvider, + DeadlockRecognizer deadlockRecognizer, + ObservationProvider observationProvider) { + + public CommandPublisher commandPublisher() { + return commandPublisherProvider.get(); + } } /** @@ -145,11 +153,10 @@ default int getThenIncrementTransactionSequence() { var priorExecution = getPriorExecution(); var executionExceptionIfAny = getPriorExecution().getThrew(); actionInvocation.setThrew(executionExceptionIfAny); - if(executionExceptionIfAny != null) { - throw executionExceptionIfAny instanceof RuntimeException - ? ((RuntimeException)executionExceptionIfAny) + if(executionExceptionIfAny != null) + throw executionExceptionIfAny instanceof RuntimeException r + ? r : new RuntimeException(executionExceptionIfAny); - } return priorExecution; } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java index 6db5cc609c0..3463613a6c2 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java @@ -48,6 +48,8 @@ import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate; +import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction; +import org.apache.causeway.core.interaction.session.CausewayInteraction; import org.apache.causeway.core.metamodel.commons.CanonicalInvoker; import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; import org.apache.causeway.core.metamodel.execution.ActionExecutor; @@ -102,6 +104,7 @@ public class MemberExecutorServiceDefault private final TransactionService transactionService; private final Provider commandPublisherProvider; private final ObservationProvider observationProvider; + private final InteractionInternal.Context interactionInternalContext; @Inject MemberExecutorServiceDefault(final InteractionLayerTracker interactionLayerTracker, @@ -124,6 +127,11 @@ public class MemberExecutorServiceDefault this.commandPublisherProvider = commandPublisherProvider; this.observationProvider = observationIntegration.provider(getClass(), CausewayObservationIntegration.withModuleName(CausewayModuleCoreRuntimeServices.NAMESPACE)); + this.interactionInternalContext = new InteractionInternal.Context( + clockService, metricsService(), commandPublisherProvider, deadlockRecognizer, + // we are creating an observation provider for CausewayInteraction to use + observationIntegration.provider(CausewayInteraction.class, + CausewayObservationIntegration.withModuleName(CausewayModuleCoreInteraction.NAMESPACE))); } private MetricsService metricsService() { @@ -197,7 +205,7 @@ private ManagedObject invokeActionInternally( interaction, actionId, targetPojo, argumentPojos); // sets up startedAt and completedAt on the execution, also manages the execution call graph - interaction.execute(actionExecutor, actionInvocation, new InteractionInternal.Context(clockService, metricsService(), commandPublisherProvider.get(), deadlockRecognizer)); + interaction.execute(actionExecutor, actionInvocation, interactionInternalContext); // handle any exceptions var priorExecution = interaction.getPriorExecutionOrThrowIfAnyException(actionInvocation); @@ -298,8 +306,7 @@ private ManagedObject setOrClearPropertyInternally( var propertyEdit = new PropertyEdit(interaction, propertyId, target, argValuePojo); // sets up startedAt and completedAt on the execution, also manages the execution call graph - var targetPojo = interaction.execute(propertyModifier, propertyEdit, - new InteractionInternal.Context(clockService, metricsService(), commandPublisherProvider.get(), deadlockRecognizer)); + var targetPojo = interaction.execute(propertyModifier, propertyEdit, interactionInternalContext); // handle any exceptions final Execution priorExecution = interaction.getPriorExecution(); From 422f4a7ac0f5ea1c7aee56242ee4bf9a9d7d8858 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 28 Mar 2026 18:11:10 +0100 Subject: [PATCH 26/31] CAUSEWAY-3975: moves CausewayObservationIntegration to config, so we can access applib classes --- api/applib/src/main/java/module-info.java | 1 + .../services/iactnlayer/InteractionLayer.java | 9 +- .../iactnlayer/InteractionLayerStack.java | 6 +- commons/pom.xml | 5 - commons/src/main/java/module-info.java | 1 - .../observation/ObservationClosure.java | 96 +++++++++++++++++++ core/config/pom.xml | 5 + core/config/src/main/java/module-info.java | 4 + .../core/config/CausewayModuleCoreConfig.java | 4 +- .../CausewayObservationDeactivated.java | 13 +++ .../CausewayObservationIntegration.java | 69 +------------ .../execution/InteractionInternal.java | 2 +- .../services/init/MetamodelInitializer.java | 4 +- .../MemberExecutorServiceDefault.java | 4 +- .../publish/ExecutionPublisherDefault.java | 4 +- .../session/InteractionServiceDefault.java | 4 +- .../runtimeservices/session/_Observation.java | 18 ++-- .../transaction/TransactionServiceSpring.java | 6 +- .../core/webapp/CausewayModuleCoreWebapp.java | 2 +- .../integration/entity/JpaEntityFacet.java | 6 +- .../viewer/integration/RequestCycle2.java | 2 +- .../integration/TelemetryStartHandler.java | 4 +- .../wicketapp/CausewayWicketApplication.java | 2 +- 23 files changed, 164 insertions(+), 107 deletions(-) create mode 100644 commons/src/main/java/org/apache/causeway/commons/internal/observation/ObservationClosure.java create mode 100644 core/config/src/main/java/org/apache/causeway/core/config/observation/CausewayObservationDeactivated.java rename {commons/src/main/java/org/apache/causeway/commons/internal => core/config/src/main/java/org/apache/causeway/core/config}/observation/CausewayObservationIntegration.java (79%) diff --git a/api/applib/src/main/java/module-info.java b/api/applib/src/main/java/module-info.java index 7d97d39ab78..99aa89988b8 100644 --- a/api/applib/src/main/java/module-info.java +++ b/api/applib/src/main/java/module-info.java @@ -146,6 +146,7 @@ requires transitive spring.web; requires spring.tx; requires org.slf4j; + requires micrometer.observation; // JAXB viewmodels opens org.apache.causeway.applib.annotation; diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java index 3f65a55eb20..35fb519407d 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayer.java @@ -21,7 +21,7 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; +import org.apache.causeway.commons.functional.Try; /** * Binds an {@link Interaction} ("what" is being executed) with @@ -48,7 +48,10 @@ public record InteractionLayer( * WHEN and WHERE. */ InteractionContext interactionContext, - ObservationClosure observationClosure) implements AutoCloseable { + /** + * @since 4.0 + */ + Runnable onCloseCallback) implements AutoCloseable { public boolean isRoot() { return parent==null; @@ -72,7 +75,7 @@ public InteractionLayer rootLayer() { @Override public void close() { - observationClosure.close(); + Try.run(onCloseCallback::run); // ignores exceptions } public void closeAll() { diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java index 96faddac1c8..88aa872ef62 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java @@ -24,7 +24,7 @@ import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure; +import org.apache.causeway.commons.internal.observation.ObservationClosure; import io.micrometer.observation.Observation; @@ -45,7 +45,9 @@ public InteractionLayer push( final Observation observation) { var parent = currentLayer().orElse(null); @SuppressWarnings("resource") - var newLayer = new InteractionLayer(parent, interaction, interactionContext, new ObservationClosure().startAndOpenScope(observation)); + var newLayer = new InteractionLayer(parent, interaction, interactionContext, + // on-close callback + new ObservationClosure().startAndOpenScope(observation)::close); threadLocalLayer.set(newLayer); return newLayer; } diff --git a/commons/pom.xml b/commons/pom.xml index d80521cdb59..18546295a83 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -144,11 +144,6 @@ org.springframework.boot spring-boot-starter - - - io.micrometer - micrometer-tracing - diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java index 12dadbb4686..47a24ab13b0 100644 --- a/commons/src/main/java/module-info.java +++ b/commons/src/main/java/module-info.java @@ -70,7 +70,6 @@ requires transitive tools.jackson.module.jakarta.xmlbind; requires transitive micrometer.commons; requires transitive micrometer.observation; - requires transitive micrometer.tracing; requires transitive org.jdom2; requires transitive org.jspecify; requires transitive org.jsoup; diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/observation/ObservationClosure.java b/commons/src/main/java/org/apache/causeway/commons/internal/observation/ObservationClosure.java new file mode 100644 index 00000000000..28f64f1d763 --- /dev/null +++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/ObservationClosure.java @@ -0,0 +1,96 @@ +/* + * 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.commons.internal.observation; + +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +import lombok.Getter; +import lombok.experimental.Accessors; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Scope; + +/** + * Helps if start and stop of an {@link Observation} happen in different code locations. + */ +@Getter @Accessors(fluent = true) +public final class ObservationClosure implements AutoCloseable { + + public static final KeyValue DISCARD_KEY = KeyValue.of("causeway.discard", ""); + + private Observation observation; + private Scope scope; + + public ObservationClosure startAndOpenScope(final Observation observation) { + if(observation==null) return this; + this.observation = observation.start(); + this.scope = observation.openScope(); + return this; + } + + @Override + public void close() { + if(observation==null) return; + if(scope!=null) { + this.scope.close(); + this.scope = null; + } + observation.stop(); + } + + public void onError(final Exception ex) { + if(observation==null) return; + // scope lifecycle terminates before exception handling + if(scope!=null) { + this.scope.close(); + this.scope = null; + } + observation.error(ex); + } + + public ObservationClosure tag(final String key, @Nullable final Supplier valueSupplier) { + if(observation==null || valueSupplier == null) return this; + try { + observation.highCardinalityKeyValue(key, "" + valueSupplier.get()); + } catch (Exception e) { + observation.highCardinalityKeyValue(key, "EXCEPTION: " + e.getMessage()); + } + return this; + } + + public void discard() { + discard(this.observation); + close(); + } + + // -- UTILITY + + /** + * Denies span export, in collaboration with a Spring registered {@link DiscardedSpanExportingPredicate}. + */ + public static void discard(@Nullable final Observation obs) { + if(obs == null) + return; + obs.lowCardinalityKeyValue(DISCARD_KEY); + } + +} diff --git a/core/config/pom.xml b/core/config/pom.xml index fe7041fef7d..1df34128425 100644 --- a/core/config/pom.xml +++ b/core/config/pom.xml @@ -91,6 +91,11 @@ org.hibernate.validator hibernate-validator + + + io.micrometer + micrometer-tracing +