You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2025/06/13 12:39:49 UTC
(causeway) 01/01: CAUSEWAY-3891 and CAUSEWAY-3883: merges from maintenance branch
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 <ah...@apache.org>
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);
++ }
++ }
++ }
++}
++
++