This is an automated email from the ASF dual-hosted git repository. danhaywood pushed a commit to branch CAUSEWAY-3654 in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 2755d74bf7b80c138caa5e75f0f76fac431f2818 Author: danhaywood <d...@haywood-associates.co.uk> AuthorDate: Mon Dec 11 15:49:39 2023 +0000 CAUSEWAY-3564: introduces TransactionScope annotation and ... ... reworks InteractionServiceDefault and TransactionServiceSpring to use more of Spring's inbuilt xactn support Specifically, using TransactionSynchronizationManager and TransactionSynchronization to allow suspend/resume (meaning REQUIRES_NEW is supported). In particular, the InteractionAwareTransactionBoundaryHandler service is removed, its functionality moved into TransactionServiceSpring, and also removed TransactionBoundaryAware interface and callbacks - instead, TransactionSynchronization can be used. --- .editorconfig | 2 +- ...ntentNegotiationServiceOrgApacheCausewayV1.adoc | 19 ++ ...tionServiceOrgApacheCausewayV1_usage-notes.adoc | 6 + antora/supplemental-ui/causeway-favicon.png | Bin 0 -> 2143 bytes .../applib/annotation/InteractionScope.java | 14 +- .../applib/annotation/TransactionScope.java | 79 +++++ .../services/xactn/TransactionalProcessor.java | 17 +- core/interaction/src/main/java/module-info.java | 1 - .../interaction/CausewayModuleCoreInteraction.java | 4 +- ...teractionAwareTransactionalBoundaryHandler.java | 175 ---------- .../InteractionScopeBeanFactoryPostProcessor.java | 2 +- .../interaction/scope/StackedTransactionScope.java | 184 ++++++++++ .../TransactionScopeBeanFactoryPostProcessor.java} | 24 +- .../metamodel/objectmanager/ObjectCreator.java | 4 +- .../objectlifecycle/ObjectLifecyclePublisher.java | 4 + .../specloader/specimpl/IntrospectionState.java} | 48 +-- .../core/runtime/CausewayModuleCoreRuntime.java | 2 - .../runtime/events/TransactionEventEmitter.java | 67 ---- .../apache/causeway/core/runtime/events/_Xray.java | 2 +- .../runtimeservices/src/main/java/module-info.java | 3 +- .../EntityPropertyChangePublisherDefault.java | 4 +- .../publish/ObjectLifecyclePublisherDefault.java | 2 + .../session/InteractionServiceDefault.java | 38 +-- .../transaction/TransactionServiceSpring.java | 218 ++++++++++-- core/transaction/src/main/java/module-info.java | 3 +- ...Event.java => TransactionCompletionStatus.java} | 31 +- .../integtests/OutboxRestClient_E2eTest.java | 6 +- .../spiimpl/SessionSubscriberForSessionLog.java | 19 +- persistence/commons/src/main/java/module-info.java | 3 +- .../commons/CausewayModulePersistenceCommons.java | 1 - .../changetracking/EntityChangeTrackerDefault.java | 118 ++----- .../CausewayModulePersistenceJdoDatanucleus.java | 1 + .../metamodel/facets/entity/JdoEntityFacet.java | 1 + ...actionRollbackTest_usingInteractionService.java | 2 - ...actionRollbackTest_usingTransactionService.java | 2 - .../testdomain/conf/Configuration_headless.java | 1 - .../publishing/PublishingTestFactoryAbstract.java | 2 - .../util/interaction/InteractionBoundaryProbe.java | 3 - .../rename-all-published-sources-to-causeway.jsh | 380 +++++++++++++++++++++ .../branding/BrandingUiServiceDefault.java | 2 - ...r.restfulobjects.applib.client.UriBuilderPlugin | 1 - ...ntentNegotiationServiceOrgApacheCausewayV1.java | 108 ++++++ .../viewer/src/main/java/module-info.java | 3 +- 43 files changed, 1137 insertions(+), 469 deletions(-) diff --git a/.editorconfig b/.editorconfig index f7644a1dfb..a806c86944 100644 --- a/.editorconfig +++ b/.editorconfig @@ -112,7 +112,7 @@ ij_java_for_statement_wrap = off ij_java_generate_final_locals = false ij_java_generate_final_parameters = false ij_java_if_brace_force = never -ij_java_imports_layout = java.**,|,$java.**,|,javax.**,|,$javax.**,|,com.**,|,$com.**,|,org.**,|,$org.**,|,org.springframework.**,|,$org.springframework.**,|,org.apache.causeway.**,|,$org.apache.causeway.**,|,lombok.**,lombok.experimental.**,lombok.extern.log4j.**,lombok.extern.**,|,demoapp.**,|,*,|,$* +ij_java_imports_layout = java.**,|,$java.**,|,javax.**,|,$javax.**,|,com.**,|,$com.**,|,org.**,|,$org.**,|,org.springframework.**,|,$org.springframework.**,|,org.apache.causeway.**,|,$org.apache.causeway.**,|,org.apache.isis.**,|,$org.apache.isis.**,|,lombok.**,lombok.experimental.**,lombok.extern.log4j.**,lombok.extern.**,|,demoapp.**,|,*,|,$* ij_java_indent_case_from_switch = true ij_java_insert_inner_class_imports = false ij_java_insert_override_annotation = true diff --git a/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc new file mode 100644 index 0000000000..6965204c15 --- /dev/null +++ b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc @@ -0,0 +1,19 @@ += ContentNegotiationServiceOrgApacheIsisV1 +:Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...] + +== API + +[source,java] +.ContentNegotiationServiceOrgApacheIsisV1.java +---- +class ContentNegotiationServiceOrgApacheIsisV1 { + public static final String ACCEPT_PROFILE; + Response.ResponseBuilder buildResponse(IResourceContext resourceContext, ManagedObject objectAdapter) + Response.ResponseBuilder buildResponse(IResourceContext resourceContext, ManagedProperty objectAndProperty) + Response.ResponseBuilder buildResponse(IResourceContext resourceContext, ManagedCollection managedCollection) + Response.ResponseBuilder buildResponse(IResourceContext resourceContext, ManagedAction objectAndAction) + Response.ResponseBuilder buildResponse(IResourceContext resourceContext, ObjectAndActionInvocation objectAndActionInvocation) +} +---- + +include::hooks/ContentNegotiationServiceOrgApacheIsisV1_usage-notes.adoc[] diff --git a/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc new file mode 100644 index 0000000000..5dd9b62ebc --- /dev/null +++ b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc @@ -0,0 +1,6 @@ +:Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...] + + +This service returns xref:vro:ROOT:content-negotiation/apache-causeway-v2-profile.adoc[simplified representations] + + diff --git a/antora/supplemental-ui/causeway-favicon.png b/antora/supplemental-ui/causeway-favicon.png new file mode 100644 index 0000000000..258658996e Binary files /dev/null and b/antora/supplemental-ui/causeway-favicon.png differ diff --git a/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java b/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java index 875f16a2ae..894b347fa0 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java @@ -25,6 +25,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.annotation.AliasFor; /** * {@code @InteractionScope} is a specialization of {@link Scope @Scope} for a @@ -40,6 +42,7 @@ import org.springframework.context.annotation.Scope; * @since 2.0 {@index} * @see org.springframework.web.context.annotation.SessionScope * @see org.springframework.web.context.annotation.ApplicationScope + * @see TransactionScope * @see org.springframework.context.annotation.Scope * @see org.springframework.stereotype.Component * @see org.springframework.context.annotation.Bean @@ -47,7 +50,16 @@ import org.springframework.context.annotation.Scope; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented -@Scope("interaction") +@Scope(InteractionScope.SCOPE_NAME) public @interface InteractionScope { + String SCOPE_NAME = "interaction"; + + /** + * Alias for {@link Scope#proxyMode}. + * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/annotation/TransactionScope.java b/api/applib/src/main/java/org/apache/causeway/applib/annotation/TransactionScope.java new file mode 100644 index 0000000000..681a2c17c0 --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/annotation/TransactionScope.java @@ -0,0 +1,79 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.annotation.AliasFor; + +/** + * {@code @TransactionScope} is a specialization of {@link Scope @Scope} for a + * service or component whose lifecycle is bound to the current top-level transaction, + * within an outer {@link InteractionScope interaction}. + * + * <p>Such services should additional implement Spring's + * {@link org.springframework.transaction.support.TransactionSynchronization} interface, defining the transaction + * lifecycle callbacks. + * + * <p>Specifically, {@code @TransactionScope} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @Scope("transaction")}. + * + * <p>{@code @TransactionScope} may be used as a meta-annotation to create custom + * composed annotations. + * + * <p> Note that (apparently) the {@link org.springframework.transaction.support.TransactionSynchronization} + * infrastructure is only really intended to work with a single {@link org.springframework.transaction.PlatformTransactionManager}. + * And indeed, this is going to be typical case. However, our framework code does at least admit the possibility of + * multiple {@link org.springframework.transaction.PlatformTransactionManager}s being defined in the app. If that is + * the case, then (I believe) the callbacks of {@link org.springframework.transaction.support.TransactionSynchronization} might + * be called multiple times, once per {@link org.springframework.transaction.PlatformTransactionManager}. The framework + * currently doesn't provide any way to distinguish between these calls. + * + * @since 2.0 {@index} + * @see org.springframework.web.context.annotation.SessionScope + * @see org.springframework.web.context.annotation.ApplicationScope + * @see InteractionScope + * @see Scope + * @see org.springframework.stereotype.Component + * @see org.springframework.context.annotation.Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Scope(TransactionScope.SCOPE_NAME) +public @interface TransactionScope { + + String SCOPE_NAME = "transaction"; + + /** + * Proxying <i>must</i> be enabled, because we inject {@link TransactionScope}d beans + * into beans with wider scopes. + * + * <p>Alias for {@link Scope#proxyMode}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java b/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java index ebd70f2d79..868f0252f7 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java @@ -44,6 +44,10 @@ public interface TransactionalProcessor { /** * Runs given {@code callable} with a transactional boundary, where the detailed transactional behavior * is governed by given {@link TransactionDefinition} {@code def}. + * + * @param def - transaction definition, in particular whether to use existing or start new transaction. Requires only a single {@link org.springframework.transaction.PlatformTransactionManager} to be configured (unless a {@link org.springframework.transaction.support.TransactionTemplate} is provided which wraps a specific {@link org.springframework.transaction.PlatformTransactionManager}. + * @param callable - the work to be performed within the transaction. + * @return {@link Try} of calling given {@code callable} * @return {@link Try} of calling given {@code callable} */ <T> Try<T> callTransactional(TransactionDefinition def, Callable<T> callable); @@ -51,6 +55,10 @@ public interface TransactionalProcessor { /** * Runs given {@code runnable} with a transactional boundary, where the detailed transactional behavior * is governed by given {@link TransactionDefinition} {@code def}. + * + * @param def - transaction definition, in particular whether to use existing or start new transaction. Requires only a single {@link org.springframework.transaction.PlatformTransactionManager} to be configured (unless a {@link org.springframework.transaction.support.TransactionTemplate} is provided which wraps a specific {@link org.springframework.transaction.PlatformTransactionManager}. + * @param runnable - the work to be performed within the transaction. + * @return {@link Try} of calling given {@code callable} */ default Try<Void> runTransactional(final TransactionDefinition def, final ThrowingRunnable runnable) { return callTransactional(def, ThrowingRunnable.toCallable(runnable)); @@ -61,8 +69,9 @@ public interface TransactionalProcessor { /** * Runs given {@code callable} with a transactional boundary, where the detailed transactional behavior * is governed by given {@link Propagation} {@code propagation}. - * <p> - * More fine grained control is given via {@link #callTransactional(TransactionDefinition, Callable)} + * + * @param propagation - transaction propagation, ie whether to use existing or start new transaction. Requires only a single {@link org.springframework.transaction.PlatformTransactionManager} to be configured. For more control, use {@link #callTransactional(TransactionDefinition, Callable)} and pass in a {@link org.springframework.transaction.support.TransactionTemplate} is provided which wraps a specific {@link org.springframework.transaction.PlatformTransactionManager}. + * @param callable - the work to be performed within the transaction. * @return {@link Try} of calling given {@code callable} */ default <T> Try<T> callTransactional(final Propagation propagation, final Callable<T> callable) { @@ -77,6 +86,10 @@ public interface TransactionalProcessor { * <p> * More fine grained control is given via * {@link #runTransactional(TransactionDefinition, ThrowingRunnable)} + * + * @param propagation - transaction propagation, ie whether to use existing or start new transaction. Requires only a single {@link org.springframework.transaction.PlatformTransactionManager} to be configured. For more control, use {@link #callTransactional(TransactionDefinition, Callable)} and pass in a {@link org.springframework.transaction.support.TransactionTemplate} is provided which wraps a specific {@link org.springframework.transaction.PlatformTransactionManager}. + * @param runnable - the work to be performed within the transaction. + * @return {@link Try} of calling given {@code callable} */ default Try<Void> runTransactional(final Propagation propagation, final ThrowingRunnable runnable) { return callTransactional(propagation, ThrowingRunnable.toCallable(runnable)); diff --git a/core/interaction/src/main/java/module-info.java b/core/interaction/src/main/java/module-info.java index 21e6719613..c13a521cc3 100644 --- a/core/interaction/src/main/java/module-info.java +++ b/core/interaction/src/main/java/module-info.java @@ -18,7 +18,6 @@ */ module org.apache.causeway.core.interaction { exports org.apache.causeway.core.interaction; - exports org.apache.causeway.core.interaction.integration; exports org.apache.causeway.core.interaction.scope; exports org.apache.causeway.core.interaction.session; diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java index c915622590..13ff6e5897 100644 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java @@ -21,14 +21,14 @@ package org.apache.causeway.core.interaction; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.apache.causeway.core.interaction.integration.InteractionAwareTransactionalBoundaryHandler; +import org.apache.causeway.core.interaction.scope.TransactionScopeBeanFactoryPostProcessor; import org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor; @Configuration @Import({ InteractionScopeBeanFactoryPostProcessor.class, - InteractionAwareTransactionalBoundaryHandler.class + TransactionScopeBeanFactoryPostProcessor.class, }) public class CausewayModuleCoreInteraction { diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java deleted file mode 100644 index d5164556c8..0000000000 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java +++ /dev/null @@ -1,175 +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.core.interaction.integration; - -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; - -import javax.annotation.Priority; -import javax.inject.Inject; -import javax.inject.Named; - -import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionTemplate; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.functional.ThrowingRunnable; -import org.apache.causeway.commons.internal.collections._Lists; -import org.apache.causeway.commons.internal.debug._Probe; -import org.apache.causeway.core.interaction.session.CausewayInteraction; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Value; -import lombok.val; -import lombok.extern.log4j.Log4j2; - -@Service -@Named(InteractionAwareTransactionalBoundaryHandler.LOGICAL_TYPE_NAME) -@Priority(PriorityPrecedence.MIDPOINT) -@Qualifier("Default") -@Log4j2 -public class InteractionAwareTransactionalBoundaryHandler { - - public static final String LOGICAL_TYPE_NAME = CausewayModuleCoreInteraction.NAMESPACE + ".InteractionAwareTransactionalBoundaryHandler"; - - private final Can<PlatformTransactionManager> txManagers; - - @Inject - public InteractionAwareTransactionalBoundaryHandler(final List<PlatformTransactionManager> txManagers) { - this.txManagers = Can.ofCollection(txManagers); - } - - // -- OPEN - - public void onOpen(final @NonNull CausewayInteraction interaction) { - - if (log.isDebugEnabled()) { - log.debug("opening on {}", _Probe.currentThreadId()); - } - - if(txManagers.isEmpty()) { - return; // nothing to do - } - - val onCloseTasks = _Lists.<CloseTask>newArrayList(txManagers.size()); - interaction.putAttribute(OnCloseHandle.class, new OnCloseHandle(onCloseTasks)); - - txManagers.forEach(txManager->newTransactionOrParticipateInExisting(txManager, onCloseTasks::add)); - - } - - // -- CLOSE - - public void onClose(final @NonNull CausewayInteraction interaction) { - - if (log.isDebugEnabled()) { - log.debug("closing on {}", _Probe.currentThreadId()); - } - - if(txManagers.isEmpty()) { - return; // nothing to do - } - - Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class)) - .ifPresent(OnCloseHandle::runOnCloseTasks); - - } - - public void requestRollback(final @NonNull CausewayInteraction interaction) { - Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class)) - .ifPresent(OnCloseHandle::requestRollback); - } - - // -- HELPER - - private void newTransactionOrParticipateInExisting( - final PlatformTransactionManager txManager, - final Consumer<CloseTask> onNewCloseTask) { - - val txTemplate = new TransactionTemplate(txManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - - // either participate in existing or create new transaction - val txStatus = txManager.getTransaction(txTemplate); - if(txStatus==null // in support of JUnit testing (TransactionManagers might be mocked or hollow stubs) - || !txStatus.isNewTransaction()) { - // we are participating in an exiting transaction (or testing), nothing to do - return; - } - - // we have created a new transaction, so need to provide a CloseTask - - onNewCloseTask.accept( - new CloseTask( - txStatus, - txManager.getClass().getName(), // info to be used for display in case of errors - ()->{ - - if(txStatus.isRollbackOnly()) { - txManager.rollback(txStatus); - } else { - txManager.commit(txStatus); - } - - })); - } - - @Value - private static class CloseTask { - private final @NonNull TransactionStatus txStatus; - private final @NonNull String onErrorInfo; - private final @NonNull ThrowingRunnable runnable; - } - - @RequiredArgsConstructor - private static class OnCloseHandle { - private final @NonNull List<CloseTask> onCloseTasks; - void requestRollback() { - onCloseTasks.forEach(onCloseTask->{ - onCloseTask.txStatus.setRollbackOnly(); - }); - } - void runOnCloseTasks() { - onCloseTasks.forEach(onCloseTask->{ - - try { - onCloseTask.getRunnable().run(); - } catch(final Throwable ex) { - // ignore - log.error( - "failed to close transactional boundary using transaction-manager {}; " - + "continuing to avoid memory leakage", - onCloseTask.getOnErrorInfo(), - ex); - } - - }); - } - } - - -} diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java index 02c0a4d07e..1b9ea5b7b6 100644 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java @@ -32,7 +32,7 @@ import lombok.val; @Component public class InteractionScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor { - public static final String SCOPE_NAME = "interaction"; + public static final String SCOPE_NAME = org.apache.causeway.applib.annotation.InteractionScope.SCOPE_NAME; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java new file mode 100644 index 0000000000..80ca608509 --- /dev/null +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java @@ -0,0 +1,184 @@ + +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed 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 + * + * https://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.interaction.scope; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Stack; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class StackedTransactionScope implements Scope { + + + @Override + public Object get(String name, ObjectFactory<?> objectFactory) { + + Object key = currentKeyOnTransactionStack(); + + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(key); + if (scopedObjects == null) { + scopedObjects = new ScopedObjectsHolder(); + TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization(scopedObjects)); + TransactionSynchronizationManager.bindResource(key, scopedObjects); + } + // NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details, + // see https://github.com/spring-projects/spring-framework/issues/25801. + Object scopedObject = scopedObjects.scopedInstances.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + scopedObjects.scopedInstances.put(name, scopedObject); + } + return scopedObject; + } + + @Override + @Nullable + public Object remove(String name) { + Object key = currentKeyOnTransactionStack(); + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(key); + if (scopedObjects != null) { + scopedObjects.destructionCallbacks.remove(name); + return scopedObjects.scopedInstances.remove(name); + } + else { + return null; + } + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(this); + if (scopedObjects != null) { + scopedObjects.destructionCallbacks.put(name, callback); + } + } + + private static final ThreadLocal<Stack<Object>> transactionStackThreadLocal = ThreadLocal.withInitial(() -> { + Stack<Object> stack = new Stack<>(); + stack.push(new Object()); + return stack; + }); + + /** + * Maintains a stack of keys, where the top-most is the key managed by {@link TransactionSynchronizationManager} + * holding the {@link ScopedObjectsHolder} for the current transaction. + * + * <p> + * If a transaction is suspended, then the {@link CleanupSynchronization#suspend() suspend} callback is used + * to pop a new key onto the stack, unbinding the previous key's resources (in other words, the + * {@link org.apache.causeway.applib.annotation.TransactionScope transaction-scope}d beans of the suspended + * transaction) from {@link TransactionSynchronizationManager}. As transaction-scoped beans are then resolved, + * they will be associated with the new key. + * </p> + * + * <p> + * Conversely, when a tranaction is resumed, then the process is reversed; the old key is popped, and the previous + * key is rebound to the {@link TransactionSynchronizationManager}, meaning that the previous transaction's + * {@link org.apache.causeway.applib.annotation.TransactionScope transaction-scope}d beans are brought back. + * </p> + * + * @see #currentKeyOnTransactionStack() + * @see #pushNewKeyOntoTransactionStack() + * @see #popOldKeyFromTransactionStack() + * @see #transactionStackThreadLocal + */ + private static Stack<Object> transactionStack() { + return transactionStackThreadLocal.get(); + } + /** + * @see #transactionStack() + */ + private Object currentKeyOnTransactionStack() { + return transactionStack().peek(); + } + /** + * @see #transactionStack() + */ + private static void pushNewKeyOntoTransactionStack() { + transactionStack().push(new Object()); + } + /** + * @see #transactionStack() + */ + private static void popOldKeyFromTransactionStack() { + transactionStack().pop(); + } + + + @Override + @Nullable + public Object resolveContextualObject(String key) { + return null; + } + + @Override + @Nullable + public String getConversationId() { + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + + /** + * Holder for scoped objects. + */ + static class ScopedObjectsHolder { + + final Map<String, Object> scopedInstances = new HashMap<>(); + + final Map<String, Runnable> destructionCallbacks = new LinkedHashMap<>(); + } + + + private class CleanupSynchronization implements TransactionSynchronization { + + private final ScopedObjectsHolder scopedObjects; + + public CleanupSynchronization(ScopedObjectsHolder scopedObjects) { + this.scopedObjects = scopedObjects; + } + + @Override + public void suspend() { + TransactionSynchronizationManager.unbindResource(currentKeyOnTransactionStack()); + pushNewKeyOntoTransactionStack(); // subsequent calls to obtain a @TransactionScope'd bean will be against this key + } + + @Override + public void resume() { + popOldKeyFromTransactionStack(); // the now-completed transaction's @TransactionScope'd beans are no longer required, and will be GC'd. + TransactionSynchronizationManager.bindResource(currentKeyOnTransactionStack(), this.scopedObjects); + } + + @Override + public void afterCompletion(int status) { + TransactionSynchronizationManager.unbindResourceIfPossible(StackedTransactionScope.this.currentKeyOnTransactionStack()); + for (Runnable callback : this.scopedObjects.destructionCallbacks.values()) { + callback.run(); + } + this.scopedObjects.destructionCallbacks.clear(); + this.scopedObjects.scopedInstances.clear(); + } + } + +} diff --git a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java similarity index 50% rename from core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java rename to core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java index 8bad9c8f54..51deb40f56 100644 --- a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java +++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java @@ -16,17 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.causeway.core.transaction.events; +package org.apache.causeway.core.interaction.scope; -import lombok.Value; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.stereotype.Component; -@Value -public class TransactionBeforeCompletionEvent { +import lombok.val; - private final static TransactionBeforeCompletionEvent INSTANCE = new TransactionBeforeCompletionEvent(); +/** + * @since 2.0 + */ +@Component +public class TransactionScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + public static final String SCOPE_NAME = org.apache.causeway.applib.annotation.TransactionScope.SCOPE_NAME; - public static TransactionBeforeCompletionEvent instance() { - return INSTANCE; + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + val transactionScope = new StackedTransactionScope(); + beanFactory.registerScope(SCOPE_NAME, transactionScope); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java index 4c6d4e90ec..3186cdde8f 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java @@ -78,7 +78,9 @@ interface ObjectCreator { domainObject.getSpecification().streamAssociations(MixedIn.EXCLUDED) .forEach(field->field.toDefault(domainObject)); - getPersistenceLifecyclePublisher().onPostCreate(domainObject); + if (domainObject.getSpecification().isEntity()) { + getPersistenceLifecyclePublisher().onPostCreate(domainObject); + } return domainObject; diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java index 4707fac77a..488f6f3828 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java @@ -32,6 +32,10 @@ import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; * Responsible for collecting and then passing along changes (to the EntityChangeTracker, in persistence commons) so * that they can be published; and is responsible for calling the various persistence call-back facets. * + * <p> + * NOTE: this should really have been called ObjectLifecyclePublisher. + * </p> + * * @since 2.0 {index} */ public interface ObjectLifecyclePublisher { diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java similarity index 54% rename from core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java index a61d5a5567..094fc4d5d5 100644 --- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java @@ -16,26 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.causeway.core.interaction.scope; - -import org.apache.causeway.applib.services.iactn.Interaction; - -public interface TransactionBoundaryAware { - - default void beforeEnteringTransactionalBoundary(Interaction interaction) { - - } - - default void afterEnteringTransactionalBoundary(Interaction interaction, boolean isSynchronizationActive) { - - } - - default void beforeLeavingTransactionalBoundary(Interaction interaction, boolean isSynchronizationActive) { - - } - - default void afterLeavingTransactionalBoundary(Interaction interaction) { - - } +package org.apache.causeway.core.metamodel.specloader.specimpl; + +public enum IntrospectionState implements Comparable<IntrospectionState> { + /** + * At this stage, {@link LogicalTypeFacet} only. + */ + NOT_INTROSPECTED, + + /** + * Interim stage, to avoid infinite loops while on way to being {@link #TYPE_INTROSPECTED} + */ + TYPE_BEING_INTROSPECTED, + + /** + * Type has been introspected (but not its members). + */ + TYPE_INTROSPECTED, + + /** + * Interim stage, to avoid infinite loops while on way to being {@link #FULLY_INTROSPECTED} + */ + MEMBERS_BEING_INTROSPECTED, + + /** + * Fully introspected... class and also its members. + */ + FULLY_INTROSPECTED } 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 0db123e2fc..0218c9bd96 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 @@ -24,7 +24,6 @@ import org.springframework.context.annotation.Import; 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.TransactionEventEmitter; import org.apache.causeway.core.transaction.CausewayModuleCoreTransaction; import org.apache.causeway.valuetypes.jodatime.integration.CausewayModuleValJodatimeIntegration; @@ -40,7 +39,6 @@ import org.apache.causeway.valuetypes.jodatime.integration.CausewayModuleValJoda // @Service's MetamodelEventService.class, - TransactionEventEmitter.class, // @Configuration's diff --git a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java deleted file mode 100644 index b49a2bba89..0000000000 --- a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java +++ /dev/null @@ -1,67 +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.core.runtime.events; - -import javax.inject.Inject; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.support.TransactionSynchronization; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -import org.apache.causeway.applib.services.eventbus.EventBusService; -import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; -import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; - -import lombok.RequiredArgsConstructor; -import lombok.val; - -@Service -@RequiredArgsConstructor(onConstructor_ = {@Inject}) -public class TransactionEventEmitter -implements TransactionSynchronization, TransactionBoundaryAware { - - private final EventBusService eventBusService; - private final InteractionLayerTracker interactionLayerTracker; - - @Override - public void beforeCompletion() { - _Xray.txBeforeCompletion(interactionLayerTracker, "tx: beforeCompletion"); - eventBusService.post(TransactionBeforeCompletionEvent.instance()); - } - - @Override - public void afterCompletion(final int status) { - val event = TransactionAfterCompletionEvent.forStatus(status); - eventBusService.post(event); - _Xray.txAfterCompletion(interactionLayerTracker, String.format("tx: afterCompletion (%s)", event.name())); - } - - @Override - public void afterEnteringTransactionalBoundary( - final Interaction interaction, - final boolean isSynchronizationActive) { - if(isSynchronizationActive) { - TransactionSynchronizationManager.registerSynchronization(this); - } - } - -} diff --git a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java index 697e7ffb01..f1f415e7e1 100644 --- a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java +++ b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java @@ -29,7 +29,7 @@ import org.apache.causeway.core.security.util.XrayUtil; import lombok.val; -final class _Xray { +public final class _Xray { static void addConfiguration(final ConfigurationViewService configurationService) { diff --git a/core/runtimeservices/src/main/java/module-info.java b/core/runtimeservices/src/main/java/module-info.java index 69986102e3..33badc9675 100644 --- a/core/runtimeservices/src/main/java/module-info.java +++ b/core/runtimeservices/src/main/java/module-info.java @@ -79,7 +79,8 @@ module org.apache.causeway.core.runtimeservices { requires spring.core; requires spring.tx; requires org.apache.causeway.core.codegen.bytebuddy; + requires spring.aop; opens org.apache.causeway.core.runtimeservices.wrapper; opens org.apache.causeway.core.runtimeservices.wrapper.proxy; //to org.apache.causeway.core.codegen.bytebuddy -} \ No newline at end of file +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java index cd77ffc77c..2f2e256bfd 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java @@ -26,13 +26,12 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; -import org.apache.causeway.applib.services.iactnlayer.InteractionService; - import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.applib.annotation.TransactionScope; import org.apache.causeway.applib.services.clock.ClockService; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.publishing.spi.EntityPropertyChange; @@ -63,7 +62,6 @@ public class EntityPropertyChangePublisherDefault implements EntityPropertyChang private final UserService userService; private final ClockService clockService; private final TransactionService transactionService; - private final InteractionService interactionService; private final InteractionLayerTracker iaTracker; private final Provider<HasEnlistedEntityPropertyChanges> hasEnlistedEntityPropertyChangesProvider; diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java index b2115fc1ed..a13b5c02e3 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java @@ -31,6 +31,7 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.applib.annotation.TransactionScope; import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.functional.Either; @@ -47,6 +48,7 @@ import lombok.RequiredArgsConstructor; * @since 2.0 {@index} */ @Service +@TransactionScope @Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".ObjectLifecyclePublisherDefault") @Priority(PriorityPrecedence.EARLY) @Qualifier("Default") 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 b59d004901..7ebe1c7f8e 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 @@ -19,7 +19,6 @@ package org.apache.causeway.core.runtimeservices.session; import java.io.File; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Stack; @@ -31,13 +30,13 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; +import org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.springframework.transaction.support.TransactionSynchronizationManager; import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.clock.ClockService; @@ -60,10 +59,8 @@ import org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList; import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayUi; import org.apache.causeway.commons.internal.exceptions._Exceptions; -import org.apache.causeway.core.interaction.integration.InteractionAwareTransactionalBoundaryHandler; import org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor; import org.apache.causeway.core.interaction.scope.InteractionScopeLifecycleHandler; -import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware; 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; @@ -91,30 +88,32 @@ implements 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<Stack<InteractionLayer>> interactionLayerStack = ThreadLocal.withInitial(Stack::new); final MetamodelEventService runtimeEventService; final SpecificationLoader specificationLoader; final ServiceInjector serviceInjector; - final InteractionAwareTransactionalBoundaryHandler txBoundaryHandler; final ClockService clockService; final Provider<CommandPublisher> commandPublisherProvider; final Provider<TransactionService> transactionServiceProvider; final ConfigurableBeanFactory beanFactory; final InteractionScopeLifecycleHandler interactionScopeLifecycleHandler; + final TransactionServiceSpring transactionServiceSpring; + final InteractionIdGenerator interactionIdGenerator; - // to allow implementations to have dependencies back on this service. - @Inject @Lazy List<TransactionBoundaryAware> transactionBoundaryAwareBeans; @Inject public InteractionServiceDefault( final MetamodelEventService runtimeEventService, final SpecificationLoader specificationLoader, final ServiceInjector serviceInjector, - final InteractionAwareTransactionalBoundaryHandler txBoundaryHandler, + final TransactionServiceSpring transactionServiceSpring, final ClockService clockService, final Provider<CommandPublisher> commandPublisherProvider, final Provider<TransactionService> transactionServiceProvider, @@ -123,7 +122,7 @@ implements this.runtimeEventService = runtimeEventService; this.specificationLoader = specificationLoader; this.serviceInjector = serviceInjector; - this.txBoundaryHandler = txBoundaryHandler; + this.transactionServiceSpring = transactionServiceSpring; this.clockService = clockService; this.commandPublisherProvider = commandPublisherProvider; this.transactionServiceProvider = transactionServiceProvider; @@ -205,7 +204,8 @@ implements interactionLayerStack.get().push(interactionLayer); if(isAtTopLevel()) { - postInteractionOpened(causewayInteraction); + transactionServiceSpring.onOpen(causewayInteraction); + interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); } if(log.isDebugEnabled()) { @@ -355,21 +355,13 @@ implements return; } val interaction = _Casts.<CausewayInteraction>uncheckedCast(stack.get(0).getInteraction()); - txBoundaryHandler.requestRollback(interaction); + transactionServiceSpring.requestRollback(interaction); } private boolean isAtTopLevel() { return interactionLayerStack.get().size()==1; } - private void postInteractionOpened(final CausewayInteraction interaction) { - transactionBoundaryAwareBeans.forEach(bean->bean.beforeEnteringTransactionalBoundary(interaction)); - txBoundaryHandler.onOpen(interaction); - val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); - transactionBoundaryAwareBeans.forEach(bean->bean.afterEnteringTransactionalBoundary(interaction, isSynchronizationActive)); - interactionScopeLifecycleHandler.onTopLevelInteractionOpened(); - } - @SneakyThrows private void preInteractionClosed(final CausewayInteraction interaction) { @@ -387,10 +379,8 @@ implements flushException = e; } - val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); - transactionBoundaryAwareBeans.forEach(bean->bean.beforeLeavingTransactionalBoundary(interaction, isSynchronizationActive)); - txBoundaryHandler.onClose(interaction); - transactionBoundaryAwareBeans.forEach(bean->bean.afterLeavingTransactionalBoundary(interaction)); + transactionServiceSpring.onClose(interaction); + interactionScopeLifecycleHandler.onTopLevelInteractionPreDestroy(); // cleanup the InteractionScope (Spring scope) interactionScopeLifecycleHandler.onTopLevelInteractionClosed(); // cleanup the InteractionScope (Spring scope) interaction.close(); // do this last diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java index ca4552dfbe..af7b7149ac 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java @@ -26,32 +26,42 @@ import java.util.concurrent.atomic.LongAdder; import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Provider; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.event.EventListener; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker; import org.apache.causeway.applib.services.xactn.TransactionId; import org.apache.causeway.applib.services.xactn.TransactionService; import org.apache.causeway.applib.services.xactn.TransactionState; import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.functional.ThrowingRunnable; import org.apache.causeway.commons.functional.Try; import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.internal.collections._Lists; +import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.exceptions._Exceptions; -import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware; +import org.apache.causeway.core.interaction.session.CausewayInteraction; +import org.apache.causeway.core.runtime.events._Xray; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; +import org.apache.causeway.core.transaction.events.TransactionCompletionStatus; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; import lombok.val; import lombok.extern.log4j.Log4j2; @@ -70,27 +80,35 @@ import lombok.extern.log4j.Log4j2; @Log4j2 public class TransactionServiceSpring implements - TransactionService, - TransactionBoundaryAware { + TransactionService { private final Can<PlatformTransactionManager> platformTransactionManagers; - private final InteractionLayerTracker interactionLayerTracker; + private final Provider<InteractionLayerTracker> interactionLayerTrackerProvider; private final Can<PersistenceExceptionTranslator> persistenceExceptionTranslators; - + private final ConfigurableListableBeanFactory configurableListableBeanFactory; +// private final Can<TransactionBoundaryAware> transactionBoundaryAwareBeans; @Inject public TransactionServiceSpring( final List<PlatformTransactionManager> platformTransactionManagers, final List<PersistenceExceptionTranslator> persistenceExceptionTranslators, - final InteractionLayerTracker interactionLayerTracker) { + final Provider<InteractionLayerTracker> interactionLayerTrackerProvider, + final ConfigurableListableBeanFactory configurableListableBeanFactory +// , final List<TransactionBoundaryAware> transactionBoundaryAwareBeans + ) { this.platformTransactionManagers = Can.ofCollection(platformTransactionManagers); log.info("PlatformTransactionManagers: {}", platformTransactionManagers); + this.configurableListableBeanFactory = configurableListableBeanFactory; + this.persistenceExceptionTranslators = Can.ofCollection(persistenceExceptionTranslators); log.info("PersistenceExceptionTranslators: {}", persistenceExceptionTranslators); - this.interactionLayerTracker = interactionLayerTracker; + this.interactionLayerTrackerProvider = interactionLayerTrackerProvider; + +// this.transactionBoundaryAwareBeans = Can.ofCollection(transactionBoundaryAwareBeans); +// log.info("TransactionBoundaryAwareBeans: {}", transactionBoundaryAwareBeans); } // -- SPRING INTEGRATION @@ -98,23 +116,33 @@ implements @Override public <T> Try<T> callTransactional(final TransactionDefinition def, final Callable<T> callable) { - val txManager = transactionManagerForElseFail(def); // always throws if configuration is wrong + val platformTransactionManager = transactionManagerForElseFail(def); // always throws if configuration is wrong Try<T> result = null; try { - val tx = txManager.getTransaction(def); + TransactionStatus txStatus = platformTransactionManager.getTransaction(def); +// if(tx.isNewTransaction()) { +// transactionBoundaryAwareBeans.forEach(tba -> tba.afterEnteringTransactionalBoundary(platformTransactionManager)); +// } + registerTransactionSynchronizations(txStatus); + result = Try.call(callable) - .mapFailure(ex->translateExceptionIfPossible(ex, txManager)); + .mapFailure(ex->translateExceptionIfPossible(ex, platformTransactionManager)); +// if(tx.isNewTransaction()) { +// transactionBoundaryAwareBeans.forEach(tba -> tba.beforeLeavingTransactionalBoundary(platformTransactionManager)); +// } if(result.isFailure()) { - txManager.rollback(tx); + platformTransactionManager.rollback(txStatus); } else { - txManager.commit(tx); + platformTransactionManager.commit(txStatus); } - +// if(tx.isNewTransaction()) { +// transactionBoundaryAwareBeans.forEach(tba -> tba.afterLeavingTransactionalBoundary(platformTransactionManager)); +// } } catch (Exception ex) { return result!=null @@ -125,13 +153,30 @@ implements ? result // return the failure we just catched - : Try.failure(translateExceptionIfPossible(ex, txManager)); + : Try.failure(translateExceptionIfPossible(ex, platformTransactionManager)); } return result; } + + private void registerTransactionSynchronizations(final TransactionStatus txStatus) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + if (txStatus instanceof DefaultTransactionStatus) { + configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class) + .values() + .stream().filter(AopUtils::isAopProxy) // only the proxies + .forEach(TransactionSynchronizationManager::registerSynchronization); + } else { + configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class) + .values() + .stream().filter(AopUtils::isAopProxy) // only the proxies + .forEach(TransactionSynchronizationManager::registerSynchronization); + } + } + } + // @Override // public void nextTransaction() { // @@ -204,7 +249,7 @@ implements @Override public Optional<TransactionId> currentTransactionId() { - return interactionLayerTracker.getInteractionId() + return interactionLayerTrackerProvider.get().getInteractionId() .map(uuid->{ //XXX get current transaction's persistence context (once we support multiple contexts) val persistenceContext = ""; @@ -234,25 +279,11 @@ implements // -- TRANSACTION SEQUENCE TRACKING + // TODO: this ThreadLocal (as with all thread-locals) should perhaps somehow be managed using + // TransactionSynchronizationManager; see its javadoc for more details and look at implementations of + // TransactionSynchronization private ThreadLocal<LongAdder> txCounter = ThreadLocal.withInitial(LongAdder::new); - /** INTERACTION BEGIN BOUNDARY */ - @Override - public void beforeEnteringTransactionalBoundary(final Interaction interaction) { - txCounter.get().reset(); - } - - /** TRANSACTION END BOUNDARY */ - @EventListener(TransactionAfterCompletionEvent.class) - public void onTransactionEnded(final TransactionAfterCompletionEvent event) { - txCounter.get().increment(); - } - - /** INTERACTION END BOUNDARY */ - @Override - public void afterLeavingTransactionalBoundary(final Interaction interaction) { - txCounter.remove(); //XXX not tested yet: can we be certain that no txCounter.get() is called afterwards? - } // -- HELPER @@ -266,8 +297,9 @@ implements return platformTransactionManagers.getSingleton() .orElseThrow(()-> platformTransactionManagers.getCardinality().isMultiple() - ? _Exceptions.illegalState("Multiple PlatformTransactionManagers are configured, " - + "make sure a PlatformTransactionManager is provided via the TransactionTemplate argument.") + ? _Exceptions.illegalState( + "Multiple PlatformTransactionManagers are configured, cannot determine which one to use. " + + "Instead make sure a PlatformTransactionManager is provided explicitly by passing in a TransactionTemplate (implementation of TransactionDefinition).") : _Exceptions.illegalState("Needs a PlatformTransactionManager.")); } @@ -323,4 +355,116 @@ implements return ex; } + + public void onOpen(final @NonNull CausewayInteraction interaction) { + + txCounter.get().reset(); + + if (log.isDebugEnabled()) { + log.debug("opening on {}", _Probe.currentThreadId()); + } + + + if (!platformTransactionManagers.isEmpty()) { + val onCloseTasks = _Lists.<CloseTask>newArrayList(platformTransactionManagers.size()); + + interaction.putAttribute(OnCloseHandle.class, new OnCloseHandle(onCloseTasks)); + + platformTransactionManagers.forEach(txManager -> { + + val txDefn = new TransactionTemplate(txManager); // specify the txManager in question + txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + + // either participate in existing or create new transaction + TransactionStatus txStatus = txManager.getTransaction(txDefn); + + if(!txStatus.isNewTransaction()) { + // we are participating in an exiting transaction (or testing), nothing to do + return; + } + registerTransactionSynchronizations(txStatus); + + + // we have created a new transaction, so need to provide a CloseTask + onCloseTasks.add( + new CloseTask( + txStatus, + txManager.getClass().getName(), // info to be used for display in case of errors + () -> { +// transactionBoundaryAwareBeans.forEach(tbab -> tbab.beforeLeavingTransactionalBoundary(txManager)); + _Xray.txBeforeCompletion(interactionLayerTrackerProvider.get(), "tx: beforeCompletion"); + final TransactionCompletionStatus event; + if (txStatus.isRollbackOnly()) { + txManager.rollback(txStatus); + event = TransactionCompletionStatus.ROLLED_BACK; + } else { + txManager.commit(txStatus); + event = TransactionCompletionStatus.COMMITTED; + } + _Xray.txAfterCompletion(interactionLayerTrackerProvider.get(), String.format("tx: afterCompletion (%s)", event.name())); + +// transactionBoundaryAwareBeans.forEach(tbab -> tbab.afterLeavingTransactionalBoundary(txManager)); + txCounter.get().increment(); + } + ) + ); + + }); + } + + } + + + public void requestRollback(final @NonNull CausewayInteraction interaction) { + Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class)) + .ifPresent(OnCloseHandle::requestRollback); + } + + public void onClose(final @NonNull CausewayInteraction interaction) { + + if (log.isDebugEnabled()) { + log.debug("closing on {}", _Probe.currentThreadId()); + } + + if (!platformTransactionManagers.isEmpty()) { + Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class)) + .ifPresent(OnCloseHandle::runOnCloseTasks); + } + + txCounter.remove(); //XXX not tested yet: can we be certain that no txCounter.get() is called afterwards? + } + + @Value + private static class CloseTask { + @NonNull TransactionStatus txStatus; + @NonNull String onErrorInfo; + @NonNull ThrowingRunnable runnable; + } + + @RequiredArgsConstructor + private static class OnCloseHandle { + private final @NonNull List<CloseTask> onCloseTasks; + void requestRollback() { + onCloseTasks.forEach(onCloseTask->{ + onCloseTask.txStatus.setRollbackOnly(); + }); + } + void runOnCloseTasks() { + onCloseTasks.forEach(onCloseTask->{ + + try { + onCloseTask.getRunnable().run(); + } catch(final Throwable ex) { + // ignore + log.error( + "failed to close transactional boundary using transaction-manager {}; " + + "continuing to avoid memory leakage", + onCloseTask.getOnErrorInfo(), + ex); + } + + }); + } + } + } diff --git a/core/transaction/src/main/java/module-info.java b/core/transaction/src/main/java/module-info.java index f3fc966546..d64c8c9e33 100644 --- a/core/transaction/src/main/java/module-info.java +++ b/core/transaction/src/main/java/module-info.java @@ -34,4 +34,5 @@ module org.apache.causeway.core.transaction { requires spring.context; requires spring.core; requires spring.tx; -} \ No newline at end of file + requires java.transaction; +} diff --git a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java b/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java similarity index 60% rename from core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java rename to core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java index 3e8fbfeb4d..9117afaece 100644 --- a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java +++ b/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java @@ -18,13 +18,13 @@ */ package org.apache.causeway.core.transaction.events; -import org.springframework.transaction.support.TransactionSynchronization; +import javax.transaction.Status; /** * @since 2.0 {@index} - * @see TransactionSynchronization + * @see Status */ -public enum TransactionAfterCompletionEvent { +public enum TransactionCompletionStatus { /** Completion status in case of proper commit. */ COMMITTED, @@ -36,8 +36,29 @@ public enum TransactionAfterCompletionEvent { UNKNOWN, ; - public static TransactionAfterCompletionEvent forStatus(int status) { - return TransactionAfterCompletionEvent.values()[status]; + /** + * @param status field from {@link Status}. + * @return + */ + public static TransactionCompletionStatus forStatus(int status) { + switch (status) { + case 3: + // int STATUS_COMMITTED = 3; + return COMMITTED; + case 4: + // int STATUS_ROLLEDBACK = 4; + return ROLLED_BACK; + default: + // int STATUS_ACTIVE = 0; + // int STATUS_MARKED_ROLLBACK = 1; + // int STATUS_PREPARED = 2; + // int STATUS_UNKNOWN = 5; + // int STATUS_NO_TRANSACTION = 6; + // int STATUS_PREPARING = 7; + // int STATUS_COMMITTING = 8; + // int STATUS_ROLLING_BACK = 9; + return UNKNOWN; + } } public boolean isCommitted() { diff --git a/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java b/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java index 77cab60cb2..a9c0166d87 100644 --- a/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java +++ b/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java @@ -49,9 +49,9 @@ public class OutboxRestClient_E2eTest { .oauthClientSecret("xxx") .build(), OutboxClientConfig.builder() - .pendingUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/pending/invoke") - .deleteUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/delete/invoke") - .deleteManyUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke") + .pendingUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/pending/invoke") + .deleteUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/delete/invoke") + .deleteManyUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke") .build() ); } diff --git a/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java b/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java index a2aace75a7..5881da3d40 100644 --- a/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java +++ b/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java @@ -69,18 +69,17 @@ public class SessionSubscriberForSessionLog implements SessionSubscriber { public void log(final Type type, final String username, final Date date, final CausedBy causedBy, final UUID sessionGuid, final String httpSessionId) { interactionService.runAnonymous(() -> { transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> { + val sessionLogEntryIfAny = sessionLogEntryRepository.findBySessionGuid(sessionGuid); if (type == Type.LOGIN) { - sessionLogEntryRepository.create(username, sessionGuid, httpSessionId, causedBy, Timestamp.from(date.toInstant())); + if (sessionLogEntryIfAny.isEmpty()) { + sessionLogEntryRepository.create(username, sessionGuid, httpSessionId, causedBy, Timestamp.from(date.toInstant())); + } } else { - - val sessionLogEntryIfAny = sessionLogEntryRepository.findBySessionGuid(sessionGuid); - sessionLogEntryIfAny - .ifPresent(entry -> { - entry.setLogoutTimestamp(Timestamp.from(date.toInstant())); - entry.setCausedBy(causedBy); - transactionService.flushTransaction(); - } - ); + sessionLogEntryIfAny.ifPresent(entry -> { + entry.setLogoutTimestamp(Timestamp.from(date.toInstant())); + entry.setCausedBy(causedBy); + transactionService.flushTransaction(); + }); } }) .ifFailureFail(); // throw if rolled back diff --git a/persistence/commons/src/main/java/module-info.java b/persistence/commons/src/main/java/module-info.java index ddc1bccd1e..58a535970c 100644 --- a/persistence/commons/src/main/java/module-info.java +++ b/persistence/commons/src/main/java/module-info.java @@ -39,4 +39,5 @@ module org.apache.causeway.persistence.commons { requires spring.beans; requires spring.context; requires spring.core; -} \ No newline at end of file + requires spring.tx; +} diff --git a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java index 58d7f24b6e..ac62cade36 100644 --- a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java +++ b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java @@ -32,7 +32,6 @@ import org.apache.causeway.persistence.commons.integration.changetracking.PreAnd // @Service's EntityChangeTrackerDefault.class, - EntityChangeTrackerDefault.TransactionSubscriber.class, PreAndPostValueEvaluatorServiceDefault.class, }) public class CausewayModulePersistenceCommons { diff --git a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java index 6a404701eb..dbf8911f1b 100644 --- a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java +++ b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java @@ -27,26 +27,25 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.LongAdder; import java.util.function.Function; -import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.event.EventListener; -import org.springframework.core.annotation.Order; +import org.springframework.core.Ordered; import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.EntityChangeKind; -import org.apache.causeway.applib.annotation.InteractionScope; import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.applib.annotation.Programmatic; +import org.apache.causeway.applib.annotation.TransactionScope; import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.iactn.InteractionProvider; -import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.metrics.MetricsService; import org.apache.causeway.applib.services.publishing.spi.EntityChanges; import org.apache.causeway.applib.services.publishing.spi.EntityPropertyChange; @@ -68,7 +67,6 @@ import org.apache.causeway.core.transaction.changetracking.EntityChangeTracker; import org.apache.causeway.core.transaction.changetracking.EntityChangesPublisher; import org.apache.causeway.core.transaction.changetracking.EntityPropertyChangePublisher; import org.apache.causeway.core.transaction.changetracking.HasEnlistedEntityChanges; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; import lombok.AccessLevel; import lombok.Getter; @@ -78,24 +76,22 @@ import lombok.val; import lombok.extern.log4j.Log4j2; /** - * This service keeps track of all of the changes within a transactoin, for entities for which entity property change + * This object keeps track of all of the changes within a transaction, for entities for which entity property change * publishing is enabled (typically using the * {@link DomainObject#entityChangePublishing() @DomainObject(entityChangePublishing=)} annotation attribute. * * <p> - * The service is {@link InteractionScope}d. In theory this could happen multiple times per interaction, so the - * data structures are cleared on each commit for potential reuse within the same interaction. (Of course, because the - * service <i>is</i> interaction-scoped, a new instance of the service is created for each interaction, and so the - * data held in this service is private to each user's interaction. + * The service is {@link TransactionScope transaction-scope}d and implements Spring's {@link TransactionSynchronization} + * interface, meaning that Spring will call the {@link #beforeCompletion()} callback. This service also implements + * {@link org.springframework.core.Ordered} to ensure it isn't called last by {@link TransactionSynchronizationManager}. * </p> * * @since 2.0 {@index} */ @Service +@TransactionScope @Named("causeway.persistence.commons.EntityChangeTrackerDefault") -@Priority(PriorityPrecedence.EARLY) @Qualifier("default") -@InteractionScope // see note above regarding this @RequiredArgsConstructor(onConstructor_ = {@Inject}) @Log4j2 public class EntityChangeTrackerDefault @@ -103,8 +99,15 @@ implements MetricsService, EntityChangeTracker, HasEnlistedEntityPropertyChanges, - HasEnlistedEntityChanges { + HasEnlistedEntityChanges, + TransactionSynchronization, + Ordered { + @Programmatic + @Override + public int getOrder() { + return PriorityPrecedence.EARLY; + } private final EntityPropertyChangePublisher entityPropertyChangePublisher; private final EntityChangesPublisher entityChangesPublisher; @@ -193,83 +196,26 @@ implements return false; } - /** - * Subscribes to transactions and forwards onto the current interaction's EntityChangeTracker, if available. - * - * <p> - * Note that this service has singleton-scope, unlike {@link EntityChangeTrackerDefault} which has - * {@link InteractionScope interaction scope}. The problem with using {@link EntityChangeTrackerDefault} as - * the direct subscriber is that if there's no {@link Interaction}, then Spring will fail to activate an instance resulting in an - * {@link org.springframework.beans.factory.support.ScopeNotActiveException}. Now, admittedly that exception - * gets swallowed in the call stack somewhere, but it's still not pretty. - * </p> - * - * <p> - * This design, instead, at least lets us check if there's an interaction in scope, and effectively ignore - * the call if not. - * </p> - */ - @Component - @Named("causeway.persistence.commons.EntityChangeTrackerDefault.TransactionSubscriber") - @Priority(PriorityPrecedence.EARLY) - @Qualifier("default") - @RequiredArgsConstructor(onConstructor_ = {@Inject}) - public static class TransactionSubscriber { - - private final InteractionService interactionService; - private final Provider<EntityChangeTrackerDefault> entityChangeTrackerProvider; - - /** - * TRANSACTION END BOUNDARY - * @apiNote intended to be called during before transaction completion by the framework internally - */ - @EventListener(value = TransactionBeforeCompletionEvent.class) - @Order(PriorityPrecedence.LATE) - public void onTransactionCompleting(final TransactionBeforeCompletionEvent event) { - - if(!interactionService.isInInteraction()) { - // discard request is there is no interaction in scope. - // this shouldn't ever really occur, but some low-level (could be improved?) integration tests do - // hit this case. - return; - } - entityChangeTracker().onTransactionCompleting(event); - } - - private EntityChangeTrackerDefault entityChangeTracker() { - return entityChangeTrackerProvider.get(); - } - } - /** - * As called by {@link TransactionSubscriber}, so long as there is an {@link Interaction} in - * {@link InteractionScope scope}. - */ - void onTransactionCompleting(final TransactionBeforeCompletionEvent event) { + @Override + public void beforeCompletion() { try { - doPublish(); - } finally { - postPublishing(); - } - } + _Xray.publish(this, interactionProviderProvider); - private void doPublish() { - _Xray.publish(this, interactionProviderProvider); + log.debug("about to publish entity changes"); + entityPropertyChangePublisher.publishChangedProperties(); + entityChangesPublisher.publishChangingEntities(this); - log.debug("about to publish entity changes"); - entityPropertyChangePublisher.publishChangedProperties(); - entityChangesPublisher.publishChangingEntities(this); - } - - private void postPublishing() { - log.debug("purging entity change records"); + } finally { + log.debug("purging entity change records"); - enlistedPropertyChangeRecordsById.clear(); - entityPropertyChangeRecordsForPublishing.clear(); + enlistedPropertyChangeRecordsById.clear(); + entityPropertyChangeRecordsForPublishing.clear(); - changeKindByEnlistedAdapter.clear(); - entityChangeEventCount.reset(); - numberEntitiesLoaded.reset(); + changeKindByEnlistedAdapter.clear(); + entityChangeEventCount.reset(); + numberEntitiesLoaded.reset(); + } } private void enableCommandPublishing() { diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java index 598f65fb07..faa4da52c1 100644 --- a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java +++ b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java @@ -27,6 +27,7 @@ import javax.sql.DataSource; import org.datanucleus.api.jdo.JDOPersistenceManagerFactory; import org.datanucleus.metadata.PersistenceUnitMetaData; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java index b4285297a5..97f53f6eee 100644 --- a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java +++ b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java @@ -32,6 +32,7 @@ import javax.jdo.PersistenceManager; import org.datanucleus.api.jdo.JDOQuery; import org.datanucleus.enhancement.Persistable; import org.datanucleus.store.rdbms.RDBMSPropertyNames; + import org.springframework.lang.Nullable; import org.apache.causeway.applib.query.AllInstancesQuery; diff --git a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java index 9621f025b6..fb80bc4358 100644 --- a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java +++ b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java @@ -43,8 +43,6 @@ import org.apache.causeway.applib.services.xactn.TransactionService; import org.apache.causeway.commons.internal.base._Refs; import org.apache.causeway.commons.internal.base._Refs.ObjectReference; import org.apache.causeway.core.config.presets.CausewayPresets; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; import org.apache.causeway.testdomain.conf.Configuration_usingJpa; import org.apache.causeway.testdomain.jpa.JpaTestDomainPersona; import org.apache.causeway.testdomain.jpa.entities.JpaBook; diff --git a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java index ff09d6858f..6d295dd39e 100644 --- a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java +++ b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java @@ -42,8 +42,6 @@ import org.apache.causeway.applib.services.xactn.TransactionService; import org.apache.causeway.commons.internal.base._Refs; import org.apache.causeway.commons.internal.base._Refs.ObjectReference; import org.apache.causeway.core.config.presets.CausewayPresets; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; import org.apache.causeway.testdomain.conf.Configuration_usingJpa; import org.apache.causeway.testdomain.jpa.JpaTestDomainPersona; import org.apache.causeway.testdomain.jpa.entities.JpaBook; diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java index c89c965495..29b93681f6 100644 --- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java +++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java @@ -36,7 +36,6 @@ import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.iactn.Interaction; import org.apache.causeway.applib.services.metrics.MetricsService; import org.apache.causeway.core.config.presets.CausewayPresets; -import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass; import org.apache.causeway.testdomain.util.interaction.DomainObjectTesterFactory; diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java index f279e3696f..46e344f5f2 100644 --- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java +++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java @@ -46,8 +46,6 @@ import org.apache.causeway.commons.internal.debug._Probe; import org.apache.causeway.commons.internal.debug.xray.XrayModel.Stickiness; import org.apache.causeway.commons.internal.debug.xray.XrayModel.ThreadMemento; import org.apache.causeway.commons.internal.debug.xray.XrayUi; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; import lombok.Getter; import lombok.NonNull; diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java index 4e5304f695..33c97b25e1 100644 --- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java +++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java @@ -29,9 +29,6 @@ import org.springframework.stereotype.Service; import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.iactn.Interaction; -import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware; -import org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent; -import org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent; import org.apache.causeway.testdomain.util.kv.KVStoreForTesting; import lombok.val; diff --git a/scripts/ci/rename-all-published-sources-to-causeway.jsh b/scripts/ci/rename-all-published-sources-to-causeway.jsh new file mode 100644 index 0000000000..6cde48b619 --- /dev/null +++ b/scripts/ci/rename-all-published-sources-to-causeway.jsh @@ -0,0 +1,380 @@ +/* + * 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. + */ +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Renames project from oldName to newName. + * + * <p> + * This doesn't rewrite the contents of the following (binary) extensions: + * <ul> + * <li>.jar</li> + * <li>.zip</li> + * <li>.pptx</li> + * <li>.docx</li> + * <li>.xlsx</li> + * <li>.odt</li> + * <li>.rtf</li> + * <li>.pdf</li> + * </ul> + */ +class RenameProject { + + static final boolean DRY_RUN = false; + + // to obtain all the suffixes: + // find . -type f | sed -rn 's|.*/[^/]+\.([^/.]+)$|\1|p' | sort -u + static final List<String> EXTENSIONS = List.of( + "NOTICE", + "STATUS", + "MF", + "TXT", + // "adoc", // ignore adoc file content + "bat", + "cfg", + "css", + "dcl", + "dtd", + "factories", + "feature", + "fxml", + "gql", + "graphqls", + "hbs", + "html", + "importorder", + "info", + "ini", + "java", + "jdo", + "js", + "json", + "kt", + "kts", + "ldif", + "list", + "md", + "orm", + "po", + "pot", + "properties", + "puml", + "rdf", + "sh", + "soc", + "svg", + "template", + "thtml", + "ts", + "txt", + "xml", + "xsd", + "yaml", + "yml" + ); + + static final List<String> PATH_EXCLUSIONS = List.of( + "/build/", + "/target/", + "/adoc/", // not published in its legacy form + "/scripts/ci/", // don't touch + "/." + ); + + static final List<String> UNCONDITIONAL_INCLUSIONS = List.of( + "/META-INF/services/"); + + public static RenameProject renameBackToLegacy(final File root) { + return new RenameProject(root, "isis", "causeway"); + } + + private RenameProject(final File root, final String oldName, final String newName) { + this.root = root; + this.fromLower = oldName.toLowerCase(); + this.toLower = newName.toLowerCase(); + this.fromUpper = oldName.toUpperCase(); + this.toUpper = newName.toUpperCase(); + this.fromTitle = capitalize(fromLower); + this.toTitle = capitalize(toLower); + } + + final File root; + + final String fromLower; + final String toLower; + final String fromUpper; + final String toUpper; + final String fromTitle; + final String toTitle; + + static String capitalize(final String s) { return s.substring(0, 1).toUpperCase() + s.substring(1); } + + public void renameAllFiles() throws IOException { + Files.find(root.toPath(), Integer.MAX_VALUE, (path, attr)->{ + if(isPathExcluded(path.toFile())) { + return false; + } + return !attr.isDirectory(); + }, FileVisitOption.FOLLOW_LINKS) + .map(Path::toFile) + //.filter(file->fileNameEndsWithSupportedExtension(file)) // rename files unconditionally + .forEach(file->{ + renameIfRequired(file); + }); + } + + private void renameIfRequired(final File file) { + var relativeFilePathRenamed = pathRelativeToRoot(file) + .replace(fromTitle, toTitle) + .replace(fromUpper, toUpper) + .replace("\\" + fromLower, "\\" + toLower) + .replace("/" + fromLower, "/" + toLower) + .replace("-" + fromLower, "-" + toLower) + .replace("_" + fromLower, "_" + toLower) + .replace("." + fromLower, "." + toLower); + + var filePathRenamed = pathOf(root) + relativeFilePathRenamed; + + if (!filePathRenamed.equals(pathOf(file))) { + + System.err.printf("rename %s -> %s%n", pathRelativeToRoot(file), relativeFilePathRenamed); + //System.err.printf("rename %s -> %s%n", pathOf(file), filePathRenamed); + + if(!DRY_RUN) { + var fileRename = new File(filePathRenamed); + var parentFile = fileRename.getParentFile(); + parentFile.mkdirs(); + try { + Files.move(file.toPath(), fileRename.toPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public void rewriteAllFileContents() throws IOException { + + Files.find(root.toPath(), Integer.MAX_VALUE, (path, attr)->{ + return !attr.isDirectory(); + }, FileVisitOption.FOLLOW_LINKS) + .map(Path::toFile) + .filter(File::exists) + .forEach(file->{ + if(isPathExcluded(file)) { + return; + } + if(isPathUnconditionallyIncluded(file) + || fileNameEndsWithSupportedExtension(file)) { + try { + rewriteIfRequired(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + private void rewriteIfRequired(final File file) throws FileNotFoundException, IOException { + var lines = readLinesFromFile(file, StandardCharsets.UTF_8); + var newLines = new ArrayList<String>(lines.size()); + + final int linesChangedCount = + lines.stream().mapToInt(line->{ + var newLine = line + .replace(fromLower, toLower) + .replace(fromUpper, toUpper) + .replace(fromTitle, toTitle) + + // update schema declarations in .layout.xml files. + // terrible hack - we are assuming the target is 'causeway' + // (also, the first arg has to handle the case that we've already converted replaced 'isis' -> 'causeway' throughout...) + .replace( + "https://causeway.apache.org/applib/layout/menubars/bootstrap3 https://causeway.apache.org/applib/layout/menubars/bootstrap3/menubars.xsd", + "http://causeway.apache.org/applib/layout/menubars/bootstrap3 https://isis.apache.org/applib/layout-v1/menubars/bootstrap3/menubars.xsd") + .replace( + "https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd", + "http://causeway.apache.org/applib/layout/component https://isis.apache.org/applib/layout-v1/component/component.xsd") + .replace( + "https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd", + "http://causeway.apache.org/applib/layout/grid/bootstrap3 https://isis.apache.org/applib/layout-v1/grid/bootstrap3/bootstrap3.xsd") + .replace( + "https://causeway.apache.org/applib/layout/links https://causeway.apache.org/applib/layout/links/links.xsd", + "http://causeway.apache.org/applib/layout/links https://isis.apache.org/applib/layout-v1/links/links.xsd") + + // update namespace declarations in all files (.layout.xml and also constants in .java classes) + // (again, the first arg has to handle the case that we've already converted replaced 'isis' -> 'causeway' throughout...) + .replace( + "\"https://causeway.apache.org/applib/layout/menubars/bootstrap3\"", + "\"http://causeway.apache.org/applib/layout/menubars/bootstrap3\"") + .replace( + "\"https://causeway.apache.org/applib/layout/component\"", + "\"http://causeway.apache.org/applib/layout/component\"") + .replace( + "\"https://causeway.apache.org/applib/layout/grid/bootstrap3\"", + "\"http://causeway.apache.org/applib/layout/grid/bootstrap3\"") + .replace( + "\"https://causeway.apache.org/applib/layout/links", + "\"http://causeway.apache.org/applib/layout/links") + .replace( + "\"https://causeway.apache.org/schema", + "\"http://causeway.apache.org/schema") + + ; + newLines.add(newLine); + return line.equals(newLine) + ? 0 + : 1; + }) + .sum(); + + if (linesChangedCount>0) { + System.err.printf("rewriting %s%n", pathRelativeToRoot(file)); + if(!DRY_RUN) { + writeLinesToFile(newLines, file, StandardCharsets.UTF_8); + } + } + } + + private static boolean fileNameEndsWithSupportedExtension(final File file) { + for (String ext : EXTENSIONS) { + if (file.getName().endsWith("." + ext) + || file.getName().equals(ext)) { + return true; + } + } + return false; + } + + private static boolean isPathUnconditionallyIncluded(final File file) { + var path = pathOf(file); + for (String incl : UNCONDITIONAL_INCLUSIONS) { + if (path.contains(incl)) { + return true; + } + } + return false; + } + + private static boolean isPathExcluded(final File file) { + var path = pathOf(file); +// System.out.println(path); + + for (String excl : PATH_EXCLUSIONS) { + if (path.contains(excl)) { + return true; + } + } + return false; + } + + private String pathRelativeToRoot(final File file) { + var prefix = pathOf(root); + var path = pathOf(file); + if(!path.startsWith(prefix)) { + throw new IllegalArgumentException("file not subpath of root"); + } + return path.substring(prefix.length()); + } + + private static String pathOf(final File file) { + return file.getAbsolutePath().replace('\\', '/'); + } + + // -- READING + + private static List<String> readLines( + final InputStream input, + final Charset charset){ + if(input==null) { + return List.of(); + } + var lines = new ArrayList<String>(); + try(Scanner scanner = new Scanner(input, charset.name())){ + scanner.useDelimiter("\\n"); + while(scanner.hasNext()) { + var line = scanner.next(); + //line = line.replace("\r", ""); // preserve windows specific line terminal + lines.add(line); + } + } + return lines; + } + + private static List<String> readLinesFromFile( + final File file, + final Charset charset) throws FileNotFoundException, IOException { + try(var input = new FileInputStream(file)){ + return readLines(input, charset); + } + } + + // -- WRITING + + static void writeLinesToFile( + final Iterable<String> lines, + final File file, + final Charset charset) throws FileNotFoundException, IOException { + + try(var bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset))) { + for(var line : lines) { + bw.append(line).append("\n"); + } + } + } + +} + +boolean valid = true; + +String rootPath = System.getenv("ROOT_PATH_LEGACY"); +if(rootPath.isBlank() || ! new File(rootPath).exists()) { + System.err.println("env ROOT_PATH_LEGACY must point to an existing directory"); + valid = false; +} + +if (valid) { + var root = new File(rootPath); + + var renamer = RenameProject.renameBackToLegacy(root); + System.out.printf("processing root %s%n", root.getAbsolutePath()); + + renamer.renameAllFiles(); + renamer.rewriteAllFileContents(); + + System.out.println("done."); +} + + +/exit diff --git a/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java b/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java index 7bcc65b6fb..0121ee3a48 100644 --- a/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java +++ b/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java @@ -43,8 +43,6 @@ implements BrandingUiService { @Inject public BrandingUiServiceDefault(final CausewayConfiguration causewayConfiguration) { - //TODO application name/logo borrowed from Wicket's configuration, - // we might generalize this config option to all viewers this.appConfig = causewayConfiguration.getViewer().getCommon().getApplication(); } diff --git a/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin b/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin deleted file mode 100644 index 055a88de82..0000000000 --- a/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin +++ /dev/null @@ -1 +0,0 @@ -org.apache.causeway.viewer.restfulobjects.jaxrsresteasy4.IsisResteasy4Plugin \ No newline at end of file diff --git a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.java b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.java new file mode 100644 index 0000000000..215c8c4120 --- /dev/null +++ b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.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.viewer.restfulobjects.rendering.service.conneg; + +import java.util.List; + +import javax.inject.Named; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import org.apache.causeway.applib.annotation.PriorityPrecedence; +import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction; +import org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection; +import org.apache.causeway.core.metamodel.interactions.managed.ManagedProperty; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.viewer.restfulobjects.applib.CausewayModuleViewerRestfulObjectsApplib; +import org.apache.causeway.viewer.restfulobjects.rendering.IResourceContext; +import org.apache.causeway.viewer.restfulobjects.rendering.Responses; +import org.apache.causeway.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation; + +import lombok.extern.log4j.Log4j2; + +/** + * @since 1.x {@index} + */ +@Service +@Named(ContentNegotiationServiceOrgApacheCausewayV1.LOGICAL_TYPE_NAME) +@javax.annotation.Priority(PriorityPrecedence.MIDPOINT - 200) +@Qualifier("OrgApacheCausewayV1") +@Log4j2 +public class ContentNegotiationServiceOrgApacheCausewayV1 extends ContentNegotiationServiceAbstract { + + static final String LOGICAL_TYPE_NAME = CausewayModuleViewerRestfulObjectsApplib.NAMESPACE + ".ContentNegotiationServiceOrgApacheCausewayV1"; + public static final String ACCEPT_PROFILE = "urn:org.apache.causeway/v1"; + + @Override + public Response.ResponseBuilder buildResponse( + final IResourceContext resourceContext, + final ManagedObject objectAdapter) { + return whenV1ThenNotImplemented(resourceContext); + } + + @Override + public Response.ResponseBuilder buildResponse( + final IResourceContext resourceContext, + final ManagedProperty objectAndProperty) { + return whenV1ThenNotImplemented(resourceContext); + } + + @Override + public Response.ResponseBuilder buildResponse( + final IResourceContext resourceContext, + final ManagedCollection managedCollection) { + return whenV1ThenNotImplemented(resourceContext); + } + + @Override + public Response.ResponseBuilder buildResponse( + final IResourceContext resourceContext, + final ManagedAction objectAndAction) { + return whenV1ThenNotImplemented(resourceContext); + } + + @Override + public Response.ResponseBuilder buildResponse( + final IResourceContext resourceContext, + final ObjectAndActionInvocation objectAndActionInvocation) { + return whenV1ThenNotImplemented(resourceContext); + } + + // -- HELPER + + private boolean canAccept(final IResourceContext resourceContext) { + final List<MediaType> acceptableMediaTypes = resourceContext.getAcceptableMediaTypes(); + return mediaTypeParameterMatches(acceptableMediaTypes, "profile", ACCEPT_PROFILE); + } + + private ResponseBuilder whenV1ThenNotImplemented(final IResourceContext resourceContext) { + if(!canAccept(resourceContext)) { + return null; + } + log.warn("profile '{}' is no longer supported use '{}' instead", + ACCEPT_PROFILE, + ContentNegotiationServiceOrgApacheCausewayV2.ACCEPT_PROFILE); + return Responses.ofNotImplemented(); + } + +} diff --git a/viewers/restfulobjects/viewer/src/main/java/module-info.java b/viewers/restfulobjects/viewer/src/main/java/module-info.java index 2963adccec..cd5dfa686d 100644 --- a/viewers/restfulobjects/viewer/src/main/java/module-info.java +++ b/viewers/restfulobjects/viewer/src/main/java/module-info.java @@ -52,5 +52,4 @@ module org.apache.causeway.viewer.restfulobjects.viewer { requires spring.context; requires spring.core; requires spring.web; - requires spring.boot; -} \ No newline at end of file +}