From 34b360bc20baff20810b50c59f83b82a13b899ab Mon Sep 17 00:00:00 2001 From: Dan Haywood Date: Sun, 27 Apr 2025 13:41:43 +0100 Subject: [PATCH 1/5] CAUSEWAY-3883: updates docs a little --- .../internal/proxy/_ProxyFactoryService.java | 2 + .../ProxyFactoryServiceByteBuddy.java | 61 +++- .../context/HasMetaModelContext.java | 5 + .../context/MetaModelContext_usingSpring.java | 5 + .../specimpl/ObjectMemberAbstract.java | 5 - .../src/main/java/module-info.java | 1 + .../InvocationHandlerForAsyncWrapMixin.java | 112 ++++++ .../InvocationHandlerforAsyncAbstract.java | 163 +++++++++ .../InvocationHandlerforAsyncWrapRegular.java | 101 ++++++ .../wrapper/MemberAndTarget.java | 64 ++++ .../wrapper/WrapperFactoryDefault.java | 321 +++--------------- .../handlers/CollectionInvocationHandler.java | 3 +- .../handlers/DelegatingInvocationHandler.java | 6 +- ... DelegatingInvocationHandlerAbstract.java} | 67 ++-- .../DomainObjectInvocationHandler.java | 243 ++++++------- .../handlers/MapInvocationHandler.java | 3 +- .../PluralInvocationHandlerAbstract.java | 26 +- .../wrapper/handlers/ProxyContextHandler.java | 70 ++-- .../handlers/WrapperInvocationContext.java | 74 ++++ .../wrapper/proxy/ProxyCreator.java | 6 +- .../wrapper/WrapperFactoryDefaultTest.java | 7 +- .../ProxyCreatorTestUsingCodegenPlugin.java | 10 +- .../BackgroundService_IntegTestAbstract.java | 4 +- .../testdomain/jpa/JpaInventoryManager.java | 4 + .../wrapper/jdo/JpaWrapperSyncTest.java | 90 +++++ ...WrapperFactoryMetaspaceMemoryLeakTest.java | 111 ++++++ 26 files changed, 1067 insertions(+), 497 deletions(-) create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerForAsyncWrapMixin.java create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncAbstract.java create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncWrapRegular.java create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/MemberAndTarget.java rename core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/{DelegatingInvocationHandlerDefault.java => DelegatingInvocationHandlerAbstract.java} (54%) create mode 100644 core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/WrapperInvocationContext.java create mode 100644 regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/JpaWrapperSyncTest.java create mode 100644 regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java index 518d4ded55d..e93b48c864a 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java @@ -26,6 +26,8 @@ */ public interface _ProxyFactoryService { + String WRAPPER_INVOCATION_CONTEXT_FIELD_NAME = "__causeway_wrapperInvocationContext"; + _ProxyFactory factory( Class base, @Nullable Class[] interfaces, diff --git a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java index abf726d4c4e..2726b2988f8 100644 --- a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java +++ b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java @@ -18,8 +18,12 @@ */ package org.apache.causeway.core.codegen.bytebuddy.services; +import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.Map; import java.util.function.Function; import org.springframework.lang.Nullable; @@ -29,6 +33,7 @@ import org.apache.causeway.commons.internal._Constants; import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.internal.collections._Maps; import org.apache.causeway.commons.internal.context._Context; import org.apache.causeway.commons.internal.proxy._ProxyFactory; import org.apache.causeway.commons.internal.proxy._ProxyFactoryServiceAbstract; @@ -44,7 +49,22 @@ @Service public class ProxyFactoryServiceByteBuddy extends _ProxyFactoryServiceAbstract { + private final ClassLoadingStrategyAdvisor strategyAdvisor = new ClassLoadingStrategyAdvisor(); + /** + * Cached proxy class by invocation handler. + * + *

+ * The only state held in invocation handler is the org.apache.causeway.core.metamodel.spec.ObjectSpecification, + *. in effect the target class. + *

+ * + *

+ * The remaining state (defined by WrapperInvocationContext) is held in the proxy object itself as a field. + *

+ * @return + */ + private Map> proxyClassByInvocationHandler = _Maps.newConcurrentHashMap(); @Override public _ProxyFactory factory( @@ -54,13 +74,27 @@ public _ProxyFactory factory( val objenesis = new ObjenesisStd(); - final Function> proxyClassFactory = handler-> - nextProxyDef(base, interfaces) - .intercept(InvocationHandlerAdapter.of(handler)) - .make() - .load(_Context.getDefaultClassLoader(), - strategyAdvisor.getSuitableStrategy(base)) - .getLoaded(); + final Function> proxyClassFactory = new Function<>() { + + @Override + public Class apply(InvocationHandler handler) { + return (Class) proxyClassByInvocationHandler.computeIfAbsent(handler, this::createClass); + } + + private Class createClass(InvocationHandler handler) { + try (final var unloaded = nextProxyDef(base, interfaces) + .intercept(InvocationHandlerAdapter.of(handler)) + .defineField(WRAPPER_INVOCATION_CONTEXT_FIELD_NAME, Object.class, Modifier.PUBLIC) + .make() + ) { + return unloaded + .load(_Context.getDefaultClassLoader(), strategyAdvisor.getSuitableStrategy(base)) + .getLoaded(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to generate proxy class", e); + } + } + }; return new _ProxyFactory() { @@ -68,7 +102,6 @@ public _ProxyFactory factory( public T createInstance(final InvocationHandler handler, final boolean initialize) { try { - if(initialize) { ensureSameSize(constructorArgTypes, null); return _Casts.uncheckedCast( createUsingConstructor(handler, null) ); @@ -101,20 +134,18 @@ public T createInstance(final InvocationHandler handler, final Object[] construc private Object createNotUsingConstructor(final InvocationHandler invocationHandler) { final Class proxyClass = proxyClassFactory.apply(invocationHandler); - final Object object = objenesis.newInstance(proxyClass); - return object; + return objenesis.newInstance(proxyClass); } // -- HELPER (create with initialize) private Object createUsingConstructor(final InvocationHandler invocationHandler, @Nullable final Object[] constructorArgs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { - final Class proxyClass = proxyClassFactory.apply(invocationHandler); - return proxyClass - .getConstructor(constructorArgTypes==null ? _Constants.emptyClasses : constructorArgTypes) - .newInstance(constructorArgs==null ? _Constants.emptyObjects : constructorArgs); + final var proxyClass = proxyClassFactory.apply(invocationHandler); // creates or fetches from cache + final var constructor = + proxyClass.getConstructor(constructorArgTypes == null ? _Constants.emptyClasses : constructorArgTypes); + return constructor.newInstance(constructorArgs == null ? _Constants.emptyObjects : constructorArgs); } - }; } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java index 0149cfac46d..e542b43f0c3 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java @@ -46,6 +46,7 @@ import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconService; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; +import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory; import org.apache.causeway.core.metamodel.services.message.MessageBroker; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; @@ -150,6 +151,10 @@ default InteractionService getInteractionService() { return getMetaModelContext().getInteractionService(); } + default CommandDtoFactory getCommandDtoFactory() { + return getMetaModelContext().getCommandDtoFactory(); + } + default Optional currentUserLocale() { return getInteractionService().currentInteractionContext() .map(InteractionContext::getLocale); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java index b02949fab16..23ee4ceed92 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java @@ -48,6 +48,7 @@ import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconService; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; +import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import org.apache.causeway.core.security.authentication.manager.AuthenticationManager; import org.apache.causeway.core.security.authorization.manager.AuthorizationManager; @@ -160,6 +161,10 @@ void onDestroy() { private final InteractionService interactionService = getSingletonElseFail(InteractionService.class); + @Getter(lazy = true) + private final CommandDtoFactory commandDtoFactory = + getSingletonElseFail(CommandDtoFactory.class); + @Override public final ManagedObject getHomePageAdapter() { final Object pojo = getHomePageResolverService().getHomePage(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java index 98220ba4491..4b278a22767 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java @@ -50,7 +50,6 @@ import org.apache.causeway.core.metamodel.interactions.VisibilityContext; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.object.ManagedObjects; -import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory; import org.apache.causeway.core.metamodel.spec.feature.MixedInMember; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.schema.cmd.v2.CommandDto; @@ -322,10 +321,6 @@ protected InteractionProvider getInteractionContext() { return getServiceRegistry().lookupServiceElseFail(InteractionProvider.class); } - protected CommandDtoFactory getCommandDtoFactory() { - return getServiceRegistry().lookupServiceElseFail(CommandDtoFactory.class); - } - @Override public String asciiId() { return getMetaModelContext().getAsciiIdentifierService().asciiIdFor(getId()); diff --git a/core/runtimeservices/src/main/java/module-info.java b/core/runtimeservices/src/main/java/module-info.java index e0ca4c5ce93..e8a87d7f84e 100644 --- a/core/runtimeservices/src/main/java/module-info.java +++ b/core/runtimeservices/src/main/java/module-info.java @@ -79,6 +79,7 @@ requires spring.tx; requires org.apache.causeway.core.codegen.bytebuddy; requires spring.aop; + requires java.management; opens org.apache.causeway.core.runtimeservices.wrapper; opens org.apache.causeway.core.runtimeservices.wrapper.proxy; //to org.apache.causeway.core.codegen.bytebuddy diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerForAsyncWrapMixin.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerForAsyncWrapMixin.java new file mode 100644 index 00000000000..3c5d99285fa --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerForAsyncWrapMixin.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.runtimeservices.wrapper; + +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; + +import org.apache.causeway.applib.services.wrapper.control.AsyncControl; +import org.apache.causeway.commons.internal.reflection._GenericResolver; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.MixedInMember; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator; +import org.apache.causeway.core.runtimeservices.wrapper.handlers.DomainObjectInvocationHandler; + +import lombok.NonNull; +import lombok.val; + +class InvocationHandlerForAsyncWrapMixin extends InvocationHandlerforAsyncAbstract { + + private final @NonNull Object mixeePojo; + + public InvocationHandlerForAsyncWrapMixin( + final MetaModelContext metaModelContext, + final InteractionIdGenerator interactionIdGenerator, + final ExecutorService commonExecutorService, + final @NonNull AsyncControl asyncControl, + final T targetPojo, + final ObjectSpecification targetSpecification, + final @NonNull Object mixeePojo) { + super(metaModelContext, interactionIdGenerator, commonExecutorService, asyncControl, targetPojo, targetSpecification); + this.mixeePojo = mixeePojo; + } + + @Override + public Object invoke(Object proxyObject, Method method, Object[] args) throws Throwable { + + val resolvedMethod = _GenericResolver.resolveMethod(method, targetPojo.getClass()) + .orElseThrow(); // fail early on attempt to invoke method that is not part of the meta-model + + if (isInheritedFromJavaLangObject(method)) { + return method.invoke(targetPojo, args); + } + + if (shouldCheckRules(asyncControl)) { + val doih = new DomainObjectInvocationHandler<>( + metaModelContext, + null, targetSpecification + ); + + try { + doih.invoke(proxyObject, method, args); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + val memberAndTarget = locateMemberAndTarget(resolvedMethod, mixeePojo, targetSpecification); + if (!memberAndTarget.isMemberFound()) { + return method.invoke(targetPojo, args); + } + + return submitAsync(memberAndTarget, args, asyncControl); + } + + MemberAndTarget locateMemberAndTarget( + final _GenericResolver.ResolvedMethod method, + final Q mixeePojo, + final ObjectSpecification targetSpecification + ) { + + final var mixinMember = targetSpecification.getMember(method).orElse(null); + if (mixinMember == null) { + return MemberAndTarget.notFound(); + } + + // find corresponding action of the mixee (this is the 'real' target, the target usable for invocation). + final var mixeeClass = mixeePojo.getClass(); + + // don't care about anything other than actions + // (contributed properties and collections are read-only). + final ObjectAction targetAction = metaModelContext.getSpecificationLoader().specForType(mixeeClass) + .flatMap(mixeeSpec->mixeeSpec.streamAnyActions(MixedIn.ONLY) + .filter(act -> ((MixedInMember)act).hasMixinAction((ObjectAction) mixinMember)) + .findFirst() + ) + .orElseThrow(()->new UnsupportedOperationException(String.format( + "Could not locate objectAction delegating to mixinAction id='%s' on mixee class '%s'", + mixinMember.getId(), mixeeClass.getName()))); + + return MemberAndTarget.foundAction(targetAction, metaModelContext.getObjectManager().adapt(mixeePojo), method.method()); + } + +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncAbstract.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncAbstract.java new file mode 100644 index 00000000000..bc4d58c1f54 --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncAbstract.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.runtimeservices.wrapper; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +import org.springframework.transaction.annotation.Propagation; + +import org.apache.causeway.applib.locale.UserLocale; +import org.apache.causeway.applib.services.bookmark.Bookmark; +import org.apache.causeway.applib.services.iactnlayer.InteractionContext; +import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; +import org.apache.causeway.applib.services.iactnlayer.InteractionService; +import org.apache.causeway.applib.services.wrapper.control.AsyncControl; +import org.apache.causeway.applib.services.wrapper.control.ExecutionMode; +import org.apache.causeway.commons.internal.collections._Lists; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.interactions.InteractionHead; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator; +import org.apache.causeway.schema.cmd.v2.CommandDto; + +import lombok.NonNull; +import lombok.val; + +abstract class InvocationHandlerforAsyncAbstract implements InvocationHandler { + + final MetaModelContext metaModelContext; + final InteractionIdGenerator interactionIdGenerator; + private final ExecutorService commonExecutorService; + final @NonNull AsyncControl asyncControl; + final @NonNull T targetPojo; + final ObjectSpecification targetSpecification; + + public InvocationHandlerforAsyncAbstract( + final MetaModelContext metaModelContext, + final InteractionIdGenerator interactionIdGenerator, + final ExecutorService commonExecutorService, + final @NonNull AsyncControl asyncControl, + final @NonNull T targetPojo, + final ObjectSpecification targetSpecification) { + this.metaModelContext = metaModelContext; + this.interactionIdGenerator = interactionIdGenerator; + this.commonExecutorService = commonExecutorService; + this.asyncControl = asyncControl; + this.targetPojo = targetPojo; + this.targetSpecification = targetSpecification; + } + + boolean isInheritedFromJavaLangObject(final Method method) { + return method.getDeclaringClass().equals(Object.class); + } + + boolean shouldCheckRules(final AsyncControl asyncControl) { + val executionModes = asyncControl.getExecutionModes(); + val skipRules = executionModes.contains(ExecutionMode.SKIP_RULE_VALIDATION); + return !skipRules; + } + + private InteractionLayer currentInteractionLayer() { + return getInteractionService().currentInteractionLayerElseFail(); + } + + private InteractionService getInteractionService() { + return metaModelContext.getInteractionService(); + } + + private CommandDtoFactory getCommandDtoFactory() { + return metaModelContext.getCommandDtoFactory(); + } + + + Object submitAsync( + final MemberAndTarget memberAndTarget, + final Object[] args, + final AsyncControl asyncControl) { + + final var interactionLayer = currentInteractionLayer(); + final var interactionContext = interactionLayer.getInteractionContext(); + final var asyncInteractionContext = interactionContextFrom(asyncControl, interactionContext); + + final var parentCommand = getInteractionService().currentInteractionElseFail().getCommand(); + final var parentInteractionId = parentCommand.getInteractionId(); + + final var targetAdapter = memberAndTarget.getTarget(); + final var method = memberAndTarget.getMethod(); + + final var head = InteractionHead.regular(targetAdapter); + + final var childInteractionId = interactionIdGenerator.interactionId(); + CommandDto childCommandDto; + switch (memberAndTarget.getType()) { + case ACTION: + final var action = memberAndTarget.getAction(); + final var argAdapters = ManagedObject.adaptParameters(action.getParameters(), _Lists.ofArray(args)); + childCommandDto = getCommandDtoFactory() + .asCommandDto(childInteractionId, head, action, argAdapters); + break; + case PROPERTY: + final var property = memberAndTarget.getProperty(); + final var propertyValueAdapter = ManagedObject.adaptProperty(property, args[0]); + childCommandDto = getCommandDtoFactory() + .asCommandDto(childInteractionId, head, property, propertyValueAdapter); + break; + default: + // shouldn't happen, already catered for this case previously + return null; + } + final var oidDto = childCommandDto.getTargets().getOid().get(0); + + asyncControl.setMethod(method); + asyncControl.setBookmark(Bookmark.forOidDto(oidDto)); + + final var executorService = Optional.ofNullable(asyncControl.getExecutorService()) + .orElse(commonExecutorService); + final var asyncTask = metaModelContext.getServiceInjector().injectServicesInto(new WrapperFactoryDefault.AsyncTask( + asyncInteractionContext, + Propagation.REQUIRES_NEW, + childCommandDto, + asyncControl.getReturnType(), + parentInteractionId)); // this command becomes the parent of child command + + final var future = executorService.submit(asyncTask); + asyncControl.setFuture(future); + + return null; + } + + private static InteractionContext interactionContextFrom( + final AsyncControl asyncControl, + final InteractionContext interactionContext) { + + return InteractionContext.builder() + .clock(Optional.ofNullable(asyncControl.getClock()).orElseGet(interactionContext::getClock)) + .locale(Optional.ofNullable(asyncControl.getLocale()).map(UserLocale::valueOf).orElse(null)) // if not set in asyncControl use defaults (set override to null) + .timeZone(Optional.ofNullable(asyncControl.getTimeZone()).orElseGet(interactionContext::getTimeZone)) + .user(Optional.ofNullable(asyncControl.getUser()).orElseGet(interactionContext::getUser)) + .build(); + } + + +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncWrapRegular.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncWrapRegular.java new file mode 100644 index 00000000000..33419c01657 --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/InvocationHandlerforAsyncWrapRegular.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.runtimeservices.wrapper; + +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; + +import org.apache.causeway.applib.services.wrapper.control.AsyncControl; +import org.apache.causeway.commons.internal.reflection._GenericResolver; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator; +import org.apache.causeway.core.runtimeservices.wrapper.handlers.DomainObjectInvocationHandler; + +import lombok.NonNull; +import lombok.val; + +class InvocationHandlerforAsyncWrapRegular extends InvocationHandlerforAsyncAbstract { + + private final ManagedObject targetAdapter; + + public InvocationHandlerforAsyncWrapRegular( + final MetaModelContext metaModelContext, + final InteractionIdGenerator interactionIdGenerator, + final ExecutorService commonExecutorService, + final AsyncControl asyncControl, + final @NonNull T targetPojo, + final ManagedObject targetAdapter) { + super(metaModelContext, interactionIdGenerator, commonExecutorService, asyncControl, targetPojo, targetAdapter.getSpecification()); + this.targetAdapter = targetAdapter; + } + + @Override + public Object invoke(Object proxyObject, Method method, Object[] args) throws Throwable { + + val resolvedMethod = _GenericResolver.resolveMethod(method, targetPojo.getClass()) + .orElseThrow(); // fail early on attempt to invoke method that is not part of the meta-model + + if (isInheritedFromJavaLangObject(method)) { + return method.invoke(targetPojo, args); + } + + if (shouldCheckRules(asyncControl)) { + val doih = new DomainObjectInvocationHandler<>( + metaModelContext, + null, targetSpecification + ); + try { + doih.invoke(proxyObject, method, args); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + val memberAndTarget = locateMemberAndTarget(resolvedMethod, targetAdapter); + if (!memberAndTarget.isMemberFound()) { + return method.invoke(targetPojo, args); + } + + return submitAsync(memberAndTarget, args, asyncControl); + } + + MemberAndTarget locateMemberAndTarget( + final _GenericResolver.ResolvedMethod method, + final ManagedObject targetAdapter) { + + final var objectMember = targetAdapter.getSpecification().getMember(method).orElse(null); + if(objectMember == null) { + return MemberAndTarget.notFound(); + } + + if (objectMember instanceof OneToOneAssociation) { + return MemberAndTarget.foundProperty((OneToOneAssociation) objectMember, targetAdapter, method.method()); + } + if (objectMember instanceof ObjectAction) { + return MemberAndTarget.foundAction((ObjectAction) objectMember, targetAdapter, method.method()); + } + + throw new UnsupportedOperationException( + "Only properties and actions can be executed in the background " + + "(method " + method.name() + " represents a " + objectMember.getFeatureType().name() + "')"); + } +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/MemberAndTarget.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/MemberAndTarget.java new file mode 100644 index 00000000000..a2c15580a21 --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/MemberAndTarget.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.runtimeservices.wrapper; + +import java.lang.reflect.Method; + +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; + +import lombok.Data; + +@Data +class MemberAndTarget { + static MemberAndTarget notFound() { + return new MemberAndTarget(Type.NONE, null, null, null, null); + } + + static MemberAndTarget foundAction(final ObjectAction action, final ManagedObject target, final Method method) { + return new MemberAndTarget(Type.ACTION, action, null, target, method); + } + + static MemberAndTarget foundProperty(final OneToOneAssociation property, final ManagedObject target, final Method method) { + return new MemberAndTarget(Type.PROPERTY, null, property, target, method); + } + + public boolean isMemberFound() { + return type != Type.NONE; + } + + enum Type { + ACTION, + PROPERTY, + NONE + } + + private final Type type; + /** + * Populated if and only if {@link #type} is {@link Type#ACTION}. + */ + private final ObjectAction action; + /** + * Populated if and only if {@link #type} is {@link Type#PROPERTY}. + */ + private final OneToOneAssociation property; + private final ManagedObject target; + private final Method method; +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java index 3b373b3497a..f0015157962 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java @@ -18,14 +18,12 @@ */ package org.apache.causeway.core.runtimeservices.wrapper; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -39,19 +37,15 @@ import javax.inject.Provider; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.applib.locale.UserLocale; -import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.command.CommandExecutorService; import org.apache.causeway.applib.services.factory.FactoryService; import org.apache.causeway.applib.services.iactn.InteractionProvider; import org.apache.causeway.applib.services.iactnlayer.InteractionContext; -import org.apache.causeway.applib.services.iactnlayer.InteractionLayer; import org.apache.causeway.applib.services.iactnlayer.InteractionService; import org.apache.causeway.applib.services.inject.ServiceInjector; import org.apache.causeway.applib.services.repository.RepositoryService; @@ -80,37 +74,29 @@ import org.apache.causeway.applib.services.xactn.TransactionService; import org.apache.causeway.commons.collections.ImmutableEnumSet; import org.apache.causeway.commons.internal.base._Casts; -import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.commons.internal.proxy._ProxyFactoryService; -import org.apache.causeway.commons.internal.reflection._GenericResolver; -import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MixinConstructor; import org.apache.causeway.core.metamodel.context.HasMetaModelContext; import org.apache.causeway.core.metamodel.context.MetaModelContext; -import org.apache.causeway.core.metamodel.interactions.InteractionHead; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.object.ManagedObjects; -import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory; -import org.apache.causeway.core.metamodel.spec.feature.MixedIn; -import org.apache.causeway.core.metamodel.spec.feature.MixedInMember; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator; import org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcher; import org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcherTypeSafe; -import org.apache.causeway.core.runtimeservices.wrapper.handlers.DomainObjectInvocationHandler; import org.apache.causeway.core.runtimeservices.wrapper.handlers.ProxyContextHandler; +import org.apache.causeway.core.runtimeservices.wrapper.handlers.WrapperInvocationContext; import org.apache.causeway.core.runtimeservices.wrapper.proxy.ProxyCreator; import org.apache.causeway.schema.cmd.v2.CommandDto; import static org.apache.causeway.applib.services.wrapper.control.SyncControl.control; -import lombok.Data; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.val; /** @@ -130,7 +116,6 @@ public class WrapperFactoryDefault @Inject private FactoryService factoryService; @Inject @Getter(onMethod_= {@Override}) MetaModelContext metaModelContext; // HasMetaModelContext @Inject protected _ProxyFactoryService proxyFactoryService; // protected: in support of JUnit tests - @Inject @Lazy private CommandDtoFactory commandDtoFactory; @Inject private Provider interactionServiceProvider; @Inject private Provider transactionServiceProvider; @@ -192,8 +177,8 @@ public T wrap( final @NonNull T domainObject, final @NonNull SyncControl syncControl) { - val spec = getSpecificationLoader().specForTypeElseFail(domainObject.getClass()); - if(spec.isMixin()) { + val targetSpecification = getSpecificationLoader().specForTypeElseFail(domainObject.getClass()); + if(targetSpecification.isMixin()) { throw _Exceptions.illegalArgument("cannot wrap a mixin instance directly, " + "use WrapperFactory.wrapMixin(...) instead"); } @@ -205,9 +190,9 @@ public T wrap( return domainObject; } val underlyingDomainObject = wrapperObject.__causeway_wrapped(); - return _Casts.uncheckedCast(createProxy(underlyingDomainObject, syncControl)); + return _Casts.uncheckedCast(createProxy(targetSpecification, underlyingDomainObject, syncControl)); } - return createProxy(domainObject, syncControl); + return createProxy(targetSpecification, domainObject, syncControl); } private static boolean equivalent(final ImmutableEnumSet first, final ImmutableEnumSet second) { @@ -234,33 +219,41 @@ public T wrapMixin( T mixin = factoryService.mixin(mixinClass, mixee); // no need to inject services into the mixin, factoryService does it for us. + final var targetSpecification = getSpecificationLoader().loadSpecification(mixinClass); if (isWrapper(mixee)) { - val wrapperObject = (WrappingObject) mixee; - val executionMode = wrapperObject.__causeway_executionModes(); - val underlyingMixee = wrapperObject.__causeway_wrapped(); + val wrappingObject = (WrappingObject) mixee; + val executionMode = wrappingObject.__causeway_executionModes(); + val underlyingMixee = wrappingObject.__causeway_wrapped(); getServiceInjector().injectServicesInto(underlyingMixee); if(equivalent(executionMode, syncControl.getExecutionModes())) { return mixin; } - return _Casts.uncheckedCast(createMixinProxy(underlyingMixee, mixin, syncControl)); + return _Casts.uncheckedCast(createMixinProxy(targetSpecification, mixin, underlyingMixee, syncControl)); } getServiceInjector().injectServicesInto(mixee); - return createMixinProxy(mixee, mixin, syncControl); + return createMixinProxy(targetSpecification, mixin, mixee, syncControl); } - protected T createProxy(final T domainObject, final SyncControl syncControl) { - val objAdapter = adaptAndGuardAgainstWrappingNotSupported(domainObject); - return proxyContextHandler.proxy(domainObject, objAdapter, syncControl); + + protected T createProxy( + final ObjectSpecification targetSpecification, + final T targetPojo, + final SyncControl syncControl + ) { + return proxyContextHandler.proxy(metaModelContext, targetSpecification, targetPojo, syncControl); } - protected T createMixinProxy(final Object mixee, final T mixin, final SyncControl syncControl) { - val mixeeAdapter = adaptAndGuardAgainstWrappingNotSupported(mixee); - val mixinAdapter = adaptAndGuardAgainstWrappingNotSupported(mixin); - return proxyContextHandler.mixinProxy(mixin, mixeeAdapter, mixinAdapter, syncControl); + protected T createMixinProxy( + final ObjectSpecification targetSpecification, + final T targetMixinPojo, + final Object mixeePojo, + final SyncControl syncControl + ) { + return proxyContextHandler.mixinProxy(metaModelContext, targetSpecification, targetMixinPojo, mixeePojo, syncControl); } @Override @@ -280,254 +273,53 @@ public T unwrap(final T possibleWrappedDomainObject) { // -- ASYNC WRAPPING - + @SneakyThrows @Override public T asyncWrap( - final @NonNull T domainObject, + final @NonNull T targetPojo, final AsyncControl asyncControl) { - val targetAdapter = adaptAndGuardAgainstWrappingNotSupported(domainObject); - if(targetAdapter.getSpecification().isMixin()) { + val targetAdapter = adaptAndGuardAgainstWrappingNotSupported(targetPojo); + final var targetSpecification = targetAdapter.getSpecification(); + if(targetSpecification.isMixin()) { throw _Exceptions.illegalArgument("cannot wrap a mixin instance directly, " + "use WrapperFactory.asyncWrapMixin(...) instead"); } val proxyFactory = proxyFactoryService - .factory(_Casts.uncheckedCast(domainObject.getClass()), WrappingObject.class); - - return proxyFactory.createInstance((proxy, method, args) -> { - - val resolvedMethod = _GenericResolver.resolveMethod(method, domainObject.getClass()) - .orElseThrow(); // fail early on attempt to invoke method that is not part of the meta-model - - if (isInheritedFromJavaLangObject(method)) { - return method.invoke(domainObject, args); - } + .factory(_Casts.uncheckedCast(targetPojo.getClass()), WrappingObject.class); - if (shouldCheckRules(asyncControl)) { - val doih = new DomainObjectInvocationHandler<>( - domainObject, - null, // mixeeAdapter ignored - targetAdapter, - control().withNoExecute(), - null); - doih.invoke(null, method, args); - } - - val memberAndTarget = memberAndTargetForRegular(resolvedMethod, targetAdapter); - if( ! memberAndTarget.isMemberFound()) { - return method.invoke(domainObject, args); - } + final T proxyObject = proxyFactory.createInstance(new InvocationHandlerforAsyncWrapRegular<>(this.metaModelContext, interactionIdGenerator, commonExecutorService, asyncControl, targetPojo, targetAdapter), false); - return submitAsync(memberAndTarget, args, asyncControl); - }, false); - } + WrapperInvocationContext.set(proxyObject, new WrapperInvocationContext(targetPojo, null, control().withNoExecute(), asyncControl)); - private boolean shouldCheckRules(final AsyncControl asyncControl) { - val executionModes = asyncControl.getExecutionModes(); - val skipRules = executionModes.contains(ExecutionMode.SKIP_RULE_VALIDATION); - return !skipRules; + return proxyObject; } + @SneakyThrows @Override public T asyncWrapMixin( final @NonNull Class mixinClass, - final @NonNull Object mixee, + final @NonNull Object mixeePojo, final @NonNull AsyncControl asyncControl) { - T mixin = factoryService.mixin(mixinClass, mixee); + final T targetMixinPojo = factoryService.mixin(mixinClass, mixeePojo); - val mixeeAdapter = adaptAndGuardAgainstWrappingNotSupported(mixee); - val mixinAdapter = adaptAndGuardAgainstWrappingNotSupported(mixin); + final var targetSpecification = getSpecificationLoader().loadSpecification(mixinClass); - val mixinConstructor = MixinConstructor.PUBLIC_SINGLE_ARG_RECEIVING_MIXEE - .getConstructorElseFail(mixinClass, mixee.getClass()); + final val mixinConstructor = MixinConstructor.PUBLIC_SINGLE_ARG_RECEIVING_MIXEE + .getConstructorElseFail(mixinClass, mixeePojo.getClass()); - val proxyFactory = proxyFactoryService + final val proxyFactory = proxyFactoryService .factory(mixinClass, new Class[]{WrappingObject.class}, mixinConstructor.getParameterTypes()); - return proxyFactory.createInstance((proxy, method, args) -> { - - val resolvedMethod = _GenericResolver.resolveMethod(method, mixinClass) - .orElseThrow(); // fail early on attempt to invoke method that is not part of the meta-model - - final boolean inheritedFromObject = isInheritedFromJavaLangObject(method); - if (inheritedFromObject) { - return method.invoke(mixin, args); - } - - if (shouldCheckRules(asyncControl)) { - val doih = new DomainObjectInvocationHandler<>( - mixin, - mixeeAdapter, - mixinAdapter, - control().withNoExecute(), - null); - doih.invoke(null, method, args); - } - - val actionAndTarget = memberAndTargetForMixin(resolvedMethod, mixee, mixinAdapter); - if (! actionAndTarget.isMemberFound()) { - return method.invoke(mixin, args); - } - - return submitAsync(actionAndTarget, args, asyncControl); - }, new Object[]{ mixee }); - } - - private boolean isInheritedFromJavaLangObject(final Method method) { - return method.getDeclaringClass().equals(Object.class); - } - - private Object submitAsync( - final MemberAndTarget memberAndTarget, - final Object[] args, - final AsyncControl asyncControl) { - - val interactionLayer = currentInteractionLayer(); - val interactionContext = interactionLayer.getInteractionContext(); - val asyncInteractionContext = interactionContextFrom(asyncControl, interactionContext); - - val parentCommand = getInteractionService().currentInteractionElseFail().getCommand(); - val parentInteractionId = parentCommand.getInteractionId(); - - val targetAdapter = memberAndTarget.getTarget(); - val method = memberAndTarget.getMethod(); - - val head = InteractionHead.regular(targetAdapter); - - val childInteractionId = interactionIdGenerator.interactionId(); - CommandDto childCommandDto; - switch (memberAndTarget.getType()) { - case ACTION: - val action = memberAndTarget.getAction(); - val argAdapters = ManagedObject.adaptParameters(action.getParameters(), _Lists.ofArray(args)); - childCommandDto = commandDtoFactory - .asCommandDto(childInteractionId, head, action, argAdapters); - break; - case PROPERTY: - val property = memberAndTarget.getProperty(); - val propertyValueAdapter = ManagedObject.adaptProperty(property, args[0]); - childCommandDto = commandDtoFactory - .asCommandDto(childInteractionId, head, property, propertyValueAdapter); - break; - default: - // shouldn't happen, already catered for this case previously - return null; - } - val oidDto = childCommandDto.getTargets().getOid().get(0); - - asyncControl.setMethod(method); - asyncControl.setBookmark(Bookmark.forOidDto(oidDto)); - - val executorService = Optional.ofNullable(asyncControl.getExecutorService()) - .orElse(commonExecutorService); - val asyncTask = getServiceInjector().injectServicesInto(new AsyncTask( - asyncInteractionContext, - Propagation.REQUIRES_NEW, - childCommandDto, - asyncControl.getReturnType(), - parentInteractionId)); // this command becomes the parent of child command + final T proxyObject = proxyFactory.createInstance(new InvocationHandlerForAsyncWrapMixin<>(this.metaModelContext, interactionIdGenerator, commonExecutorService, asyncControl, targetMixinPojo, targetSpecification, mixeePojo), new Object[]{mixeePojo}); - val future = executorService.submit(asyncTask); - asyncControl.setFuture(future); + WrapperInvocationContext.set(proxyObject, new WrapperInvocationContext(targetMixinPojo, mixeePojo, control().withNoExecute(), asyncControl)); - return null; + return proxyObject; } - private MemberAndTarget memberAndTargetForRegular( - final ResolvedMethod method, - final ManagedObject targetAdapter) { - - val objectMember = targetAdapter.getSpecification().getMember(method).orElse(null); - if(objectMember == null) { - return MemberAndTarget.notFound(); - } - - if (objectMember instanceof OneToOneAssociation) { - return MemberAndTarget.foundProperty((OneToOneAssociation) objectMember, targetAdapter, method.method()); - } - if (objectMember instanceof ObjectAction) { - return MemberAndTarget.foundAction((ObjectAction) objectMember, targetAdapter, method.method()); - } - - throw new UnsupportedOperationException( - "Only properties and actions can be executed in the background " - + "(method " + method.name() + " represents a " + objectMember.getFeatureType().name() + "')"); - } - - private MemberAndTarget memberAndTargetForMixin( - final ResolvedMethod method, - final T mixee, - final ManagedObject mixinAdapter) { - - val mixinMember = mixinAdapter.getSpecification().getMember(method).orElse(null); - if (mixinMember == null) { - return MemberAndTarget.notFound(); - } - - // find corresponding action of the mixee (this is the 'real' target, the target usable for invocation). - val mixeeClass = mixee.getClass(); - - // don't care about anything other than actions - // (contributed properties and collections are read-only). - final ObjectAction targetAction = getSpecificationLoader().specForType(mixeeClass) - .flatMap(mixeeSpec->mixeeSpec.streamAnyActions(MixedIn.ONLY) - .filter(act -> ((MixedInMember)act).hasMixinAction((ObjectAction) mixinMember)) - .findFirst() - ) - .orElseThrow(()->new UnsupportedOperationException(String.format( - "Could not locate objectAction delegating to mixinAction id='%s' on mixee class '%s'", - mixinMember.getId(), mixeeClass.getName()))); - - return MemberAndTarget.foundAction(targetAction, getObjectManager().adapt(mixee), method.method()); - } - - private static InteractionContext interactionContextFrom( - final AsyncControl asyncControl, - final InteractionContext interactionContext) { - - return InteractionContext.builder() - .clock(Optional.ofNullable(asyncControl.getClock()).orElseGet(interactionContext::getClock)) - .locale(Optional.ofNullable(asyncControl.getLocale()).map(UserLocale::valueOf).orElse(null)) // if not set in asyncControl use defaults (set override to null) - .timeZone(Optional.ofNullable(asyncControl.getTimeZone()).orElseGet(interactionContext::getTimeZone)) - .user(Optional.ofNullable(asyncControl.getUser()).orElseGet(interactionContext::getUser)) - .build(); - } - - @Data - static class MemberAndTarget { - static MemberAndTarget notFound() { - return new MemberAndTarget(Type.NONE, null, null, null, null); - } - static MemberAndTarget foundAction(final ObjectAction action, final ManagedObject target, final Method method) { - return new MemberAndTarget(Type.ACTION, action, null, target, method); - } - static MemberAndTarget foundProperty(final OneToOneAssociation property, final ManagedObject target, final Method method) { - return new MemberAndTarget(Type.PROPERTY, null, property, target, method); - } - - public boolean isMemberFound() { - return type != Type.NONE; - } - - enum Type { - ACTION, - PROPERTY, - NONE - } - private final Type type; - /** - * Populated if and only if {@link #type} is {@link Type#ACTION}. - */ - private final ObjectAction action; - /** - * Populated if and only if {@link #type} is {@link Type#PROPERTY}. - */ - private final OneToOneAssociation property; - private final ManagedObject target; - private final Method method; - } // -- LISTENERS @@ -548,9 +340,9 @@ public boolean removeInteractionListener(final InteractionListener listener) { @Override public void notifyListeners(final InteractionEvent interactionEvent) { - val dispatcher = dispatchersByEventClass.get(interactionEvent.getClass()); + final var dispatcher = dispatchersByEventClass.get(interactionEvent.getClass()); if (dispatcher == null) { - val msg = String.format("Unknown InteractionEvent %s - " + final var msg = String.format("Unknown InteractionEvent %s - " + "needs registering into dispatchers map", interactionEvent.getClass()); throw _Exceptions.unrecoverable(msg); } @@ -562,7 +354,7 @@ public void notifyListeners(final InteractionEvent interactionEvent) { private ManagedObject adaptAndGuardAgainstWrappingNotSupported( final @NonNull Object domainObject) { - val adapter = getObjectManager().adapt(domainObject); + final var adapter = getObjectManager().adapt(domainObject); if(ManagedObjects.isNullOrUnspecifiedOrEmpty(adapter) || !adapter.getSpecification().getBeanSort().isWrappingSupported()) { throw _Exceptions.illegalArgument("Cannot wrap an object of type %s", @@ -577,7 +369,7 @@ private ManagedObject adaptAndGuardAgainstWrappingNotSupported( private void putDispatcher( final Class type, final BiConsumer onDispatch) { - val dispatcher = new InteractionEventDispatcherTypeSafe() { + final var dispatcher = new InteractionEventDispatcherTypeSafe() { @Override public void dispatchTypeSafe(final T interactionEvent) { for (InteractionListener l : listeners) { @@ -589,12 +381,9 @@ public void dispatchTypeSafe(final T interactionEvent) { dispatchersByEventClass.put(type, dispatcher); } - private InteractionLayer currentInteractionLayer() { - return getInteractionService().currentInteractionLayerElseFail(); - } - @RequiredArgsConstructor - private static class AsyncTask implements AsyncCallable { + @RequiredArgsConstructor(onConstructor_ = {@Inject}) + static class AsyncTask implements AsyncCallable { private static final long serialVersionUID = 1L; @@ -662,13 +451,13 @@ private R updateDomainObjectHonoringTransactionalPropagation(final AsyncCall private R updateDomainObject(final AsyncCallable asyncCallable) { // obtain the Command that is implicitly created (initially mainly empty) whenever an Interaction is started. - val childCommand = interactionProviderProvider.get().currentInteractionElseFail().getCommand(); + final var childCommand = interactionProviderProvider.get().currentInteractionElseFail().getCommand(); // we will "take over" this Command, updating it with the parentInteractionId of the command for the action // that called WrapperFactory#asyncMixin in the first place. childCommand.updater().setParentInteractionId(asyncCallable.getParentInteractionId()); - val tryBookmark = commandExecutorServiceProvider.get().executeCommand(asyncCallable.getCommandDto()); + final var tryBookmark = commandExecutorServiceProvider.get().executeCommand(asyncCallable.getCommandDto()); return tryBookmark.fold( throwable -> null, // failure @@ -688,6 +477,7 @@ private R updateDomainObject(final AsyncCallable asyncCallable) { private final static int MIN_POOL_SIZE = 2; // at least 2 private final static int MAX_POOL_SIZE = 4; // max 4 + private ExecutorService newCommonExecutorService() { final int poolSize = Math.min( MAX_POOL_SIZE, @@ -696,5 +486,4 @@ private ExecutorService newCommonExecutorService() { Runtime.getRuntime().availableProcessors())); return Executors.newFixedThreadPool(poolSize); } - } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CollectionInvocationHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CollectionInvocationHandler.java index bc4c97d1051..3ec189eaa28 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CollectionInvocationHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CollectionInvocationHandler.java @@ -28,11 +28,12 @@ class CollectionInvocationHandler> extends PluralInvocationHandlerAbstract { public CollectionInvocationHandler( + final Object proxyObject, final C collectionToBeProxied, final DomainObjectInvocationHandler handler, final OneToManyAssociation otma) { - super(collectionToBeProxied, handler, otma, + super(proxyObject, collectionToBeProxied, handler, otma, CollectionSemantics .valueOfElseFail(collectionToBeProxied.getClass())); diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandler.java index 915c70fd468..c0cca581177 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandler.java @@ -25,10 +25,8 @@ */ public interface DelegatingInvocationHandler extends InvocationHandler { - T getDelegate(); + Class getTargetClass(); - public boolean isResolveObjectChangedEnabled(); - - public void setResolveObjectChangedEnabled(boolean resolveObjectChangedEnabled); + T getTarget(Object proxyObject); } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerAbstract.java similarity index 54% rename from core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerDefault.java rename to core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerAbstract.java index 8c6d6efe5bd..5575100b92a 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DelegatingInvocationHandlerAbstract.java @@ -21,53 +21,42 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import org.apache.causeway.applib.services.wrapper.WrapperFactory; -import org.apache.causeway.applib.services.wrapper.control.SyncControl; import org.apache.causeway.applib.services.wrapper.events.InteractionEvent; import org.apache.causeway.commons.internal._Constants; -import org.apache.causeway.commons.internal.base._Blackhole; +import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.object.ManagedObjects; -import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; import lombok.Getter; import lombok.NonNull; -import lombok.Setter; +import lombok.val; /** * @param */ -public class DelegatingInvocationHandlerDefault implements DelegatingInvocationHandler { - - private ObjectManager objectManager; +public abstract class DelegatingInvocationHandlerAbstract implements DelegatingInvocationHandler { // getter is API - @Getter(onMethod = @__(@Override)) private final T delegate; - @Getter protected final WrapperFactory wrapperFactory; - @Getter private final SyncControl syncControl; + @Getter protected final MetaModelContext metaModelContext; + + @Getter(onMethod_ = {@Override}) + private final Class targetClass; protected final Method equalsMethod; protected final Method hashCodeMethod; protected final Method toStringMethod; - // getter and setter are API - @Getter(onMethod = @__(@Override)) @Setter(onMethod = @__(@Override)) - private boolean resolveObjectChangedEnabled; - - public DelegatingInvocationHandlerDefault( + public DelegatingInvocationHandlerAbstract( final @NonNull MetaModelContext metaModelContext, - final @NonNull T delegate, - final SyncControl syncControl) { - this.delegate = delegate; - this.objectManager = metaModelContext.getObjectManager(); - this.wrapperFactory = metaModelContext.getWrapperFactory(); - this.syncControl = syncControl; + final Class targetClass) { + this.metaModelContext = metaModelContext; + this.targetClass = targetClass; try { - equalsMethod = delegate.getClass().getMethod("equals", _Constants.classesOfObject); - hashCodeMethod = delegate.getClass().getMethod("hashCode", _Constants.emptyClasses); - toStringMethod = delegate.getClass().getMethod("toString", _Constants.emptyClasses); + equalsMethod = this.targetClass.getMethod("equals", _Constants.classesOfObject); + hashCodeMethod = this.targetClass.getMethod("hashCode", _Constants.emptyClasses); + toStringMethod = this.targetClass.getMethod("toString", _Constants.emptyClasses); } catch (final NoSuchMethodException e) { // ///CLOVER:OFF throw new RuntimeException("An Object method could not be found: " + e.getMessage()); @@ -75,28 +64,24 @@ public DelegatingInvocationHandlerDefault( } } - protected void resolveIfRequired(final ManagedObject adapter) { + protected ManagedObject adaptAndGuardAgainstWrappingNotSupported(final Object domainObject) { - if(!resolveObjectChangedEnabled) { - return; + if(domainObject == null) { + return null; } - if(adapter==null) { - return; + val adapter = metaModelContext.getObjectManager().adapt(domainObject); + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(adapter) + || !adapter.getSpecification().getBeanSort().isWrappingSupported()) { + throw _Exceptions.illegalArgument("Cannot wrap an object of type %s", + domainObject.getClass().getName()); } - if(!ManagedObjects.isEntity(adapter)) { - return; - } - - _Blackhole.consume(adapter.getPojo()); - } - protected void resolveIfRequired(final Object domainObject) { - resolveIfRequired(objectManager.adapt(domainObject)); + return adapter; } - protected Object delegate(final Method method, final Object[] args) + protected Object delegate(Object proxyObject, final Method method, final Object[] args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { - return method.invoke(getDelegate(), args); + return method.invoke(getTarget(proxyObject), args); } protected boolean isObjectMethod(final Method method) { @@ -109,7 +94,7 @@ public Object invoke(final Object object, final Method method, final Object[] ar } protected InteractionEvent notifyListeners(final InteractionEvent interactionEvent) { - wrapperFactory.notifyListeners(interactionEvent); + metaModelContext.getWrapperFactory().notifyListeners(interactionEvent); return interactionEvent; } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java index 7492b146c3a..2b4439245bd 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.core.runtimeservices.wrapper.handlers; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.Map; @@ -51,13 +52,11 @@ import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facets.ImperativeFacet; import org.apache.causeway.core.metamodel.facets.ImperativeFacet.Intent; -import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet; import org.apache.causeway.core.metamodel.interactions.managed.ActionInteractionHead; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.object.MmAssertionUtils; import org.apache.causeway.core.metamodel.object.MmEntityUtils; import org.apache.causeway.core.metamodel.object.MmUnwrapUtils; -import org.apache.causeway.core.metamodel.objectmanager.ObjectManager; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.MixedInMember; @@ -67,6 +66,7 @@ import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.core.metamodel.util.Facets; +import lombok.EqualsAndHashCode; import lombok.SneakyThrows; import lombok.val; import lombok.extern.log4j.Log4j2; @@ -75,12 +75,15 @@ * * @param */ +@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) @Log4j2 public class DomainObjectInvocationHandler -extends DelegatingInvocationHandlerDefault { +extends DelegatingInvocationHandlerAbstract { private final ProxyContextHandler proxyContextHandler; - private final MetaModelContext mmContext; + + @EqualsAndHashCode.Include // this is the only state that is significant to distinguish one handler from another + private final ObjectSpecification targetSpecification; /** * The title() method; may be null. @@ -102,25 +105,21 @@ public class DomainObjectInvocationHandler */ protected Method __causeway_executionModes; - private final EntityFacet entityFacet; - private final ManagedObject mixeeAdapter; public DomainObjectInvocationHandler( - final T domainObject, - final ManagedObject mixeeAdapter, // ignored if not handling a mixin - final ManagedObject targetAdapter, - final SyncControl syncControl, - final ProxyContextHandler proxyContextHandler) { + final MetaModelContext metaModelContext, + final ProxyContextHandler proxyContextHandler, + final ObjectSpecification targetSpecification + ) { super( - targetAdapter.getSpecification().getMetaModelContext(), - domainObject, - syncControl); - - this.mmContext = targetAdapter.getSpecification().getMetaModelContext(); + metaModelContext, + (Class) targetSpecification.getCorrespondingClass() + ); this.proxyContextHandler = proxyContextHandler; + this.targetSpecification = targetSpecification; try { - titleMethod = getDelegate().getClass().getMethod("title", _Constants.emptyClasses); + titleMethod = getTargetClass().getMethod("title", _Constants.emptyClasses); } catch (final NoSuchMethodException e) { // ignore } @@ -128,38 +127,60 @@ public DomainObjectInvocationHandler( __causeway_saveMethod = WrappingObject.class.getMethod("__causeway_save", _Constants.emptyClasses); __causeway_wrappedMethod = WrappingObject.class.getMethod("__causeway_wrapped", _Constants.emptyClasses); __causeway_executionModes = WrappingObject.class.getMethod("__causeway_executionModes", _Constants.emptyClasses); - - } catch (final NoSuchMethodException nsme) { throw new IllegalStateException( "Could not locate reserved declared methods in the WrappingObject interfaces", nsme); } - - entityFacet = targetAdapter.getSpecification().entityFacet().orElse(null); - - this.mixeeAdapter = mixeeAdapter; } /** * - * @param proxyObjectUnused - not used. + * @param proxyObject - not used. * @param method - the method invoked on the proxy * @param args - the args to the method invoked on the proxy * @throws Throwable */ @Override - public Object invoke(final Object proxyObjectUnused, final Method method, final Object[] args) throws Throwable { + public Object invoke(final Object proxyObject, final Method method, final Object[] args) throws Throwable { + + final var wic = WrapperInvocationContext.get(proxyObject); + if(wic != null) { + return doInvoke(proxyObject, method, args); + } + + throw new IllegalStateException("Unable to find the wrapper invocation context"); + } + + /** + * The target, either a domain object or mixin instance (wrapping a mixee). + * @return + */ + @Override + public T getTarget(Object proxyObject) { + return (T) WrapperInvocationContext.get(proxyObject).targetPojo; + } + + + public ManagedObject getMixeeAdapter(Object proxyObject) { + Object mixeePojo = WrapperInvocationContext.get(proxyObject).mixeePojo; + return adaptAndGuardAgainstWrappingNotSupported(mixeePojo); + } + + public SyncControl getSyncControl(Object proxyObject) { + return WrapperInvocationContext.get(proxyObject).syncControl; + } + private Object doInvoke(Object proxyObject, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException { if (isObjectMethod(method)) { - return delegate(method, args); + return delegate(proxyObject, method, args); } if(isEnhancedEntityMethod(method)) { - return delegate(method, args); + return delegate(proxyObject, method, args); } - final ManagedObject targetAdapter = getObjectManager().adapt(getDelegate()); + final ManagedObject targetAdapter = metaModelContext.getObjectManager().adapt(getTarget(proxyObject)); if(!targetAdapter.getSpecialization().isMixin()) { MmAssertionUtils.assertIsBookmarkSupported(targetAdapter); @@ -169,24 +190,23 @@ public Object invoke(final Object proxyObjectUnused, final Method method, final return handleTitleMethod(targetAdapter); } - final ObjectSpecification targetSpec = targetAdapter.getSpecification(); - val resolvedMethod = _GenericResolver.resolveMethod(method, targetSpec.getCorrespondingClass()) + val resolvedMethod = _GenericResolver.resolveMethod(method, targetSpecification.getCorrespondingClass()) .orElseThrow(); // save method, through the proxy if (method.equals(__causeway_saveMethod)) { - return handleSaveMethod(targetAdapter, targetSpec); + return handleSaveMethod(proxyObject, targetAdapter, targetSpecification); } if (method.equals(__causeway_wrappedMethod)) { - return getDelegate(); + return getTarget(proxyObject); } if (method.equals(__causeway_executionModes)) { - return getSyncControl().getExecutionModes(); + return getSyncControl(proxyObject).getExecutionModes(); } - val objectMember = targetSpec.getMemberElseFail(resolvedMethod); + val objectMember = targetSpecification.getMemberElseFail(resolvedMethod); val memberId = objectMember.getId(); val intent = ImperativeFacet.getIntent(objectMember, resolvedMethod); @@ -195,7 +215,7 @@ public Object invoke(final Object proxyObjectUnused, final Method method, final } if (intent == Intent.DEFAULTS || intent == Intent.CHOICES_OR_AUTOCOMPLETE) { - return method.invoke(getDelegate(), args); + return method.invoke(getTarget(proxyObject), args); } if (objectMember.isOneToOneAssociation()) { @@ -207,11 +227,11 @@ public Object invoke(final Object proxyObjectUnused, final Method method, final final OneToOneAssociation otoa = (OneToOneAssociation) objectMember; if (intent == Intent.ACCESSOR) { - return handleGetterMethodOnProperty(targetAdapter, args, otoa); + return handleGetterMethodOnProperty(proxyObject, targetAdapter, args, otoa); } if (intent == Intent.MODIFY_PROPERTY || intent == Intent.INITIALIZATION) { - return handleSetterMethodOnProperty(targetAdapter, args, otoa); + return handleSetterMethodOnProperty(proxyObject, targetAdapter, args, otoa); } } if (objectMember.isOneToManyAssociation()) { @@ -222,7 +242,7 @@ public Object invoke(final Object proxyObjectUnused, final Method method, final final OneToManyAssociation otma = (OneToManyAssociation) objectMember; if (intent == Intent.ACCESSOR) { - return handleGetterMethodOnCollection(targetAdapter, args, otma, memberId); + return handleGetterMethodOnCollection(proxyObject, targetAdapter, args, otma, memberId); } } @@ -234,34 +254,34 @@ public Object invoke(final Object proxyObjectUnused, final Method method, final val objectAction = (ObjectAction) objectMember; - if(Facets.mixinIsPresent(targetSpec)) { - if (mixeeAdapter == null) { + if(Facets.mixinIsPresent(targetSpecification)) { + if (getMixeeAdapter(proxyObject) == null) { throw _Exceptions.illegalState( "Missing the required mixeeAdapter for action '%s'", objectAction.getId()); } - MmAssertionUtils.assertIsBookmarkSupported(mixeeAdapter); + MmAssertionUtils.assertIsBookmarkSupported(getMixeeAdapter(proxyObject)); - final ObjectMember mixinMember = determineMixinMember(mixeeAdapter, objectAction); + final ObjectMember mixinMember = determineMixinMember(getMixeeAdapter(proxyObject), objectAction); if (mixinMember != null) { if(mixinMember instanceof ObjectAction) { - return handleActionMethod(mixeeAdapter, args, (ObjectAction)mixinMember); + return handleActionMethod(proxyObject, getMixeeAdapter(proxyObject), args, (ObjectAction) mixinMember); } if(mixinMember instanceof OneToOneAssociation) { - return handleGetterMethodOnProperty(mixeeAdapter, new Object[0], (OneToOneAssociation)mixinMember); + return handleGetterMethodOnProperty(proxyObject, getMixeeAdapter(proxyObject), new Object[0], (OneToOneAssociation) mixinMember); } if(mixinMember instanceof OneToManyAssociation) { - return handleGetterMethodOnCollection(mixeeAdapter, new Object[0], (OneToManyAssociation)mixinMember, memberId); + return handleGetterMethodOnCollection(proxyObject, getMixeeAdapter(proxyObject), new Object[0], (OneToManyAssociation) mixinMember, memberId); } } else { throw _Exceptions.illegalState(String.format( - "Could not locate mixin member for action '%s' on spec '%s'", objectAction.getId(), targetSpec)); + "Could not locate mixin member for action '%s' on spec '%s'", objectAction.getId(), targetSpecification)); } } // this is just a regular non-mixin action. - return handleActionMethod(targetAdapter, args, objectAction); + return handleActionMethod(proxyObject, targetAdapter, args, objectAction); } throw new UnsupportedOperationException(String.format("Unknown member type '%s'", objectMember)); @@ -289,23 +309,21 @@ private static ObjectMember determineMixinMember( // throw new RuntimeException("Unable to find the mixed-in action corresponding to " + objectAction.getIdentifier().toFullIdentityString()); } - public InteractionInitiatedBy getInteractionInitiatedBy() { - return shouldEnforceRules() + public InteractionInitiatedBy getInteractionInitiatedBy(Object proxyObject) { + return shouldEnforceRules(proxyObject) ? InteractionInitiatedBy.USER : InteractionInitiatedBy.FRAMEWORK; } private boolean isEnhancedEntityMethod(final Method method) { - return entityFacet!=null - ? entityFacet.isProxyEnhancement(method) - : false; + return targetSpecification.entityFacet() + .map(x -> x.isProxyEnhancement(method)) + .orElse(false); } private Object handleTitleMethod(final ManagedObject targetAdapter) { - resolveIfRequired(targetAdapter); - val targetNoSpec = targetAdapter.getSpecification(); val titleContext = targetNoSpec .createTitleInteractionContext(targetAdapter, InteractionInitiatedBy.FRAMEWORK); @@ -316,18 +334,18 @@ private Object handleTitleMethod(final ManagedObject targetAdapter) { private Object handleSaveMethod( - final ManagedObject targetAdapter, final ObjectSpecification targetNoSpec) { + Object proxyObject, final ManagedObject targetAdapter, final ObjectSpecification targetNoSpec) { - runValidationTask(()->{ + runValidationTask(proxyObject, ()->{ val interactionResult = - targetNoSpec.isValidResult(targetAdapter, getInteractionInitiatedBy()); + targetNoSpec.isValidResult(targetAdapter, getInteractionInitiatedBy(proxyObject)); notifyListenersAndVetoIfRequired(interactionResult); }); val spec = targetAdapter.getSpecification(); if(spec.isEntity()) { - return runExecutionTask(()->{ + return runExecutionTask(proxyObject, ()->{ MmEntityUtils.persistInCurrentTransaction(targetAdapter); return null; }); @@ -338,28 +356,27 @@ private Object handleSaveMethod( private Object handleGetterMethodOnProperty( + final Object proxyObject, final ManagedObject targetAdapter, final Object[] args, final OneToOneAssociation property) { zeroArgsElseThrow(args, "get"); - runValidationTask(()->{ - checkVisibility(targetAdapter, property); + runValidationTask(proxyObject, ()->{ + checkVisibility(proxyObject, targetAdapter, property); }); - resolveIfRequired(targetAdapter); - - return runExecutionTask(()->{ + return runExecutionTask(proxyObject, ()->{ - val interactionInitiatedBy = getInteractionInitiatedBy(); + val interactionInitiatedBy = getInteractionInitiatedBy(proxyObject); val currentReferencedAdapter = property.get(targetAdapter, interactionInitiatedBy); val currentReferencedObj = MmUnwrapUtils.single(currentReferencedAdapter); val propertyAccessEvent = new PropertyAccessEvent( - getDelegate(), property.getFeatureIdentifier(), currentReferencedObj); + getTarget(proxyObject), property.getFeatureIdentifier(), currentReferencedObj); notifyListeners(propertyAccessEvent); return currentReferencedObj; @@ -370,30 +387,29 @@ private Object handleGetterMethodOnProperty( private Object handleSetterMethodOnProperty( + final Object proxyObject, final ManagedObject targetAdapter, final Object[] args, final OneToOneAssociation property) { val singleArg = singleArgUnderlyingElseNull(args, "setter"); - runValidationTask(()->{ - checkVisibility(targetAdapter, property); - checkUsability(targetAdapter, property); + runValidationTask(proxyObject, ()->{ + checkVisibility(proxyObject, targetAdapter, property); + checkUsability(proxyObject, targetAdapter, property); }); - val argumentAdapter = getObjectManager().adapt(singleArg); + val argumentAdapter = metaModelContext.getObjectManager().adapt(singleArg); - resolveIfRequired(targetAdapter); - - runValidationTask(()->{ + runValidationTask(proxyObject, ()->{ val interactionResult = property.isAssociationValid( - targetAdapter, argumentAdapter, getInteractionInitiatedBy()) + targetAdapter, argumentAdapter, getInteractionInitiatedBy(proxyObject)) .getInteractionResult(); notifyListenersAndVetoIfRequired(interactionResult); }); - return runExecutionTask(()->{ - property.set(targetAdapter, argumentAdapter, getInteractionInitiatedBy()); + return runExecutionTask(proxyObject, ()->{ + property.set(targetAdapter, argumentAdapter, getInteractionInitiatedBy(proxyObject)); return null; }); @@ -402,6 +418,7 @@ targetAdapter, argumentAdapter, getInteractionInitiatedBy()) private Object handleGetterMethodOnCollection( + final Object proxyObject, final ManagedObject targetAdapter, final Object[] args, final OneToManyAssociation collection, @@ -409,28 +426,26 @@ private Object handleGetterMethodOnCollection( zeroArgsElseThrow(args, "get"); - runValidationTask(()->{ - checkVisibility(targetAdapter, collection); + runValidationTask(proxyObject, ()->{ + checkVisibility(proxyObject, targetAdapter, collection); }); - resolveIfRequired(targetAdapter); - - return runExecutionTask(()->{ + return runExecutionTask(proxyObject, ()->{ - val interactionInitiatedBy = getInteractionInitiatedBy(); + val interactionInitiatedBy = getInteractionInitiatedBy(proxyObject); val currentReferencedAdapter = collection.get(targetAdapter, interactionInitiatedBy); val currentReferencedObj = MmUnwrapUtils.single(currentReferencedAdapter); - val collectionAccessEvent = new CollectionAccessEvent(getDelegate(), collection.getFeatureIdentifier()); + val collectionAccessEvent = new CollectionAccessEvent(getTarget(proxyObject), collection.getFeatureIdentifier()); if (currentReferencedObj instanceof Collection) { val collectionViewObject = lookupWrappingObject( - (Collection) currentReferencedObj, collection); + proxyObject, (Collection) currentReferencedObj, collection); notifyListeners(collectionAccessEvent); return collectionViewObject; } else if (currentReferencedObj instanceof Map) { - val mapViewObject = lookupWrappingObject((Map) currentReferencedObj, + val mapViewObject = lookupWrappingObject(proxyObject, (Map) currentReferencedObj, collection); notifyListeners(collectionAccessEvent); return mapViewObject; @@ -444,6 +459,7 @@ private Object handleGetterMethodOnCollection( } private Collection lookupWrappingObject( + final Object proxyObject, final Collection collectionToLookup, final OneToManyAssociation otma) { if (collectionToLookup instanceof WrappingObject) { @@ -453,10 +469,11 @@ private Collection lookupWrappingObject( throw new IllegalStateException("Unable to create proxy for collection; " + "proxyContextHandler not provided"); } - return proxyContextHandler.proxy(collectionToLookup, this, otma); + return proxyContextHandler.proxy(proxyObject, collectionToLookup, this, otma); } private Map lookupWrappingObject( + final Object proxyObject, final Map mapToLookup, final OneToManyAssociation otma) { if (mapToLookup instanceof WrappingObject) { @@ -466,18 +483,19 @@ private Collection lookupWrappingObject( throw new IllegalStateException("Unable to create proxy for collection; " + "proxyContextHandler not provided"); } - return proxyContextHandler.proxy(mapToLookup, this, otma); + return proxyContextHandler.proxy(proxyObject, mapToLookup, this, otma); } private Object handleActionMethod( + final Object proxyObject, final ManagedObject targetAdapter, final Object[] args, final ObjectAction objectAction) { val head = objectAction.interactionHead(targetAdapter); - val objectManager = getObjectManager(); + val objectManager = metaModelContext.getObjectManager(); // adapt argument pojos to managed objects val argAdapters = objectAction.getParameterTypes().map(IndexedFunction.zeroBased((paramIndex, paramSpec)->{ @@ -488,14 +506,14 @@ private Object handleActionMethod( : ManagedObject.empty(paramSpec); })); - runValidationTask(()->{ - checkVisibility(targetAdapter, objectAction); - checkUsability(targetAdapter, objectAction); - checkValidity(head, objectAction, argAdapters); + runValidationTask(proxyObject, ()->{ + checkVisibility(proxyObject, targetAdapter, objectAction); + checkUsability(proxyObject, targetAdapter, objectAction); + checkValidity(proxyObject, head, objectAction, argAdapters); }); - return runExecutionTask(()->{ - val interactionInitiatedBy = getInteractionInitiatedBy(); + return runExecutionTask(proxyObject, ()->{ + val interactionInitiatedBy = getInteractionInitiatedBy(proxyObject); val returnedAdapter = objectAction.execute( head, argAdapters, @@ -507,12 +525,13 @@ private Object handleActionMethod( } private void checkValidity( + final Object proxyObject, final ActionInteractionHead head, final ObjectAction objectAction, final Can argAdapters) { val interactionResult = objectAction - .isArgumentSetValid(head, argAdapters, getInteractionInitiatedBy()) + .isArgumentSetValid(head, argAdapters, getInteractionInitiatedBy(proxyObject)) .getInteractionResult(); notifyListenersAndVetoIfRequired(interactionResult); } @@ -534,21 +553,23 @@ private Object underlying(final Object arg) { private final Where where = Where.ANYWHERE; private void checkVisibility( + final Object proxyObject, final ManagedObject targetObjectAdapter, final ObjectMember objectMember) { - val visibleConsent = objectMember.isVisible(targetObjectAdapter, getInteractionInitiatedBy(), where); + val visibleConsent = objectMember.isVisible(targetObjectAdapter, getInteractionInitiatedBy(proxyObject), where); val interactionResult = visibleConsent.getInteractionResult(); notifyListenersAndVetoIfRequired(interactionResult); } private void checkUsability( + final Object proxyObject, final ManagedObject targetObjectAdapter, final ObjectMember objectMember) { val interactionResult = objectMember.isUsable( targetObjectAdapter, - getInteractionInitiatedBy(), + getInteractionInitiatedBy(proxyObject), where) .getInteractionResult(); notifyListenersAndVetoIfRequired(interactionResult); @@ -590,39 +611,39 @@ private InteractionException toException(final InteractionEvent interactionEvent // -- HELPER - private boolean shouldEnforceRules() { - return !getSyncControl().getExecutionModes().contains(ExecutionMode.SKIP_RULE_VALIDATION); + private boolean shouldEnforceRules(Object proxyObject) { + return !getSyncControl(proxyObject).getExecutionModes().contains(ExecutionMode.SKIP_RULE_VALIDATION); } - private boolean shouldExecute() { - return !getSyncControl().getExecutionModes().contains(ExecutionMode.SKIP_EXECUTION); + private boolean shouldExecute(Object proxyObject) { + return !getSyncControl(proxyObject).getExecutionModes().contains(ExecutionMode.SKIP_EXECUTION); } - private void runValidationTask(final Runnable task) { - if(!shouldEnforceRules()) { + private void runValidationTask(Object proxyObject, final Runnable task) { + if(!shouldEnforceRules(proxyObject)) { return; } try { task.run(); } catch(Exception ex) { - handleException(ex); + handleException(proxyObject, ex); } } - private X runExecutionTask(final Supplier task) { - if(!shouldExecute()) { + private X runExecutionTask(Object proxyObject, final Supplier task) { + if(!shouldExecute(proxyObject)) { return null; } try { return task.get(); } catch(Exception ex) { - return _Casts.uncheckedCast(handleException(ex)); + return _Casts.uncheckedCast(handleException(proxyObject, ex)); } } @SneakyThrows - private Object handleException(final Exception ex) { - val exceptionHandler = getSyncControl().getExceptionHandler() + private Object handleException(Object proxyObject, final Exception ex) { + val exceptionHandler = getSyncControl(proxyObject).getExceptionHandler() .orElse(null); if(exceptionHandler==null) { @@ -650,10 +671,4 @@ private void zeroArgsElseThrow(final Object[] args, final String name) { } } - // -- DEPENDENCIES - - private ObjectManager getObjectManager() { - return mmContext.getObjectManager(); - } - } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/MapInvocationHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/MapInvocationHandler.java index d73a953b0bf..54278ee1b3b 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/MapInvocationHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/MapInvocationHandler.java @@ -28,11 +28,12 @@ class MapInvocationHandler> extends PluralInvocationHandlerAbstract { public MapInvocationHandler( + final Object proxyObject, final M mapToBeProxied, final DomainObjectInvocationHandler handler, final OneToManyAssociation otma) { - super(mapToBeProxied, handler, otma, + super(proxyObject, mapToBeProxied, handler, otma, CollectionSemantics.MAP); _Assert.assertTrue(Map.class.isAssignableFrom(mapToBeProxied.getClass()), diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java index 55bd3919694..91f62e30d40 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java @@ -19,6 +19,7 @@ package org.apache.causeway.core.runtimeservices.wrapper.handlers; import java.lang.reflect.Method; +import java.util.Collection; import java.util.Map; import org.apache.causeway.applib.services.wrapper.events.CollectionMethodEvent; @@ -34,24 +35,35 @@ * @param

non-scalar type (eg. {@link Collection} or {@link Map}) to be proxied */ abstract class PluralInvocationHandlerAbstract -extends DelegatingInvocationHandlerDefault

{ +extends DelegatingInvocationHandlerAbstract

{ private final OneToManyAssociation oneToManyAssociation; private final T domainObject; private final CollectionSemantics collectionSemantics; + private final Object proxyObject; + + private final P target; + + public P getTarget(Object proxyObject) { + return target; + } + protected PluralInvocationHandlerAbstract( + final Object proxyObject, final P collectionOrMapToBeProxied, final DomainObjectInvocationHandler handler, final OneToManyAssociation otma, final CollectionSemantics collectionSemantics) { super(otma.getMetaModelContext(), - collectionOrMapToBeProxied, - handler.getSyncControl()); + (Class

)collectionOrMapToBeProxied.getClass() + ); + this.proxyObject = proxyObject; + this.target = collectionOrMapToBeProxied; this.oneToManyAssociation = otma; - this.domainObject = handler.getDelegate(); + this.domainObject = handler.getTarget(proxyObject); this.collectionSemantics = collectionSemantics; } @@ -67,17 +79,15 @@ public T getDomainObject() { public Object invoke(final Object collectionObject, final Method method, final Object[] args) throws Throwable { // delegate - final Object returnValueObj = delegate(method, args); + final Object returnValueObj = delegate(proxyObject, method, args); val policy = collectionSemantics.getInvocationHandlingPolicy(); if (policy.getIntercepted().contains(method)) { - resolveIfRequired(domainObject); - val event = new CollectionMethodEvent( - getDelegate(), + getTarget(proxyObject), getCollection().getFeatureIdentifier(), getDomainObject(), method.getName(), diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyContextHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyContextHandler.java index 02ed90dd368..062f8a6bcd6 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyContextHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyContextHandler.java @@ -24,12 +24,14 @@ import org.apache.causeway.applib.services.wrapper.control.SyncControl; import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.semantics.CollectionSemantics; -import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.runtimeservices.wrapper.proxy.ProxyCreator; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.val; @RequiredArgsConstructor @@ -37,35 +39,42 @@ public class ProxyContextHandler { @NonNull private final ProxyCreator proxyCreator; + @SneakyThrows public T proxy( - final T domainObject, - final ManagedObject adapter, - final SyncControl syncControl) { - + final MetaModelContext metaModelContext, + final ObjectSpecification targetSpecification, + final T targetPojo, + final SyncControl syncControl + ) { val invocationHandler = new DomainObjectInvocationHandler( - domainObject, - null, // mixeeAdapter ignored - adapter, - syncControl, - this); + metaModelContext, + this, + targetSpecification + ); - return proxyCreator.instantiateProxy(invocationHandler); + T proxyObject = proxyCreator.instantiateProxy(invocationHandler); + WrapperInvocationContext.set(proxyObject, new WrapperInvocationContext(targetPojo, null, syncControl, null)); + + return proxyObject; } + @SneakyThrows public T mixinProxy( - final T mixin, - final ManagedObject mixeeAdapter, - final ManagedObject mixinAdapter, + final MetaModelContext metaModelContext, + final ObjectSpecification targetSpecification, + final T targetMixinPojo, + final Object mixeePojo, final SyncControl syncControl) { val invocationHandler = new DomainObjectInvocationHandler( - mixin, - mixeeAdapter, - mixinAdapter, - syncControl, - this); + metaModelContext, + this, targetSpecification + ); + + T proxyObject = proxyCreator.instantiateProxy(invocationHandler); + WrapperInvocationContext.set(proxyObject, new WrapperInvocationContext(targetMixinPojo, mixeePojo, syncControl, null)); - return proxyCreator.instantiateProxy(invocationHandler); + return proxyObject; } @@ -74,20 +83,22 @@ public T mixinProxy( * handler. */ public Collection proxy( + final Object proxyObject, final Collection collectionToBeProxied, final DomainObjectInvocationHandler handler, final OneToManyAssociation otma) { + // TODO: to introduce caching of proxy classes, we'd need to pull collectionToBeProxied + // out of the handler's state, and move into a variant of WrapperInvocationContext, set into the proxyCollection val collectionInvocationHandler = new CollectionInvocationHandler>( - collectionToBeProxied, handler, otma); - collectionInvocationHandler.setResolveObjectChangedEnabled( - handler.isResolveObjectChangedEnabled()); + proxyObject, collectionToBeProxied, handler, otma); val proxyBase = CollectionSemantics .valueOfElseFail(collectionToBeProxied.getClass()) .getContainerType(); - return proxyCreator.instantiateProxy(_Casts.uncheckedCast(proxyBase), collectionInvocationHandler); + final var proxyCollection = proxyCreator.instantiateProxy(_Casts.uncheckedCast(proxyBase), collectionInvocationHandler); + return proxyCollection; } /** @@ -95,17 +106,20 @@ public Collection proxy( * handler. */ public Map proxy( - final Map collectionToBeProxied, + final Object proxyObject, + final Map mapToBeProxied, final DomainObjectInvocationHandler handler, final OneToManyAssociation otma) { + // TODO: to introduce caching of proxy classes, we'd need to pull mapToBeProxied + // out of the handler's state, and move into a variant of WrapperInvocationContext, set into the proxyMap val mapInvocationHandler = new MapInvocationHandler>( - collectionToBeProxied, handler, otma); - mapInvocationHandler.setResolveObjectChangedEnabled(handler.isResolveObjectChangedEnabled()); + proxyObject, mapToBeProxied, handler, otma); val proxyBase = Map.class; - return proxyCreator.instantiateProxy(_Casts.uncheckedCast(proxyBase), mapInvocationHandler); + final var proxyMap = proxyCreator.instantiateProxy(_Casts.uncheckedCast(proxyBase), mapInvocationHandler); + return proxyMap; } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/WrapperInvocationContext.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/WrapperInvocationContext.java new file mode 100644 index 00000000000..928bc8037f1 --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/WrapperInvocationContext.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.runtimeservices.wrapper.handlers; + +import java.lang.reflect.Field; + +import org.apache.causeway.applib.services.wrapper.WrappingObject; +import org.apache.causeway.applib.services.wrapper.control.AsyncControl; +import org.apache.causeway.applib.services.wrapper.control.SyncControl; +import org.apache.causeway.commons.internal.proxy._ProxyFactoryService; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +@Getter +public class WrapperInvocationContext { + + /** + * Either a domain object (entity, view model, service) or a mixin. + */ + final Object targetPojo; + /** + * Not applicable if a domain object. + */ + final Object mixeePojo; + + final SyncControl syncControl; + final AsyncControl asyncControl; + + + @SneakyThrows + public static T set(T proxyObject, WrapperInvocationContext wic) { + if(proxyObject instanceof WrappingObject) { + getField(proxyObject).set(proxyObject, wic); + } + return proxyObject; + } + + @SneakyThrows + public static WrapperInvocationContext get(Object proxyObject) { + if(!(proxyObject instanceof WrappingObject)) { + return null; + } + final var wic = getField(proxyObject).get(proxyObject); + return (WrapperInvocationContext) wic; + } + + @SneakyThrows + private static Field getField(Object proxyObject) { + final var proxyObjectClass = proxyObject.getClass(); + final var field = proxyObjectClass.getDeclaredField(_ProxyFactoryService.WRAPPER_INVOCATION_CONTEXT_FIELD_NAME); + field.setAccessible(true); + return field; + } + +} diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreator.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreator.java index 15cf6e6bf74..2fcb1a6c918 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreator.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreator.java @@ -37,15 +37,14 @@ public class ProxyCreator { @NonNull private final _ProxyFactoryService proxyFactoryService; public T instantiateProxy(final DelegatingInvocationHandler handler) { - final T classToBeProxied = handler.getDelegate(); - final Class base = _Casts.uncheckedCast(classToBeProxied.getClass()); + final Class base = _Casts.uncheckedCast(handler.getTargetClass()); return instantiateProxy(base, handler); } /** * Creates a proxy, using given {@code base} type as the proxy's base. * @implNote introduced to circumvent access issues on cases, - * where {@code handler.getDelegate().getClass()} is not visible + * where {@code handler.getTarget().getClass()} is not visible * (eg. nested private type) */ public T instantiateProxy(final Class base, final DelegatingInvocationHandler handler) { @@ -61,4 +60,5 @@ public T instantiateProxy(final Class base, final DelegatingInvocationHan } } + } diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java index 89e228d013e..4e048462f46 100644 --- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java +++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java @@ -35,6 +35,7 @@ import org.apache.causeway.commons.internal.proxy._ProxyFactoryService; import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting; import org.apache.causeway.core.metamodel.execution.MemberExecutorService; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import lombok.RequiredArgsConstructor; import lombok.val; @@ -87,10 +88,10 @@ public void init() { } @Override - protected T createProxy(final T domainObject, final SyncControl syncControl) { + protected T createProxy(ObjectSpecification targetSpecification, final T targetPojo, final SyncControl syncControl) { WrapperFactoryDefaultTest.this.createProxyCalledWithSyncControl = syncControl; - WrapperFactoryDefaultTest.this.createProxyCalledWithDomainObject = (DomainObject) domainObject; - return domainObject; + WrapperFactoryDefaultTest.this.createProxyCalledWithDomainObject = (DomainObject) targetPojo; + return targetPojo; } }; diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreatorTestUsingCodegenPlugin.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreatorTestUsingCodegenPlugin.java index ca1cb2235e4..410f197809d 100644 --- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreatorTestUsingCodegenPlugin.java +++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/proxy/ProxyCreatorTestUsingCodegenPlugin.java @@ -64,17 +64,13 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg } @Override - public Employee getDelegate() { + public Employee getTarget(Object proxyObject) { return delegate; } @Override - public boolean isResolveObjectChangedEnabled() { - return false; - } - - @Override - public void setResolveObjectChangedEnabled(final boolean resolveObjectChangedEnabled) { + public Class getTargetClass() { + return Employee.class; } public boolean wasInvoked(final String methodName) { diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java index 78e2a661e18..b93d06c6919 100644 --- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java +++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java @@ -32,6 +32,7 @@ import org.quartz.JobExecutionContext; import static org.assertj.core.api.Assertions.assertThat; + import org.springframework.transaction.annotation.Propagation; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -106,7 +107,8 @@ void async_using_default_executor_service() { val counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow(); val control = AsyncControl.returning(Counter.class); - wrapperFactory.asyncWrap(counter, control).bumpUsingDeclaredAction(); + Counter counter1 = wrapperFactory.asyncWrap(counter, control); + counter1.bumpUsingDeclaredAction(); // wait til done control.getFuture().get(); diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java index 10f8ccbb2d8..9ea9eda0ab4 100644 --- a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java @@ -49,6 +49,10 @@ public JpaProduct updateProductPrice(final JpaProduct product, final double newP // -- COUNT PRODUCTS + @Action + public void foo() { + } + @Action public int countProducts() { return getAllProducts().size(); diff --git a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/JpaWrapperSyncTest.java b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/JpaWrapperSyncTest.java new file mode 100644 index 00000000000..5c45a11f0a9 --- /dev/null +++ b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/JpaWrapperSyncTest.java @@ -0,0 +1,90 @@ +/* + * 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.testdomain.wrapper.jdo; + +import javax.inject.Inject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import org.apache.causeway.applib.services.repository.RepositoryService; +import org.apache.causeway.applib.services.wrapper.WrapperFactory; +import org.apache.causeway.core.config.presets.CausewayPresets; +import org.apache.causeway.testdomain.conf.Configuration_usingJpa; +import org.apache.causeway.testdomain.fixtures.EntityTestFixtures; +import org.apache.causeway.testdomain.jpa.JpaInventoryManager; +import org.apache.causeway.testdomain.jpa.JpaTestFixtures; +import org.apache.causeway.testdomain.jpa.entities.JpaBook; +import org.apache.causeway.testdomain.jpa.entities.JpaProduct; +import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract; + +import lombok.val; + +@SpringBootTest( + classes = { + Configuration_usingJpa.class, + }, + properties = { + "spring.datasource.url=jdbc:h2:mem:JpaWrapperSyncTest" + } +) +@TestPropertySource(CausewayPresets.UseLog4j2Test) +class JpaWrapperSyncTest extends CausewayIntegrationTestAbstract { + + @Inject private RepositoryService repository; + @Inject private WrapperFactory wrapper; + @Inject private JpaTestFixtures testFixtures; + + protected EntityTestFixtures.Lock lock; + + @BeforeEach + void installFixture() { + this.lock = testFixtures.aquireLock(); + lock.install(); + } + + @AfterEach + void uninstallFixture() { + this.lock.release(); + } + + @Test + void testWrapper_waitingOnDomainEvent() { + + val inventoryManager = factoryService.viewModel(JpaInventoryManager.class); + val sumOfPrices = repository.allInstances(JpaProduct.class) + .stream() + .mapToDouble(JpaProduct::getPrice) + .sum(); + + assertEquals(167d, sumOfPrices, 1E-6); + + val products = wrapper.wrap(inventoryManager).getAllProducts(); + + assertEquals(3, products.size()); + assertEquals(JpaBook.class, products.get(0).getClass()); + } + +} diff --git a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java new file mode 100644 index 00000000000..b7a2b190070 --- /dev/null +++ b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java @@ -0,0 +1,111 @@ +/* + * 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.testdomain.wrapper.jdo; + +import java.util.List; + +import javax.inject.Inject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import org.apache.causeway.applib.services.wrapper.WrapperFactory; +import org.apache.causeway.commons.memory.MemoryUsage; +import org.apache.causeway.core.config.presets.CausewayPresets; +import org.apache.causeway.testdomain.conf.Configuration_usingJpa; +import org.apache.causeway.testdomain.fixtures.EntityTestFixtures; +import org.apache.causeway.testdomain.jpa.JpaInventoryManager; +import org.apache.causeway.testdomain.jpa.JpaTestFixtures; +import org.apache.causeway.testdomain.jpa.entities.JpaProduct; +import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract; + +import lombok.val; + + +@SpringBootTest( + classes = { + Configuration_usingJpa.class, + }, + properties = { + "spring.datasource.url=jdbc:h2:mem:JpaWrapperSyncTest" + } +) +@TestPropertySource(CausewayPresets.UseLog4j2Test) +class WrapperFactoryMetaspaceMemoryLeakTest extends CausewayIntegrationTestAbstract { + + @Inject private WrapperFactory wrapper; + @Inject private JpaTestFixtures testFixtures; + + protected EntityTestFixtures.Lock lock; + + @BeforeEach + void installFixture() { + this.lock = testFixtures.aquireLock(); + lock.install(); + } + + @AfterEach + void uninstallFixture() { + this.lock.release(); + } + + @Test + void testWrapper_waitingOnDomainEvent() throws InterruptedException { + MemoryUsage.measureMetaspace("exercise", ()->{ +// with caching +// exercise(1, 0); // 2,053 KB +// exercise(1, 2000); // 3,839 KB. // some leakage from collections +// exercise(20, 0); // 2,112 KB +// exercise(20, 2000); // 3,875 KB +// exercise(2000, 0); // 3,260 KB. // ? increased some, is it significant; a lot less than without caching +// exercise(2000, 200); // 4,294 KB. +// exercise(20000, 0); // 3,265 KB // no noticeable leakage compared to 2000; MUCH less than without caching + +// without caching +// exercise(1, 0); // 2,244 KB +// exercise(1, 2000); //. 3,669 KB // some leakage from collections +// exercise(20, 0); // 2,440 KB +// exercise(20, 2000); //. 4,286 KB +// exercise(2000, 0); // 15,148 KB // significant leakage from 20 +// exercise(2000, 200); // 20,423 KB +// exercise(20000, 0); //.115,729 KB + }); + } + + private void exercise(int instances, int loops) { + for (int i = 0; i < instances; i++) { + val inventoryManager = factoryService.viewModel(JpaInventoryManager.class); + JpaInventoryManager jpaInventoryManager = wrapper.wrap(inventoryManager); + jpaInventoryManager.foo(); + + for (var j = 0; j < loops; j++) { + List allProducts = jpaInventoryManager.getAllProducts(); + allProducts.forEach(product -> { + String unused = product.getName(); + }); + } + } + } +} + + From a6a16c01547a34bb4357202afa975ff3c2e6ebb4 Mon Sep 17 00:00:00 2001 From: Dan Haywood Date: Sun, 27 Apr 2025 13:53:56 +0100 Subject: [PATCH 2/5] CAUSEWAY-3883: updates more javadoc --- .../wrapper/handlers/DomainObjectInvocationHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java index 2b4439245bd..49061465dfb 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java @@ -136,7 +136,7 @@ public DomainObjectInvocationHandler( /** * - * @param proxyObject - not used. + * @param proxyObject - holds the reference to {@link WrapperInvocationContext} which in turn references the target pojo (and mixee pojo if target is a mixin). * @param method - the method invoked on the proxy * @param args - the args to the method invoked on the proxy * @throws Throwable From a3769a845a229bd986b91e1f9d7ab4c88795220c Mon Sep 17 00:00:00 2001 From: Dan Haywood Date: Sun, 27 Apr 2025 14:05:23 +0100 Subject: [PATCH 3/5] CAUSEWAY-3883: improves javadoc some more --- .../services/ProxyFactoryServiceByteBuddy.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java index 2726b2988f8..a768e7ff23a 100644 --- a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java +++ b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java @@ -24,6 +24,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.Map; +import java.util.concurrent.Callable; import java.util.function.Function; import org.springframework.lang.Nullable; @@ -51,12 +52,24 @@ public class ProxyFactoryServiceByteBuddy extends _ProxyFactoryServiceAbstract { private final ClassLoadingStrategyAdvisor strategyAdvisor = new ClassLoadingStrategyAdvisor(); + /** * Cached proxy class by invocation handler. * *

- * The only state held in invocation handler is the org.apache.causeway.core.metamodel.spec.ObjectSpecification, - *. in effect the target class. + * For the wrapper factory, the passed in implementation of invocation handler + * (org.apache.causeway.core.runtimeservices.wrapper.handlers.DomainObjectInvocationHandler) + * implements equals/hashCode based only on the + * org.apache.causeway.core.metamodel.spec.ObjectSpecification (effectively the target class, + * which might be a mixin class); the corresponding proxied class is therefore cached. + *

+ * + *

+ * For other implementations, if the invocation handler does not explicitly implement equals/hashCode, then + * effectively there is no caching, and therefore there will be a metaclass memory leak. Use + * {@link org.apache.causeway.commons.memory.MemoryUsage#measureMetaspace(String, Callable)} to determine + * whether this is a problem. Note that at the time of writing, the proxy classes for parented collections + * of (wrapped) domain objects are not proxied; but these are rarely used. *

* *

From 65d64cd85b7bebb14f82615747388f4901fca108 Mon Sep 17 00:00:00 2001 From: Dan Haywood Date: Sun, 27 Apr 2025 16:07:13 +0100 Subject: [PATCH 4/5] CAUSEWAY-3883: testing --- enhance.sh | 24 ++- ...va => WrapperInteraction_1_IntegTest.java} | 8 +- ...va => WrapperInteraction_2_IntegTest.java} | 8 +- ...va => WrapperInteraction_3_IntegTest.java} | 8 +- ...va => WrapperInteraction_4_IntegTest.java} | 8 +- .../WrapperInteraction_Caching_IntegTest.java | 144 ++++++++++++++++++ ...WrapperFactoryMetaspaceMemoryLeakTest.java | 8 +- 7 files changed, 180 insertions(+), 28 deletions(-) rename regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/{WrapperInteractionTest.java => WrapperInteraction_1_IntegTest.java} (95%) rename regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/{WrapperInteractionTest2.java => WrapperInteraction_2_IntegTest.java} (94%) rename regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/{WrapperInteractionTest3.java => WrapperInteraction_3_IntegTest.java} (96%) rename regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/{WrapperInteractionTest4.java => WrapperInteraction_4_IntegTest.java} (95%) create mode 100644 regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java diff --git a/enhance.sh b/enhance.sh index 91e9d854fc8..ddfaee3c054 100755 --- a/enhance.sh +++ b/enhance.sh @@ -28,8 +28,9 @@ usage() { echo " -m : secman (extensions/security)" >&2 echo " -s : session log (extensions/security)" >&2 echo " -d : demo (examples/demo/domain)" >&2 - echo " -t : JDO regression tests (regressiontests/stable)" >&2 - echo " -u : JDO regression tests (regressiontests/stable-cmdexecauditsess)" >&2 + echo " -r : JDO regression tests (all)" >&2 + echo " -u : JDO regression tests (cmdexecauditsess only)" >&2 + echo " -w : JDO regression tests (wrapperfactory only)" >&2 echo " -A : add -am (also make)" >&2 } @@ -42,6 +43,7 @@ EXECUTIONLOG="" EXECUTIONOUTBOX="" REGRESSIONTESTS_STABLE="" REGRESSIONTESTS_CMDEXECAUDITSESS="" +REGRESSIONTESTS_WRAPPERFACTORY="" SECMAN="" SESSIONLOG="" ALSO_MAKE="" @@ -49,7 +51,7 @@ ALSO_MAKE="" PATHS=() ALL_IF_REQUIRED="" -while getopts ":acdeomshtuwA" arg; do +while getopts ":acdeomsrhtuwA" arg; do case $arg in h) usage @@ -83,14 +85,19 @@ while getopts ":acdeomshtuwA" arg; do DEMO="enhance" PATHS+=( "examples/demo/domain" ) ;; - t) - REGRESSIONTESTS_STABLE="enhance" - PATHS+=( "regressiontests/stable" ) + r) + REGRESSIONTESTS="enhance" + PATHS+=( "regressiontests" ) ALL_IF_REQUIRED="-Dmodule-all" ;; u) REGRESSIONTESTS_CMDEXECAUDITSESS="enhance" - PATHS+=( "regressiontests/stable-cmdexecauditsess/persistence-jdo" ) + PATHS+=( "regressiontests/cmdexecauditsess/persistence-jdo" ) + ALL_IF_REQUIRED="-Dmodule-all" + ;; + w) + REGRESSIONTESTS_WRAPPERFACTORY="enhance" + PATHS+=( "regressiontests/core-wrapperfactory" ) ALL_IF_REQUIRED="-Dmodule-all" ;; A) @@ -111,8 +118,9 @@ echo "EXECUTIONOUTBOX : $EXECUTIONOUTBOX" echo "SECMAN : $SECMAN" echo "SESSIONLOG : $SESSIONLOG" echo "DEMO : $DEMO" -echo "REGRESSIONTESTS_STABLE : $REGRESSIONTESTS_STABLE" +echo "REGRESSIONTESTS : $REGRESSIONTESTS" echo "REGRESSIONTESTS_CMDEXECAUDITSESS : $REGRESSIONTESTS_CMDEXECAUDITSESS" +echo "REGRESSIONTESTS_WRAPPERFACTORY : $REGRESSIONTESTS_WRAPPERFACTORY" echo "" echo "ALSO_MAKE : $ALSO_MAKE" diff --git a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest.java b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java similarity index 95% rename from regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest.java rename to regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java index ef9e60fb96e..c0c34b16f6f 100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java @@ -52,16 +52,16 @@ classes = { Configuration_headless.class, Configuration_usingInteractionDomain.class, - WrapperInteractionTest.Customer.class, - WrapperInteractionTest.ConcreteMixin.class, - WrapperInteractionTest.ConcreteMixin2.class, + WrapperInteraction_1_IntegTest.Customer.class, + WrapperInteraction_1_IntegTest.ConcreteMixin.class, + WrapperInteraction_1_IntegTest.ConcreteMixin2.class, } ) @TestPropertySource({ CausewayPresets.SilenceMetaModel, CausewayPresets.SilenceProgrammingModel }) -class WrapperInteractionTest +class WrapperInteraction_1_IntegTest extends InteractionTestAbstract { @Data @DomainObject(nature = Nature.VIEW_MODEL) diff --git a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest2.java b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java similarity index 94% rename from regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest2.java rename to regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java index 95083dd2b93..9855529a747 100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest2.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java @@ -49,16 +49,16 @@ classes = { Configuration_headless.class, Configuration_usingInteractionDomain.class, - WrapperInteractionTest2.Customer.class, - WrapperInteractionTest2.Customer.ConcreteMixin.class, - WrapperInteractionTest2.Customer.ConcreteMixin2.class, + WrapperInteraction_2_IntegTest.Customer.class, + WrapperInteraction_2_IntegTest.Customer.ConcreteMixin.class, + WrapperInteraction_2_IntegTest.Customer.ConcreteMixin2.class, } ) @TestPropertySource({ CausewayPresets.SilenceMetaModel, CausewayPresets.SilenceProgrammingModel }) -class WrapperInteractionTest2 +class WrapperInteraction_2_IntegTest extends InteractionTestAbstract { @Data @DomainObject(nature = Nature.VIEW_MODEL) diff --git a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest3.java b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java similarity index 96% rename from regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest3.java rename to regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java index ae7331385e6..cb665b24b80 100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest3.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java @@ -56,16 +56,16 @@ Configuration_headless.class, Configuration_usingInteractionDomain.class, - WrapperInteractionTest3.Task.class, - WrapperInteractionTest3.Task.Succeeded.class, - WrapperInteractionTest3.Task.Failed.class, + WrapperInteraction_3_IntegTest.Task.class, + WrapperInteraction_3_IntegTest.Task.Succeeded.class, + WrapperInteraction_3_IntegTest.Task.Failed.class, } ) @TestPropertySource({ CausewayPresets.SilenceMetaModel, CausewayPresets.SilenceProgrammingModel }) -class WrapperInteractionTest3 +class WrapperInteraction_3_IntegTest extends InteractionTestAbstract { @Data @DomainObject(nature = Nature.VIEW_MODEL) diff --git a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest4.java b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java similarity index 95% rename from regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest4.java rename to regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java index 3cd4646d270..70287bff640 100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteractionTest4.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java @@ -55,16 +55,16 @@ classes = { Configuration_headless.class, Configuration_usingInteractionDomain.class, - WrapperInteractionTest4.Task.class, - WrapperInteractionTest4.Task.Succeeded.class, - WrapperInteractionTest4.Task.Failed.class, + WrapperInteraction_4_IntegTest.Task.class, + WrapperInteraction_4_IntegTest.Task.Succeeded.class, + WrapperInteraction_4_IntegTest.Task.Failed.class, } ) @TestPropertySource({ CausewayPresets.SilenceMetaModel, CausewayPresets.SilenceProgrammingModel }) -class WrapperInteractionTest4 +class WrapperInteraction_4_IntegTest extends InteractionTestAbstract { @Data @DomainObject(nature = Nature.VIEW_MODEL) diff --git a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java new file mode 100644 index 00000000000..cf2ba4cd808 --- /dev/null +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java @@ -0,0 +1,144 @@ +/* + * 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.testdomain.interact; + +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +import org.apache.causeway.applib.annotation.Action; +import org.apache.causeway.applib.annotation.DomainObject; +import org.apache.causeway.applib.annotation.Nature; +import org.apache.causeway.applib.annotation.SemanticsOf; +import org.apache.causeway.applib.services.wrapper.control.AsyncControl; +import org.apache.causeway.core.config.presets.CausewayPresets; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; +import org.apache.causeway.testdomain.conf.Configuration_headless; +import org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain; +import org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest( + classes = { + Configuration_headless.class, + Configuration_usingInteractionDomain.class, + WrapperInteraction_Caching_IntegTest.StatefulCalculator.class, + WrapperInteraction_Caching_IntegTest.StatefulCalculator_add.class + } +) +@TestPropertySource({ + CausewayPresets.SilenceMetaModel, + CausewayPresets.SilenceProgrammingModel +}) +class WrapperInteraction_Caching_IntegTest +extends InteractionTestAbstract { + + @Data @DomainObject(nature = Nature.VIEW_MODEL) + static class StatefulCalculator { + @Getter int total; + @Action public Integer inc(final int amount) { return total += amount; } + @Action(semantics = SemanticsOf.IDEMPOTENT) public void reset() { total = 0; } + } + + @Action + @RequiredArgsConstructor + public static class StatefulCalculator_add { + @SuppressWarnings("unused") + private final StatefulCalculator mixee; + public Integer act(final int amount) { + return mixee.inc(amount); + } + } + + StatefulCalculator calculator1; + StatefulCalculator calculator2; + + @BeforeEach + void before() { + calculator1 = new StatefulCalculator(); + calculator2 = new StatefulCalculator(); + + Assertions.assertThat(calculator1.total).isEqualTo(0); + Assertions.assertThat(calculator2.total).isEqualTo(0); + } + + @Test + void sync_wrapped() throws ExecutionException, InterruptedException, TimeoutException { + + // when + wrap(calculator1).inc(5); + wrap(calculator2).inc(10); + + // then + Assertions.assertThat(calculator1.total).isEqualTo(5); + Assertions.assertThat(calculator2.total).isEqualTo(10); + } + + @Test + void sync_mixin() throws ExecutionException, InterruptedException, TimeoutException { + + // when + wrapMixin(StatefulCalculator_add.class, calculator1).act(5); + wrapMixin(StatefulCalculator_add.class, calculator2).act(10); + + // then + Assertions.assertThat(calculator1.total).isEqualTo(5); + Assertions.assertThat(calculator2.total).isEqualTo(10); + } + + @Disabled + @Test + void async_wrapped() throws ExecutionException, InterruptedException, TimeoutException { + + // when + AsyncControl asyncControlForCalculator1 = AsyncControl.returning(Integer.class); + StatefulCalculator asyncCalculator1 = wrapperFactory.asyncWrap(calculator1, asyncControlForCalculator1); + + AsyncControl asyncControlForCalculator2 = AsyncControl.returning(Integer.class); + StatefulCalculator asyncCalculator2 = wrapperFactory.asyncWrap(calculator2, asyncControlForCalculator2); + + asyncCalculator1.inc(12); + asyncCalculator2.inc(24); + + // then + Future future = asyncControlForCalculator1.getFuture(); + Integer i = future.get(10, TimeUnit.SECONDS); + Assertions.assertThat(i.intValue()).isEqualTo(12); + Assertions.assertThat(calculator1.getTotal()).isEqualTo(12); + + Assertions.assertThat(asyncControlForCalculator2.getFuture().get(10, TimeUnit.SECONDS).intValue()).isEqualTo(24); + Assertions.assertThat(calculator2.getTotal()).isEqualTo(24); + } + + +} diff --git a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java index b7a2b190070..bbb30facae7 100644 --- a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java +++ b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/wrapper/jdo/WrapperFactoryMetaspaceMemoryLeakTest.java @@ -73,20 +73,20 @@ void uninstallFixture() { void testWrapper_waitingOnDomainEvent() throws InterruptedException { MemoryUsage.measureMetaspace("exercise", ()->{ // with caching -// exercise(1, 0); // 2,053 KB +// exercise(1, 0); // 2,221 KB // exercise(1, 2000); // 3,839 KB. // some leakage from collections // exercise(20, 0); // 2,112 KB // exercise(20, 2000); // 3,875 KB -// exercise(2000, 0); // 3,260 KB. // ? increased some, is it significant; a lot less than without caching +// exercise(2000, 0); // 3,263 KB. // ? increased some, is it significant; a lot less than without caching // exercise(2000, 200); // 4,294 KB. -// exercise(20000, 0); // 3,265 KB // no noticeable leakage compared to 2000; MUCH less than without caching +// exercise(20000, 0); // 3,243 KB // no noticeable leakage compared to 2000; MUCH less than without caching // without caching // exercise(1, 0); // 2,244 KB // exercise(1, 2000); //. 3,669 KB // some leakage from collections // exercise(20, 0); // 2,440 KB // exercise(20, 2000); //. 4,286 KB -// exercise(2000, 0); // 15,148 KB // significant leakage from 20 + exercise(2000, 0); // 14,580 KB // significant leakage from 20 // exercise(2000, 200); // 20,423 KB // exercise(20000, 0); //.115,729 KB }); From 5fbcdd60daba351d3177fa1f3427f4e4b9b9d547 Mon Sep 17 00:00:00 2001 From: Dan Haywood Date: Mon, 26 May 2025 13:33:41 +0100 Subject: [PATCH 5/5] CAUSEWAY-3891: adds noCache flag as an experiment --- .../pdfjs/wkt/ui/components/PdfJsViewerPanel.java | 7 ++++--- .../viewer/wicket/ui/actionresponse/_DownloadHandler.java | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java b/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java index 167f10c0d1f..98f2b1781d5 100644 --- a/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java +++ b/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java @@ -186,13 +186,14 @@ protected MarkupContainer createRegularFrame() { val regularFrame = new WebMarkupContainer(ID_SCALAR_IF_REGULAR); + CharSequence documentUrl = urlFor( + new ListenerRequestHandler( + new PageAndComponentProvider(getPage(), this))) + "&noCache=" + System.currentTimeMillis(); val pdfJsConfig = scalarModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class) .map(pdfJsViewerFacet->pdfJsViewerFacet.configFor(buildKey())) .orElseGet(PdfJsConfig::new) - .withDocumentUrl(urlFor( - new ListenerRequestHandler( - new PageAndComponentProvider(getPage(), this)))); + .withDocumentUrl(documentUrl); val pdfJsPanel = new PdfJsPanel(ID_SCALAR_VALUE, pdfJsConfig); diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/_DownloadHandler.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/_DownloadHandler.java index 5e3a1ae1506..dc658b70bba 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/_DownloadHandler.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/_DownloadHandler.java @@ -104,9 +104,11 @@ private static IRequestHandler enforceNoCacheOnClientSide(final IRequestHandler if(downloadHandler==null) { return downloadHandler; } - if(downloadHandler instanceof ResourceStreamRequestHandler) - ((ResourceStreamRequestHandler) downloadHandler) - .setCacheDuration(Duration.ZERO); + if(downloadHandler instanceof ResourceStreamRequestHandler) { + final var requestHandler = (ResourceStreamRequestHandler) downloadHandler; + requestHandler.setCacheDuration(Duration.ZERO); + return requestHandler; + } return downloadHandler; }