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 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