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..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
@@ -18,36 +18,68 @@
*/
package org.apache.causeway.applib.services.iactnlayer;
+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
* 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,
+ ObservationClosure observationClosure) implements AutoCloseable {
+
+ 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;
+ }
+
+ @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
new file mode 100644
index 00000000000..9a1b7b69fdb
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/iactnlayer/InteractionLayerStack.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure;
+
+import io.micrometer.observation.Observation;
+
+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,
+ final Observation observation) {
+ var parent = currentLayer().orElse(null);
+ @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();
+ }
+
+ 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();
+ if(current==null) return null;
+
+ var newTop = current.parent();
+ current.close();
+ return set(newTop);
+ }
+
+ 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..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
@@ -20,11 +20,11 @@
import java.util.concurrent.Callable;
+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.
*
diff --git a/bom/pom.xml b/bom/pom.xml
index 1d7abd049f0..cd8c95f4445 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..47a24ab13b0 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,8 @@
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;
requires transitive org.jsoup;
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/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..7248a116165
--- /dev/null
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationInternal.java
@@ -0,0 +1,133 @@
+/*
+ * 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 java.util.function.Supplier;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.StringUtils;
+
+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;
+
+/**
+ * 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)
+ .lowCardinalityKeyValue("bean", bean.getSimpleName());
+ }
+
+ @FunctionalInterface
+ public interface ObservationProvider {
+ Observation get(String name);
+ }
+
+ 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 final 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;
+ }
+
+ }
+
+ public static KeyValue currentThreadId() {
+ var ct = Thread.currentThread();
+ return KeyValue.of("threadId", "%d [%s]".formatted(ct.getId(), ct.getName()));
+
+ }
+
+}
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/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..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
@@ -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;
@@ -65,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;
@@ -116,7 +119,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,9 +205,12 @@
// standalone validators
LogicalTypeMalformedValidator.class,
-
+
// menubar contributions
- MetamodelInspectMenu.class
+ MetamodelInspectMenu.class,
+
+ //last
+ MetamodelInitializer.class
})
public class CausewayModuleCoreMetamodel {
@@ -261,4 +269,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/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/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..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
@@ -18,10 +18,8 @@
*/
package org.apache.causeway.core.runtimeservices.session;
-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;
@@ -30,44 +28,40 @@
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;
-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.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;
+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;
-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.base._Casts;
-import org.apache.causeway.commons.internal.concurrent._ConcurrentContext;
-import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList;
+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;
+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;
@@ -88,13 +82,9 @@ 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 MetamodelEventService runtimeEventService;
- final Provider specificationLoaderProvider;
+ final ObservationProvider observationProvider;
final ServiceInjector serviceInjector;
final ClockService clockService;
@@ -106,9 +96,12 @@ public class InteractionServiceDefault
final InteractionIdGenerator interactionIdGenerator;
+ @SuppressWarnings("exports")
@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,59 +109,19 @@ public InteractionServiceDefault(
final Provider commandPublisherProvider,
final ConfigurableBeanFactory beanFactory,
final InteractionIdGenerator interactionIdGenerator) {
- this.runtimeEventService = runtimeEventService;
- this.specificationLoaderProvider = specificationLoaderProvider;
+ this.observationProvider = observation.provider(getClass());
this.serviceInjector = serviceInjector;
this.transactionServiceSpring = transactionServiceSpring;
this.clockService = clockService;
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();
-
- var specificationLoader = specificationLoaderProvider.get();
-
- 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");
- }
- }
-
- runtimeEventService.fireAfterMetamodelLoaded();
-
- }
-
@Override
public int getInteractionLayerCount() {
- return interactionLayerStack.get().size();
+ return layerStack.size();
}
@Override
@@ -179,27 +132,37 @@ public InteractionLayer openInteraction() {
}
@Override
- public InteractionLayer openInteraction(
- final @NonNull InteractionContext interactionContextToUse) {
-
- var causewayInteraction = getOrCreateCausewayInteraction();
+ public 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) {
+ if(reuseCurrentLayer)
// we are done, just return the stack's top
- return interactionLayerStack.get().peek();
+ return currentInteractionLayerElseFail();
+
+ var causewayInteraction = currentInteractionLayer()
+ .map(InteractionLayer::interaction)
+ .map(it->(CausewayInteraction)it)
+ .orElseGet(()->new CausewayInteraction(interactionIdGenerator.interactionId()));
+
+
+ var obs = observationProvider.get(getInteractionLayerCount()==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());
}
- var interactionLayer = new InteractionLayer(causewayInteraction, interactionContextToUse);
-
- interactionLayerStack.get().push(interactionLayer);
-
if(isAtTopLevel()) {
transactionServiceSpring.onOpen(causewayInteraction);
interactionScopeLifecycleHandler.onTopLevelInteractionOpened();
@@ -208,30 +171,22 @@ public InteractionLayer openInteraction(
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());
//
@@ -243,15 +198,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
@@ -262,7 +214,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);
@@ -283,7 +235,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);
@@ -355,8 +307,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; "
@@ -365,12 +316,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
@@ -435,39 +386,39 @@ 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();
}
}
}
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/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..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
@@ -19,24 +19,27 @@
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;
+import io.micrometer.observation.Observation;
+
/**
* A pass-through implementation, free of side-effects,
* in support of simple JUnit tests.
@@ -44,7 +47,7 @@
public class InteractionService_forTesting
implements InteractionService {
- private Stack interactionLayers = new Stack<>();
+ final InteractionLayerStack layerStack = new InteractionLayerStack();
@Override
public InteractionLayer openInteraction() {
@@ -55,18 +58,17 @@ public InteractionLayer openInteraction() {
@Override
public InteractionLayer openInteraction(final @NonNull InteractionContext interactionContext) {
final Interaction interaction = new Interaction_forTesting();
- return interactionLayers.push(
- new InteractionLayer(interaction, interactionContext));
+ return layerStack.push(interaction, interactionContext, Observation.NOOP);
}
@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 +78,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 +93,7 @@ public R call(final @NonNull InteractionContext interactionContext, final @N
openInteraction(interactionContext);
return callable.call();
} finally {
- interactionLayers.pop();
+ layerStack.pop();
}
}
@@ -101,7 +103,7 @@ public void run(final @NonNull InteractionContext interactionContext, final @Non
openInteraction(interactionContext);
runnable.run();
} finally {
- interactionLayers.pop();
+ layerStack.pop();
}
}
@@ -111,7 +113,7 @@ public void runAnonymous(final @NonNull ThrowingRunnable runnable) {
openInteraction();
runnable.run();
} finally {
- interactionLayers.pop();
+ layerStack.pop();
}
}
@@ -121,7 +123,7 @@ public R callAnonymous(final @NonNull Callable callable) {
openInteraction();
return callable.call();
} finally {
- interactionLayers.pop();
+ layerStack.pop();
}
}
@@ -136,6 +138,6 @@ static class Interaction_forTesting implements Interaction {
@Override public Command getCommand() { return null; }
@Override public Execution, ?> getCurrentExecution() { return null; }
@Override public Execution, ?> getPriorExecution() { return null; }
- };
+ }
}
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..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
@@ -23,9 +23,13 @@
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.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;
import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
@@ -36,7 +40,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 +67,7 @@ public class CausewayModuleCoreWebapp {
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
- public MessageBroker sessionScopedMessageBroker() {
+ MessageBroker sessionScopedMessageBroker() {
return new MessageBrokerImpl();
}
@@ -73,8 +79,29 @@ 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 "%s (%s)".formatted(
+ super.getContextualName(context),
+ _Strings.ellipsifyAtEnd(context.getCarrier().getRequestURI(), 80, "..."));
+ }
+ @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/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..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.wicketapp.CausewayWicketAjaxRequestListenerUtil;
+import org.apache.causeway.viewer.wicket.viewer.integration.RehydrationHandler;
import lombok.AccessLevel;
import lombok.Getter;
@@ -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);
}
}
@@ -352,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, metaModelContext);
+ getRequestCycleListeners().add(new RehydrationHandler());
}
}
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..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
@@ -17,7 +17,6 @@
* under the License.
*
*/
-
package org.apache.causeway.testing.integtestsupport.applib;
import org.junit.jupiter.api.extension.BeforeEachCallback;
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");
+ }
}
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/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/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/RequestCycle2.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java
new file mode 100644
index 00000000000..7bda8be00ba
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/RequestCycle2.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationClosure;
+
+public class RequestCycle2 extends RequestCycle {
+
+ final long startTimeNanos;
+ public final ObservationClosure observationClosure = new ObservationClosure();
+
+ public RequestCycle2(final RequestCycleContext context) {
+ super(context);
+ this.startTimeNanos = System.nanoTime();
+ }
+
+ long millisSinceStart() {
+ return (System.nanoTime() - startTimeNanos)/1000_000;
+ }
+
+}
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..4d1e223f174
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/SessionAuthenticator.java
@@ -0,0 +1,62 @@
+/*
+ * 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("session was not opened (because not authenticated)");
+ return Optional.empty();
+ }
+
+ // impersonation support
+ 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/TelemetryStartHandler.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java
new file mode 100644
index 00000000000..1d5cda6ada8
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStartHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+import org.apache.causeway.commons.internal.observation.CausewayObservationInternal.ObservationProvider;
+
+/**
+ * @since 4.0
+ */
+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"));
+ }
+ }
+
+ @Override
+ public IRequestHandler onException(final RequestCycle requestCycle, final Exception ex) {
+ if (requestCycle instanceof RequestCycle2 requestCycle2) {
+ 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
new file mode 100644
index 00000000000..646bad2c5b3
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/TelemetryStopHandler.java
@@ -0,0 +1,46 @@
+/*
+ * 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) {
+
+ if(requestCycle2.millisSinceStart() > 50) { // avoid clutter
+ requestCycle2.observationClosure.tag("numberEntitiesLoaded", metricsService::numberEntitiesLoaded);
+ requestCycle2.observationClosure.tag("numberEntitiesDirtied", metricsService::numberEntitiesDirtied);
+ }
+
+ requestCycle2.observationClosure.close();
+ }
+ }
+
+}
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..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
@@ -55,15 +55,14 @@
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;
-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 {
@@ -119,19 +122,16 @@ 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;
-
- 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.
@@ -142,55 +142,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");
- }
-
- if(log.isDebugEnabled()) {
- timings.set(_Timing.now());
- }
+ sessionAuthenticator.determineInteractionContext()
+ .ifPresent(interactionService::openInteraction);
}
@Override
@@ -212,19 +168,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()
+ var validationResult = specificationLoader().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());
}
@@ -252,9 +206,6 @@ public void onRequestHandlerResolved(final RequestCycle requestCycle, final IReq
}
}
- if(log.isTraceEnabled()) {
- log.trace("onRequestHandlerResolved out");
- }
}
/**
@@ -273,44 +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);
- }
- }
- }
-
- getMetaModelContext().lookupService(InteractionService.class).ifPresent(
- InteractionService::closeInteractionLayers
- );
- }
-
- @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()) {
@@ -321,8 +242,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.
@@ -334,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);
@@ -436,17 +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()) {
+ && 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,
@@ -477,6 +392,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 extends Page> constructor = signInPageClass.getConstructor(PageParameters.class, ExceptionModel.class);
@@ -499,10 +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) {
@@ -514,26 +430,11 @@ 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;
}
- // -- 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/CausewayWicketAjaxRequestListenerUtil.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java
deleted file mode 100644
index d155e62fd94..00000000000
--- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/CausewayWicketAjaxRequestListenerUtil.java
+++ /dev/null
@@ -1,94 +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.core.metamodel.context.MetaModelContext;
-import org.apache.causeway.viewer.wicket.ui.pages.PageAbstract;
-
-import lombok.experimental.UtilityClass;
-
-@UtilityClass
-public final class CausewayWicketAjaxRequestListenerUtil {
-
- public void setRootRequestMapper(
- final WebApplication app,
- final MetaModelContext commonContext) {
-
- 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) {
- var pageAbstract = (PageAbstract) iRequestablePage;
- 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 6dbfe9c7d07..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
@@ -41,17 +41,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,6 +70,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.RehydrationHandler;
+import org.apache.causeway.viewer.wicket.viewer.integration.RequestCycle2;
+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;
@@ -111,6 +117,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;
@@ -136,11 +146,7 @@ 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();
-
- // intercept AJAX requests and reload view-models so any detached entities are re-fetched
- CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this, metaModelContext);
}
private AjaxRequestTarget decorate(final AjaxRequestTarget ajaxRequestTarget) {
@@ -202,10 +208,14 @@ protected void init() {
.submit(_ConcurrentContext.sequential())
.await();
+ setRequestCycleProvider(RequestCycle2::new);
getRequestCycleSettings().setRenderStrategy(RequestCycleSettings.RenderStrategy.REDIRECT_TO_RENDER);
getResourceSettings().setParentFolderPlaceholder("$up$");
- getRequestCycleListeners().add(createWebRequestCycleListenerForCauseway());
+ getRequestCycleListeners().add(new TelemetryStartHandler(observationInternal));
+ 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
@@ -275,15 +285,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();
@@ -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