This is an automated email from the ASF dual-hosted git repository. ahuber pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/causeway.git
commit d589bafcdb3eac3f539557881fe33559d36cb7c5 Merge: 5044f10852c 5fbcdd60dab Author: Andi Huber <[email protected]> AuthorDate: Fri Jun 13 14:39:41 2025 +0200 CAUSEWAY-3891 and CAUSEWAY-3883: merges from maintenance branch - PDF.JS to play nice with browser caching - MemoryUsage new internal utility - new Wrapper memory leak tests commons/src/main/java/module-info.java | 1 + .../commons/internal/debug/_MemoryUsage.java | 98 +++++++++++++ .../metamodel/context/HasMetaModelContext.java | 5 + .../context/MetaModelContext_usingSpring.java | 5 + .../metamodel/spec/impl/ObjectMemberAbstract.java | 5 - .../runtimeservices/src/main/java/module-info.java | 1 + .../pdfjs/wkt/ui/components/PdfJsViewerPanel.java | 36 ++--- .../testdomain/jpa/JpaInventoryManager.java | 6 + .../interact/WrapperInteraction_1_IntegTest.java | 149 +++++++++++++++++++ .../interact/WrapperInteraction_2_IntegTest.java | 132 +++++++++++++++++ .../interact/WrapperInteraction_3_IntegTest.java | 158 +++++++++++++++++++++ .../interact/WrapperInteraction_4_IntegTest.java | 152 ++++++++++++++++++++ .../WrapperInteraction_Caching_IntegTest.java | 141 ++++++++++++++++++ .../jpa/wrapper/JpaWrapperSyncTest.java | 90 ++++++++++++ .../WrapperFactoryMetaspaceMemoryLeakTest.java | 107 ++++++++++++++ 15 files changed, 1060 insertions(+), 26 deletions(-) diff --cc commons/src/main/java/module-info.java index b74ac89285a,92e4474ad9c..27b06a6870d --- a/commons/src/main/java/module-info.java +++ b/commons/src/main/java/module-info.java @@@ -74,16 -73,16 +74,17 @@@ module org.apache.causeway.commons requires transitive spring.beans; requires transitive spring.context; requires transitive spring.core; - requires java.inject; - requires java.annotation; + requires transitive jakarta.xml.bind; + requires transitive jakarta.inject; + requires jakarta.annotation; requires com.sun.xml.bind; - requires com.fasterxml.jackson.datatype.jsr310; requires com.fasterxml.jackson.dataformat.yaml; + requires com.fasterxml.jackson.datatype.jsr310; requires com.fasterxml.jackson.datatype.jdk8; + requires java.management; - opens org.apache.causeway.commons.internal.resources to java.xml.bind, com.sun.xml.bind; // JUnit test - opens org.apache.causeway.commons.io to java.xml.bind, com.sun.xml.bind; - exports org.apache.causeway.commons.memory; + // JAXB JUnit test + opens org.apache.causeway.commons.internal.resources to jakarta.xml.bind; + opens org.apache.causeway.commons.io to jakarta.xml.bind; } diff --cc commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java index 00000000000,00000000000..ab6958541a1 new file mode 100644 --- /dev/null +++ b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java @@@ -1,0 -1,0 +1,98 @@@ ++/* ++ * Licensed to the Apache Software Foundation (ASF) under one ++ * or more contributor license agreements. See the NOTICE file ++ * distributed with this work for additional information ++ * regarding copyright ownership. The ASF licenses this file ++ * to you under the Apache License, Version 2.0 (the ++ * "License"); you may not use this file except in compliance ++ * with the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, ++ * software distributed under the License is distributed on an ++ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ++ * KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations ++ * under the License. ++ */ ++package org.apache.causeway.commons.internal.debug; ++ ++import lombok.SneakyThrows; ++ ++import java.lang.management.ManagementFactory; ++import java.lang.management.MemoryPoolMXBean; ++import java.util.concurrent.Callable; ++ ++/** ++ * <h1>- internal use only -</h1> ++ * <p> ++ * Memory Usage Utility ++ * <p> ++ * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/> ++ * These may be changed or removed without notice! ++ * </p> ++ * @since 3.4.0 ++ */ ++public record _MemoryUsage(long usedInKibiBytes) { ++ ++ // -- UTILITIES ++ ++ static int indent = 0; ++ ++ @SneakyThrows ++ public static <T> T measureMetaspace(final String desc, final Callable<T> runnable) { ++ var before = metaspace(); ++ try { ++ indent++; ++ return runnable.call(); ++ } finally { ++ var after = metaspace(); ++ System.out.printf("%s%s : %s%n", spaces(indent), after.minus(before), desc); ++ indent--; ++ } ++ } ++ ++ public static void measureMetaspace(String desc, final Runnable runnable) { ++ var before = metaspace(); ++ try { ++ runnable.run(); ++ } finally { ++ var after = metaspace(); ++ System.out.printf("%s : %s%n", after.minus(before), desc); ++ } ++ } ++ ++ // -- FACTORY ++ ++ private static _MemoryUsage metaspace() { ++ for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) { ++ if (pool.getName().contains("Metaspace")) { ++ return new _MemoryUsage(pool.getUsage()); ++ } ++ } ++ throw new RuntimeException("Metaspace Usage not found"); ++ } ++ ++ // -- NON CANONICAL CONSTRUCTOR ++ ++ private _MemoryUsage(java.lang.management.MemoryUsage usage) { ++ this(usage.getUsed() / 1024); ++ } ++ ++ @Override ++ public String toString() { ++ return String.format("%,d KiB", usedInKibiBytes); ++ } ++ ++ // -- HELPER ++ ++ private static String spaces(int indent) { ++ return " ".repeat(indent * 2); ++ } ++ ++ private _MemoryUsage minus(_MemoryUsage before) { ++ return new _MemoryUsage(this.usedInKibiBytes - before.usedInKibiBytes); ++ } ++ ++} diff --cc core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java index f023673783a,e542b43f0c3..fd2881f5fa6 --- 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 @@@ -153,6 -150,10 +154,10 @@@ public interface HasMetaModelContext default InteractionService getInteractionService() { return getMetaModelContext().getInteractionService(); } - ++ + default CommandDtoFactory getCommandDtoFactory() { + return getMetaModelContext().getCommandDtoFactory(); + } default Optional<UserLocale> currentUserLocale() { return getInteractionService().currentInteractionContext() diff --cc core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java index fa8b51e936b,23ee4ceed92..dc8c32a46ae --- 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 @@@ -158,6 -161,10 +159,10 @@@ class MetaModelContext_usingSpring exte 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 --cc core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java index 21cde68eefb,00000000000..cd53c40cb7c mode 100644,000000..100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java @@@ -1,358 -1,0 +1,353 @@@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.causeway.applib.Identifier; +import org.apache.causeway.applib.annotation.Where; +import org.apache.causeway.applib.services.iactn.InteractionProvider; +import org.apache.causeway.commons.internal.assertions._Assert; +import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.core.metamodel.consent.Consent; +import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; +import org.apache.causeway.core.metamodel.context.HasMetaModelContext; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence; +import org.apache.causeway.core.metamodel.facetapi.FeatureType; +import org.apache.causeway.core.metamodel.facets.FacetedMethod; +import org.apache.causeway.core.metamodel.facets.HasFacetedMethod; +import org.apache.causeway.core.metamodel.facets.all.described.MemberDescribedFacet; +import org.apache.causeway.core.metamodel.facets.all.help.HelpFacet; +import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet; +import org.apache.causeway.core.metamodel.facets.all.i8n.staatic.HasStaticText; +import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; +import org.apache.causeway.core.metamodel.interactions.DisablingInteractionAdvisor; +import org.apache.causeway.core.metamodel.interactions.HidingInteractionAdvisor; +import org.apache.causeway.core.metamodel.interactions.InteractionContext; +import org.apache.causeway.core.metamodel.interactions.InteractionHead; +import org.apache.causeway.core.metamodel.interactions.InteractionUtils; +import org.apache.causeway.core.metamodel.interactions.acc.AccessContext; +import org.apache.causeway.core.metamodel.interactions.use.UsabilityContext; +import org.apache.causeway.core.metamodel.interactions.vis.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.ObjectSpecification; +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; + +import lombok.Getter; +import org.jspecify.annotations.NonNull; + +abstract class ObjectMemberAbstract +implements + ObjectMember, + HasFacetedMethod, + HasMetaModelContext, + Serializable { + private static final long serialVersionUID = 1L; + + @Getter(onMethod_ = {@Override}) private final @NonNull Identifier featureIdentifier; + @Getter(onMethod_ = {@Override}) private final @NonNull FeatureType featureType; + @Getter(onMethod_ = {@Override}) private final @NonNull FacetedMethod facetedMethod; + + // -- CONSTRUCTOR + + protected ObjectMemberAbstract( + final Identifier featureIdentifier, + final FacetedMethod facetedMethod, + final FeatureType featureType) { + this.featureIdentifier = featureIdentifier; + this.facetedMethod = facetedMethod; + this.featureType = featureType; + if (getId() == null) { + throw new IllegalArgumentException("Id must always be set"); + } + } + + // -- IDENTIFIERS + + @Override + public final String getId() { + return getFeatureIdentifier().memberLogicalName(); + } + + // -- INTERACTION HEAD + + /** + * To be overridden (only) by mixed in members! + * @see MixedInMember + */ + protected InteractionHead headFor(final ManagedObject ownerAdapter) { + return InteractionHead.regular(ownerAdapter); + } + + // -- Name, Description, Help (convenience for facets) + + @Override + public final String getFriendlyName(final Supplier<ManagedObject> domainObjectProvider) { + + var namedFacet = getFacet(MemberNamedFacet.class); + + if(namedFacet==null) { + throw _Exceptions.unrecoverable("no MemberNamedFacet preset on %s", getFeatureIdentifier()); + } + + return namedFacet + .getSpecialization() + .fold( textFacet->textFacet.translated(), + textFacet->textFacet.textElseNull(headFor(domainObjectProvider.get()).target())); + } + + @Override + public final Optional<String> getStaticFriendlyName() { + return lookupFacet(MemberNamedFacet.class) + .map(MemberNamedFacet::getSpecialization) + .flatMap(specialization->specialization + .fold( + textFacet->Optional.of(textFacet.translated()), + textFacet->Optional.empty())); + } + + @Override + public final Optional<String> getDescription(final Supplier<ManagedObject> domainObjectProvider) { + return lookupFacet(MemberDescribedFacet.class) + .map(MemberDescribedFacet::getSpecialization) + .map(specialization->specialization + .fold(textFacet->textFacet.translated(), + textFacet->textFacet.textElseNull(headFor(domainObjectProvider.get()).target()))); + } + + @Override + public final Optional<String> getStaticDescription() { + return lookupFacet(MemberDescribedFacet.class) + .map(MemberDescribedFacet::getSpecialization) + .flatMap(specialization->specialization + .fold( + textFacet->Optional.of(textFacet.translated()), + textFacet->Optional.empty())); + } + + @Override + public final String getHelp() { + final HelpFacet facet = getFacet(HelpFacet.class); + return facet.value(); + } + + // -- CANONICAL NAMING + + @Override + public final String getCanonicalFriendlyName() { + return lookupFacet(MemberNamedFacet.class) + .flatMap(MemberNamedFacet::getSharedFacetRanking) + .flatMap(facetRanking->facetRanking.getWinnerNonEventLowerOrEqualTo(MemberNamedFacet.class, Precedence.HIGH)) + .map(MemberNamedFacet::getSpecialization) + .flatMap(specialization->specialization.left()) + .map(HasStaticText::translated) + //we have a facet-post-processor to ensure following code path is unreachable, + //however, we keep it in support of JUnit testing + .orElseGet(()->getFeatureIdentifier().getMemberNaturalName()); + } + + @Override + public final Optional<String> getCanonicalDescription() { + return lookupFacet(MemberDescribedFacet.class) + .flatMap(MemberDescribedFacet::getSharedFacetRanking) + .flatMap(facetRanking->facetRanking.getWinnerNonEventLowerOrEqualTo(MemberDescribedFacet.class, Precedence.HIGH)) + .map(MemberDescribedFacet::getSpecialization) + .flatMap(specialization->specialization.left()) + .map(HasStaticText::translated); + } + + // -- Hidden (or visible) + /** + * Create an {@link InteractionContext} to represent an attempt to view this + * member (that is, to check if it is visible or not). + * + * <p> + * Typically it is easier to just call + * {@link ObjectMember#isVisible(ManagedObject, InteractionInitiatedBy, Where)}; this is + * provided as API for symmetry with interactions (such as + * {@link AccessContext} accesses) have no corresponding vetoing methods. + */ + protected abstract VisibilityContext createVisibleInteractionContext( + final ManagedObject target, + final InteractionInitiatedBy interactionInitiatedBy, + final Where where); + + @Override + public boolean isAlwaysHidden() { + return HiddenFacet.isAlwaysHidden(getFacetHolder()); + } + + /** + * Loops over all {@link HidingInteractionAdvisor} {@link Facet}s and + * returns <tt>true</tt> only if none hide the member. + */ + @Override + public Consent isVisible( + final ManagedObject target, + final InteractionInitiatedBy interactionInitiatedBy, + final Where where) { + + var visibilityContext = createVisibleInteractionContext(target, interactionInitiatedBy, where); + return InteractionUtils.isVisibleResult(this, visibilityContext).createConsent(); + } + + // -- Disabled (or enabled) + /** + * Create an {@link InteractionContext} to represent an attempt to + * use this member (that is, to check if it is usable or not). + * + * <p> + * Typically it is easier to just call + * {@link ObjectMember#isUsable(ManagedObject, InteractionInitiatedBy, Where)}; this is + * provided as API for symmetry with interactions (such as + * {@link AccessContext} accesses) have no corresponding vetoing methods. + */ + protected abstract UsabilityContext createUsableInteractionContext( + final ManagedObject target, + final InteractionInitiatedBy interactionInitiatedBy, + final Where where); + + /** + * Loops over all {@link DisablingInteractionAdvisor} {@link Facet}s and + * returns <tt>true</tt> only if none disables the member. + */ + @Override + public Consent isUsable( + final ManagedObject target, + final InteractionInitiatedBy interactionInitiatedBy, + final Where where) { + + var usabilityContext = createUsableInteractionContext(target, interactionInitiatedBy, where); + return InteractionUtils.isUsableResult(this, usabilityContext).createConsent(); + } + + // -- PREDICATES + + @Override + public final boolean isAction() { + return featureType.isAction(); + } + + @Override + public final boolean isPropertyOrCollection() { + return featureType.isPropertyOrCollection(); + } + + @Override + public final boolean isOneToManyAssociation() { + return featureType.isCollection(); + } + + @Override + public final boolean isOneToOneAssociation() { + return featureType.isProperty(); + } + + // -- MIXIN ADAPTER FACTORY + + protected ManagedObject mixinAdapterFor( + final @NonNull ObjectSpecification mixinSpec, + final @NonNull ManagedObject mixee) { + + // nullable for action parameter mixins + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mixee)) { + return ManagedObject.empty(mixinSpec); + } + + var mixinPojo = getFactoryService().mixin(mixinSpec.getCorrespondingClass(), mixee.getPojo()); + return ManagedObject.mixin(mixinSpec, mixinPojo); + } + + // -- OBJECT CONTRACT + + @Override + public String toString() { + return getStaticFriendlyName() + .map(name->String.format("id=%s,name='%s'", getId(), name)) + .orElseGet(()->String.format("id=%s,name=imperative", getId())); + } + + // -- COMMAND SETUP + + protected void setupCommand( + final InteractionHead head, + final Function<UUID, CommandDto> commandDtoFactory) { + + var command = getInteractionContext().currentInteractionElseFail().getCommand(); + + _Assert.assertNotNull(command, + "No command available with current thread, " + + "are we missing an interaction context?"); + + if (command.getCommandDto() != null) { + // guard here to prevent subsequent mixin actions from + // trampling over the command's DTO + } else { + var dto = commandDtoFactory.apply(command.getInteractionId()); + command.updater().setCommandDtoAndIdentifier(dto); + } + + } + + // -- DEPENDENCIES + + 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()); + } + + // -- SERIALIZATION PROXY + + protected final Object writeReplace() { + return new SerializationProxy(this); + } + + protected final void readObject(final ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); + } + + protected record SerializationProxy(Identifier identifier) implements Serializable { + SerializationProxy(final ObjectMember objectMember) { + this(objectMember.getFeatureIdentifier()); + } + private Object readResolve() { + return MetaModelContext.instanceElseFail() + .getSpecificationLoader() + .specForLogicalTypeElseFail(identifier.logicalType()) + .getMemberElseFail( + identifier.getMemberNameAndParameterClassNamesIdentityString()); + } + } + +} diff --cc core/runtimeservices/src/main/java/module-info.java index 219035955e9,e8a87d7f84e..f435d98c1ad --- a/core/runtimeservices/src/main/java/module-info.java +++ b/core/runtimeservices/src/main/java/module-info.java @@@ -78,8 -79,8 +78,9 @@@ module org.apache.causeway.core.runtime requires spring.tx; requires org.apache.causeway.core.codegen.bytebuddy; requires spring.aop; + requires java.management; - + + opens org.apache.causeway.core.runtimeservices; opens org.apache.causeway.core.runtimeservices.wrapper; - opens org.apache.causeway.core.runtimeservices.wrapper.proxy; //to org.apache.causeway.core.codegen.bytebuddy + opens org.apache.causeway.core.runtimeservices.wrapper.handlers; //to org.apache.causeway.core.codegen.bytebuddy } diff --cc extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java index 3d761ece825,98f2b1781d5..3de5bda8de8 --- 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 @@@ -36,9 -36,9 +36,11 @@@ import org.apache.wicket.request.cycle. import org.apache.wicket.request.handler.resource.ResourceRequestHandler; import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException; import org.apache.wicket.request.resource.ByteArrayResource; ++import org.jspecify.annotations.NonNull; import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.applib.value.Blob; ++import org.apache.causeway.commons.internal.base._NullSafe; import org.apache.causeway.core.metamodel.object.MmUnwrapUtils; import org.apache.causeway.extensions.pdfjs.applib.config.PdfJsConfig; import org.apache.causeway.extensions.pdfjs.applib.config.Scale; @@@ -49,8 -49,9 +51,6 @@@ import org.apache.causeway.viewer.wicke import org.apache.causeway.viewer.wicket.ui.util.Wkt; import org.apache.causeway.viewer.wicket.ui.util.WktComponents; - import org.jspecify.annotations.NonNull; -import lombok.NonNull; -import lombok.val; -- import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel; /** @@@ -176,35 -177,36 +176,39 @@@ implements IRequestListener @Override protected MarkupContainer createRegularFrame() { - val blob = getBlob(); - if (blob == null) { + var blob = getBlob(); - if (blob == null) { ++ if (blob == null ++ || _NullSafe.isEmpty(blob.bytes())) { return createShallowRegularFrame(); } - val scalarModel = scalarModel(); + var attributeModel = attributeModel(); - val regularFrame = new WebMarkupContainer(ID_SCALAR_IF_REGULAR); + var regularFrame = new WebMarkupContainer(ID_SCALAR_IF_REGULAR); - CharSequence documentUrl = urlFor( ++ var documentUrl = urlFor( + new ListenerRequestHandler( - new PageAndComponentProvider(getPage(), this))) + "&noCache=" + System.currentTimeMillis(); - val pdfJsConfig = - scalarModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class) ++ new PageAndComponentProvider(getPage(), this))) ++ // adds a hash to the URL, such that browser caching works as desired ++ + "&md5=" + blob.md5Hex(); + var pdfJsConfig = - attributeModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class) ++ attributeModel.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); + var pdfJsPanel = new PdfJsPanel(ID_SCALAR_VALUE, pdfJsConfig); - val prevPageButton = createToolbarComponent("prevPage", pdfJsPanel); - val nextPageButton = createToolbarComponent("nextPage", pdfJsPanel); - val currentZoomSelect = createToolbarComponent("currentZoom", pdfJsPanel); - val currentPageLabel = createToolbarComponent("currentPage", pdfJsPanel); - val totalPagesLabel = createToolbarComponent("totalPages", pdfJsPanel); + var prevPageButton = createToolbarComponent("prevPage", pdfJsPanel); + var nextPageButton = createToolbarComponent("nextPage", pdfJsPanel); + var currentZoomSelect = createToolbarComponent("currentZoom", pdfJsPanel); + var currentPageLabel = createToolbarComponent("currentPage", pdfJsPanel); + var totalPagesLabel = createToolbarComponent("totalPages", pdfJsPanel); - val currentHeightSelect = createToolbarComponent("currentHeight", pdfJsPanel); - val printButton = createToolbarComponent("print", pdfJsPanel); + var currentHeightSelect = createToolbarComponent("currentHeight", pdfJsPanel); + var printButton = createToolbarComponent("print", pdfJsPanel); - val downloadResourceLink = Wkt.downloadLinkNoCache(ID_DOWNLOAD, asBlobResource(blob)); + var downloadResourceLink = Wkt.downloadLinkNoCache(ID_DOWNLOAD, asBlobResource(blob)); regularFrame.addOrReplace( pdfJsPanel, prevPageButton, nextPageButton, currentPageLabel, totalPagesLabel, @@@ -230,9 -233,9 +234,9 @@@ if (blob == null) { return createShallowCompactFrame(); } - val compactFrame = new WebMarkupContainer(ID_SCALAR_IF_COMPACT); - val downloadLink = Wkt.add(compactFrame, Wkt.downloadLinkNoCache(ID_DOWNLOAD_IF_COMPACT, asBlobResource(blob))); - Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.getName()); + var compactFrame = new WebMarkupContainer(ID_SCALAR_IF_COMPACT); + var downloadLink = Wkt.add(compactFrame, Wkt.downloadLinkNoCache(ID_DOWNLOAD_IF_COMPACT, asBlobResource(blob))); - Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.getName()); ++ Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.name()); return compactFrame; } @@@ -282,28 -285,28 +286,18 @@@ new ResourceRequestHandler(asBlobResourceNoCache(blob), null)); } --// @Override --// protected void setupInlinePrompt() { --// // not used --// } --// --// @Override --// protected Component getValidationFeedbackReceiver() { --// return null; // not used --// } -- // -- HELPER -- ++ private Blob getBlob() { - return (Blob) MmUnwrapUtils.single(scalarModel().getObject()); + return (Blob) MmUnwrapUtils.single(attributeModel().getObject()); } private static ByteArrayResource asBlobResource(final @NonNull Blob blob) { -- return new ByteArrayResource(blob.getMimeType().getBaseType(), blob.getBytes(), blob.getName()); ++ return new ByteArrayResource(blob.mimeType().getBaseType(), blob.bytes(), blob.name()); } private static ByteArrayResource asBlobResourceNoCache(final @NonNull Blob blob) { -- final byte[] bytes = blob.getBytes(); ++ final byte[] bytes = blob.bytes(); return new ByteArrayResource("application/pdf", bytes) { private static final long serialVersionUID = 1L; @Override protected void configureResponse( diff --cc regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java index 3a7ae7b0c64,00000000000..de761d9ffa1 mode 100644,000000..100644 --- a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java +++ b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java @@@ -1,62 -1,0 +1,68 @@@ +/* + * 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.jpa; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.apache.causeway.applib.annotation.Action; +import org.apache.causeway.applib.annotation.Collection; +import org.apache.causeway.applib.annotation.DomainObject; +import org.apache.causeway.applib.annotation.Nature; +import org.apache.causeway.applib.domain.DomainObjectList.ActionDomainEvent; +import org.apache.causeway.applib.services.repository.RepositoryService; +import org.apache.causeway.testdomain.jpa.entities.JpaProduct; + +@DomainObject(nature = Nature.VIEW_MODEL) +public class JpaInventoryManager { + + @Inject private RepositoryService repository; + + // -- UPDATE PRODUCT PRICE + + public static class UpdateProductPriceEvent extends ActionDomainEvent {} + + @Action( + domainEvent = UpdateProductPriceEvent.class, + choicesFrom = "allProducts") + public JpaProduct updateProductPrice(final JpaProduct product, final double newPrice) { + product.setPrice(newPrice); + return product; + } ++ ++ // -- WRAPPER MEMORY LEAK TESTING ++ ++ @Action ++ public void foo() { ++ } + + // -- COUNT PRODUCTS + + @Action + public int countProducts() { + return getAllProducts().size(); + } + + @Collection + public List<JpaProduct> getAllProducts() { + return repository.allInstances(JpaProduct.class); + } + +} diff --cc regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java index 00000000000,c0c34b16f6f..0b60039686e mode 000000,100644..100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java @@@ -1,0 -1,148 +1,149 @@@ + /* + * 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 javax.inject.Inject; ++import jakarta.inject.Inject; + + import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; + import static org.junit.jupiter.api.Assertions.assertThrows; + import static org.junit.jupiter.api.Assertions.assertTrue; + ++import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.test.context.TestPropertySource; ++ + 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.services.wrapper.InvalidException; + import org.apache.causeway.core.config.presets.CausewayPresets; + import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; + import org.apache.causeway.core.metamodel.spec.feature.MixedIn; + import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; + 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.model.interaction.InteractionDemo; + import org.apache.causeway.testdomain.model.interaction.InteractionDemo_biArgEnabled; + import org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract; + + import lombok.Data; + import lombok.RequiredArgsConstructor; + import lombok.val; + + @SpringBootTest( + classes = { + Configuration_headless.class, + Configuration_usingInteractionDomain.class, + WrapperInteraction_1_IntegTest.Customer.class, + WrapperInteraction_1_IntegTest.ConcreteMixin.class, + WrapperInteraction_1_IntegTest.ConcreteMixin2.class, + } + ) + @TestPropertySource({ + CausewayPresets.SilenceMetaModel, + CausewayPresets.SilenceProgrammingModel + }) + class WrapperInteraction_1_IntegTest + extends InteractionTestAbstract { + + @Data @DomainObject(nature = Nature.VIEW_MODEL) + static class Customer { + String name; + @Action public String who() { return name; } + } + + // an abstract mixin class + static abstract class MixinAbstract<T extends Object> { + public T act(final String startTime, final String endTime) { + return null; + } + } + + @Action + @RequiredArgsConstructor + public static class ConcreteMixin + extends MixinAbstract<String> { + @SuppressWarnings("unused") + private final Customer mixee; + @Override + public String act(final String startTime, final String endTime) { + return "acted"; + } + } + + @Action + @RequiredArgsConstructor + public static class ConcreteMixin2 + extends MixinAbstract<String> { + @SuppressWarnings("unused") + private final Customer mixee; + + @Override + public String act(final String startTime, final String endTime) { + return "acted2"; + } + } + + @Inject SpecificationLoader specificationLoader; + + @Test + void mixinMemberNamedFacet_whenSharingSameAbstractMixin() { + val objectSpec = specificationLoader.specForType(Customer.class).get(); + + assertEquals( + 2L, + objectSpec.streamRuntimeActions(MixedIn.INCLUDED) + .filter(ObjectAction::isMixedIn) + .peek(act->{ + //System.out.println("act: " + act); + val memberNamedFacet = act.getFacet(MemberNamedFacet.class); + assertNotNull(memberNamedFacet); + assertTrue(memberNamedFacet.getSpecialization().isLeft()); + }) + .count()); + } + + @Test + void mixinActionValidation() { + InvalidException cause = assertThrows(InvalidException.class, ()-> { + wrapMixin(ConcreteMixin.class, new Customer()).act(null, "17:00"); + }); + assertEquals("'Start Time' is mandatory", cause.getMessage()); + + InvalidException cause2 = assertThrows(InvalidException.class, ()-> { + wrapMixin(ConcreteMixin2.class, new Customer()).act(null, "17:00"); + }); + assertEquals("'Start Time' is mandatory", cause2.getMessage()); + } + + @Test + void regularPropertyAccess() { + assertEquals("initial", wrapper.wrap(new InteractionDemo()).getString2()); + } + + @Test + void mixinActionAccess() { + assertEquals(3, wrapper.wrapMixin(InteractionDemo_biArgEnabled.class, new InteractionDemo()).act(1, 2)); + } + + + } diff --cc regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java index 00000000000,9855529a747..60e1555cdfd mode 000000,100644..100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java @@@ -1,0 -1,131 +1,132 @@@ + /* + * 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 javax.inject.Inject; ++import jakarta.inject.Inject; + + import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; + import static org.junit.jupiter.api.Assertions.assertThrows; + import static org.junit.jupiter.api.Assertions.assertTrue; + ++import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.test.context.TestPropertySource; ++ + 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.services.wrapper.InvalidException; + import org.apache.causeway.core.config.presets.CausewayPresets; + import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; + import org.apache.causeway.core.metamodel.spec.feature.MixedIn; + import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; + 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 lombok.Data; + import lombok.val; + + @SpringBootTest( + classes = { + Configuration_headless.class, + Configuration_usingInteractionDomain.class, + WrapperInteraction_2_IntegTest.Customer.class, + WrapperInteraction_2_IntegTest.Customer.ConcreteMixin.class, + WrapperInteraction_2_IntegTest.Customer.ConcreteMixin2.class, + } + ) + @TestPropertySource({ + CausewayPresets.SilenceMetaModel, + CausewayPresets.SilenceProgrammingModel + }) + class WrapperInteraction_2_IntegTest + extends InteractionTestAbstract { + + @Data @DomainObject(nature = Nature.VIEW_MODEL) + public static class Customer { + String name; + @Action public String who() { return name; } + + @Action + public class ConcreteMixin + extends MixinAbstract<String> { + + @Override + public String act(final String startTime, final String endTime) { + return "acted"; + } + } + + @Action + public class ConcreteMixin2 + extends MixinAbstract<String> { + + @Override + public String act(final String startTime, final String endTime) { + return "acted2"; + } + } + + } + + // an abstract mixin class + static abstract class MixinAbstract<T extends Object> { + public T act(final String startTime, final String endTime) { + return null; + } + } + + @Inject SpecificationLoader specificationLoader; + + @Test + void mixinMemberNamedFacet_whenSharingSameAbstractMixin() { + val objectSpec = specificationLoader.specForType(Customer.class).get(); + + assertEquals( + 2L, + objectSpec.streamRuntimeActions(MixedIn.INCLUDED) + .filter(ObjectAction::isMixedIn) + .peek(act->{ + //System.out.println("act: " + act); + val memberNamedFacet = act.getFacet(MemberNamedFacet.class); + assertNotNull(memberNamedFacet); + assertTrue(memberNamedFacet.getSpecialization().isLeft()); + }) + .count()); + } + + @Test + void mixinActionValidation() { + + InvalidException cause = assertThrows(InvalidException.class, ()-> { + wrapMixin(Customer.ConcreteMixin.class, new Customer()).act(null, "17:00"); + }); + assertEquals("'Start Time' is mandatory", cause.getMessage()); + + InvalidException cause2 = assertThrows(InvalidException.class, ()-> { + wrapMixin(Customer.ConcreteMixin2.class, new Customer()).act(null, "17:00"); + }); + assertEquals("'Start Time' is mandatory", cause2.getMessage()); + } + + } diff --cc regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java index 00000000000,cb665b24b80..571710da007 mode 000000,100644..100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java @@@ -1,0 -1,157 +1,158 @@@ + /* + * 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 java.util.Arrays; + import java.util.List; + import java.util.stream.Collectors; + -import javax.inject.Inject; ++import jakarta.inject.Inject; + + import org.assertj.core.api.Assertions; + import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; + import static org.junit.jupiter.api.Assertions.assertTrue; + ++import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.test.context.TestPropertySource; ++ + 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.Optionality; + import org.apache.causeway.applib.annotation.Property; + import org.apache.causeway.core.config.presets.CausewayPresets; + import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; + import org.apache.causeway.core.metamodel.spec.feature.MixedIn; + import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; + 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 lombok.Data; + import lombok.RequiredArgsConstructor; + import lombok.val; + + @SpringBootTest( + classes = { + Configuration_headless.class, + Configuration_usingInteractionDomain.class, + + WrapperInteraction_3_IntegTest.Task.class, + WrapperInteraction_3_IntegTest.Task.Succeeded.class, + WrapperInteraction_3_IntegTest.Task.Failed.class, + } + ) + @TestPropertySource({ + CausewayPresets.SilenceMetaModel, + CausewayPresets.SilenceProgrammingModel + }) + class WrapperInteraction_3_IntegTest + extends InteractionTestAbstract { + + @Data @DomainObject(nature = Nature.VIEW_MODEL) + public static class Task { + + @RequiredArgsConstructor + enum Outcome { + SUPER(true), + GREAT(true), + OK(true), + BAD(false), + TERRIBLE(false), + JUST_GIVE_UP(false),; + + final boolean representsSuccess; + + public static List<Outcome> successes() { + return Arrays.stream(Outcome.values()).filter(x -> x.representsSuccess).collect(Collectors.toList()); + } + public static List<Outcome> failures() { + return Arrays.stream(Outcome.values()).filter(x -> ! x.representsSuccess).collect(Collectors.toList()); + } + } + + @Property(optionality = Optionality.OPTIONAL) + Outcome outcome; + + @Action + public class Succeeded + extends MixinAbstract { + public List<Task.Outcome> choices0Act() { return Task.Outcome.successes(); } + } + + @Action + public class Failed + extends MixinAbstract { + public List<Task.Outcome> choices0Act() { return Task.Outcome.failures(); } + } + + // an abstract (inner) mixin class (required public if introspection policy does not process private methods) + public abstract class MixinAbstract { + public Task act(final Task.Outcome outcome) { + Task.this.outcome = outcome; + return Task.this; + } + } + } + + @Inject SpecificationLoader specificationLoader; + + @Test + void mixinMemberNamedFacet_whenSharingSameAbstractMixin() { + + val mixinSpec1 = specificationLoader.specForType(Task.Succeeded.class).get(); + val mixinSpec2 = specificationLoader.specForType(Task.Failed.class).get(); + + assertTrue(mixinSpec1.isMixin()); + assertTrue(mixinSpec2.isMixin()); + + assertTrue(mixinSpec1.mixinFacetElseFail().isMixinFor(Task.class)); + assertTrue(mixinSpec2.mixinFacetElseFail().isMixinFor(Task.class)); + + val objectSpec = specificationLoader.specForType(Task.class).get(); + + assertEquals( + 2L, + objectSpec.streamRuntimeActions(MixedIn.INCLUDED) + .filter(ObjectAction::isMixedIn) + .peek(act->{ + val memberNamedFacet = act.getFacet(MemberNamedFacet.class); + assertNotNull(memberNamedFacet); + assertTrue(memberNamedFacet.getSpecialization().isLeft()); + }) + .count()); + } + + @Test + void mixinActionValidation() { + + final Task task = new Task(); + + wrapMixin(Task.Succeeded.class, task).act(Task.Outcome.SUPER); + Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.SUPER); + + wrapMixin(Task.Failed.class, task).act(Task.Outcome.JUST_GIVE_UP); + Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.JUST_GIVE_UP); + } + + } diff --cc regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java index 00000000000,70287bff640..0fc536ade52 mode 000000,100644..100644 --- a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java +++ b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java @@@ -1,0 -1,151 +1,152 @@@ + /* + * 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 java.util.Arrays; + import java.util.List; + import java.util.stream.Collectors; + -import javax.inject.Inject; ++import jakarta.inject.Inject; + + import org.assertj.core.api.Assertions; + import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; + import static org.junit.jupiter.api.Assertions.assertTrue; + ++import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.test.context.TestPropertySource; ++ + 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.Optionality; + import org.apache.causeway.applib.annotation.Property; + import org.apache.causeway.core.config.presets.CausewayPresets; + import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; + import org.apache.causeway.core.metamodel.spec.feature.MixedIn; + import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; + 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 lombok.Data; + import lombok.RequiredArgsConstructor; + import lombok.val; + + @SpringBootTest( + classes = { + Configuration_headless.class, + Configuration_usingInteractionDomain.class, + WrapperInteraction_4_IntegTest.Task.class, + WrapperInteraction_4_IntegTest.Task.Succeeded.class, + WrapperInteraction_4_IntegTest.Task.Failed.class, + } + ) + @TestPropertySource({ + CausewayPresets.SilenceMetaModel, + CausewayPresets.SilenceProgrammingModel + }) + class WrapperInteraction_4_IntegTest + extends InteractionTestAbstract { + + @Data @DomainObject(nature = Nature.VIEW_MODEL) + public static class Task { + + @RequiredArgsConstructor + enum Outcome { + SUPER(true), + GREAT(true), + OK(true), + BAD(false), + TERRIBLE(false), + JUST_GIVE_UP(false),; + + final boolean representsSuccess; + + public static List<Outcome> successes() { + return Arrays.stream(Outcome.values()).filter(x -> x.representsSuccess).collect(Collectors.toList()); + } + public static List<Outcome> failures() { + return Arrays.stream(Outcome.values()).filter(x -> ! x.representsSuccess).collect(Collectors.toList()); + } + } + + @Property(optionality = Optionality.OPTIONAL) + Outcome outcome; + + @Action + public static class Succeeded + extends MixinAbstract { + public Succeeded(final Task task) { super(task); } + public List<Task.Outcome> choices0Act() { return Task.Outcome.successes(); } + } + + @Action + public static class Failed + extends MixinAbstract { + public Failed(final Task task) { super(task); } + public List<Task.Outcome> choices0Act() { return Task.Outcome.failures(); } + } + + // an abstract mixin class (required public if introspection policy does not process private methods) + @RequiredArgsConstructor + public abstract static class MixinAbstract { + private final Task task; + public Task act(final Task.Outcome outcome) { + task.outcome = outcome; + return task; + } + } + } + + @Inject SpecificationLoader specificationLoader; + + @Test + void mixinMemberNamedFacet_whenSharingSameAbstractMixin() { + val objectSpec = specificationLoader.specForType(Task.class).get(); + + assertEquals( + 2L, + objectSpec.streamRuntimeActions(MixedIn.INCLUDED) + .filter(ObjectAction::isMixedIn) + .peek(act->{ + //System.out.println("act: " + act); + val memberNamedFacet = act.getFacet(MemberNamedFacet.class); + assertNotNull(memberNamedFacet); + assertTrue(memberNamedFacet.getSpecialization().isLeft()); + }) + .count()); + } + + @Test + void mixinActionValidation() { + + final Task task = new Task(); + + wrapMixin(Task.Succeeded.class, task).act(Task.Outcome.SUPER); + Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.SUPER); + + wrapMixin(Task.Failed.class, task).act(Task.Outcome.JUST_GIVE_UP); + Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.JUST_GIVE_UP); + } + + } diff --cc regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java index 00000000000,cf2ba4cd808..c8763437802 mode 000000,100644..100644 --- 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 @@@ -1,0 -1,144 +1,141 @@@ + /* + * 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.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; + + 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; ++import lombok.Data; ++import lombok.Getter; ++import lombok.RequiredArgsConstructor; + + @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<Integer> asyncControlForCalculator1 = AsyncControl.returning(Integer.class); + StatefulCalculator asyncCalculator1 = wrapperFactory.asyncWrap(calculator1, asyncControlForCalculator1); + + AsyncControl<Integer> asyncControlForCalculator2 = AsyncControl.returning(Integer.class); + StatefulCalculator asyncCalculator2 = wrapperFactory.asyncWrap(calculator2, asyncControlForCalculator2); + + asyncCalculator1.inc(12); + asyncCalculator2.inc(24); + + // then + Future<Integer> 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 --cc regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/JpaWrapperSyncTest.java index 00000000000,00000000000..a62821d1e69 new file mode 100644 --- /dev/null +++ b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/JpaWrapperSyncTest.java @@@ -1,0 -1,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.persistence.jpa.wrapper; ++ ++import jakarta.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.fixtures.EntityTestFixtures; ++import org.apache.causeway.testdomain.jpa.JpaInventoryManager; ++import org.apache.causeway.testdomain.jpa.JpaTestFixtures; ++import org.apache.causeway.testdomain.jpa.conf.Configuration_usingJpa; ++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 --cc regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java index 00000000000,00000000000..18dd21de935 new file mode 100644 --- /dev/null +++ b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java @@@ -1,0 -1,0 +1,107 @@@ ++/* ++ * 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.persistence.jpa.wrapper; ++ ++import jakarta.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.internal.base._Blackhole; ++import org.apache.causeway.commons.internal.debug._MemoryUsage; ++import org.apache.causeway.core.config.presets.CausewayPresets; ++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.conf.Configuration_usingJpa; ++import org.apache.causeway.testdomain.jpa.entities.JpaProduct; ++import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract; ++ ++@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,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,263 KB. // ? increased some, is it significant; a lot less than without caching ++// exercise(2000, 200); // 4,294 KB. ++// 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); // 14,580 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++) { ++ var jpaInventoryManager = wrapper.wrap(factoryService.viewModel(JpaInventoryManager.class)); ++ jpaInventoryManager.foo(); ++ ++ for (var j = 0; j < loops; j++) { ++ jpaInventoryManager ++ .getAllProducts() ++ .stream() ++ .map(JpaProduct::getName) ++ .forEach(_Blackhole::consume); ++ } ++ } ++ } ++} ++ ++
