From fd80f11a80fa31be7b87a5aaa9b0c5e7bd26fdb7 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 28 Jan 2026 17:12:52 +0100 Subject: [PATCH 1/6] CAUSEWAY-3959: converts abstract ApplicationRole to an interface Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-3959 --- .../applib/role/dom/ApplicationRole.java | 59 ++++--------------- .../secman/jpa/role/dom/ApplicationRole.java | 37 ++++++++++-- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java index 3577bb3fe16..53131cef71b 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java @@ -26,13 +26,8 @@ import java.util.List; import java.util.Set; -import jakarta.inject.Inject; -import jakarta.inject.Named; - import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.CollectionLayout; -import org.apache.causeway.applib.annotation.DomainObject; -import org.apache.causeway.applib.annotation.DomainObjectLayout; import org.apache.causeway.applib.annotation.Editing; import org.apache.causeway.applib.annotation.ObjectSupport; import org.apache.causeway.applib.annotation.Optionality; @@ -46,38 +41,23 @@ import org.apache.causeway.applib.util.ToString; import org.apache.causeway.extensions.secman.applib.CausewayModuleExtSecmanApplib; import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission; -import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository; import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser; -import lombok.experimental.UtilityClass; - /** * @since 2.0 {@index} */ -@Named(ApplicationRole.LOGICAL_TYPE_NAME) -@DomainObject( - autoCompleteRepository = ApplicationRoleRepository.class, - autoCompleteMethod = "findMatching" - ) -@DomainObjectLayout( - titleUiEvent = ApplicationRole.TitleUiEvent.class, - iconUiEvent = ApplicationRole.IconUiEvent.class, - cssClassUiEvent = ApplicationRole.CssClassUiEvent.class, - layoutUiEvent = ApplicationRole.LayoutUiEvent.class -) -public abstract class ApplicationRole implements Comparable { +public interface ApplicationRole extends Comparable { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationRole"; public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA; public static final String TABLE = "ApplicationRole"; - @UtilityClass - public static class Nq { + public static final class Nq { public static final String FIND_BY_NAME = LOGICAL_TYPE_NAME + ".findByName"; public static final String FIND_BY_NAME_CONTAINING = LOGICAL_TYPE_NAME + ".findByNameContaining"; } - @Inject transient private ApplicationPermissionRepository applicationPermissionRepository; + org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository applicationPermissionRepository(); // -- UI & DOMAIN EVENTS @@ -91,7 +71,7 @@ public static abstract class CollectionDomainEvent extends CausewayModuleExtS // -- MODEL - @ObjectSupport public String title() { + @ObjectSupport public default String title() { return getName(); } @@ -199,42 +179,27 @@ class DomainEvent extends CollectionDomainEvent {} // -- PERMISSIONS @Permissions - public List getPermissions() { - return applicationPermissionRepository.findByRole(this); + public default List getPermissions() { + return applicationPermissionRepository().findByRole(this); } // -- equals, hashCode, compareTo, toString - private static final Comparator comparator = + static final Comparator COMPARATOR = Comparator.comparing(ApplicationRole::getName); - private static final Equality equality = + static final Equality EQUALITY = ObjectContracts.checkEquals(ApplicationRole::getName); - private static final Hashing hashing = + static final Hashing HASHING = ObjectContracts.hashing(ApplicationRole::getName); - private static final ToString toString = + static final ToString TOSTRING = ObjectContracts.toString("name", ApplicationRole::getName); @Override - public int compareTo(final org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole other) { - return comparator.compare(this, other); - } - - @Override - public boolean equals(final Object obj) { - return equality.equals(this, obj); - } - - @Override - public int hashCode() { - return hashing.hashCode(this); - } - - @Override - public String toString() { - return toString.toString(this); + public default int compareTo(final org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole other) { + return COMPARATOR.compare(this, other); } } diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/role/dom/ApplicationRole.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/role/dom/ApplicationRole.java index c6f9b017e24..0121d401453 100644 --- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/role/dom/ApplicationRole.java +++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/role/dom/ApplicationRole.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.TreeSet; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; @@ -40,14 +41,18 @@ import org.apache.causeway.applib.annotation.Bounding; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainObjectLayout; +import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.jaxb.PersistentEntityAdapter; import org.apache.causeway.commons.internal.base._Casts; +import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository; import org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole.Nq; +import org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRoleRepository; import org.apache.causeway.extensions.secman.jpa.user.dom.ApplicationUser; import org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; @Entity @Table( @@ -74,13 +79,20 @@ @DomainObject( bounding = Bounding.BOUNDED, autoCompleteRepository = ApplicationRoleRepository.class, - autoCompleteMethod = "findMatching" - ) + autoCompleteMethod = "findMatching") @DomainObjectLayout( - bookmarking = BookmarkPolicy.AS_ROOT - ) + bookmarking = BookmarkPolicy.AS_ROOT, + titleUiEvent = ApplicationRole.TitleUiEvent.class, + iconUiEvent = ApplicationRole.IconUiEvent.class, + cssClassUiEvent = ApplicationRole.CssClassUiEvent.class, + layoutUiEvent = ApplicationRole.LayoutUiEvent.class) public class ApplicationRole - extends org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole { + implements org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole { + + @Inject + @Programmatic + @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + transient private ApplicationPermissionRepository applicationPermissionRepository; @Id @GeneratedValue @@ -113,4 +125,19 @@ public void addToUsers(final ApplicationUser applicationUser) { getUsers().add(applicationUser); } + @Override + public boolean equals(final Object obj) { + return EQUALITY.equals(this, obj); + } + + @Override + public int hashCode() { + return HASHING.hashCode(this); + } + + @Override + public String toString() { + return TOSTRING.toString(this); + } + } From 9c19bf084932e4c7bdf767c8f03808ec10d51e4a Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 28 Jan 2026 18:04:35 +0100 Subject: [PATCH 2/6] CAUSEWAY-3959: converts abstract ApplicationUser to an interface --- .../applib/user/dom/ApplicationUser.java | 160 +++++++----------- .../secman/jpa/user/dom/ApplicationUser.java | 54 +++++- 2 files changed, 110 insertions(+), 104 deletions(-) diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java index a7680326adb..094a63cca37 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java @@ -22,11 +22,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.List; import java.util.Objects; import java.util.Set; -import jakarta.inject.Inject; import jakarta.inject.Named; import org.apache.causeway.applib.annotation.Collection; @@ -50,7 +48,6 @@ import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.applib.util.ObjectContracts; import org.apache.causeway.commons.internal.assertions._Assert; -import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.core.config.CausewayConfiguration; @@ -80,15 +77,14 @@ cssClassUiEvent = ApplicationUser.CssClassUiEvent.class, layoutUiEvent = ApplicationUser.LayoutUiEvent.class ) -public abstract class ApplicationUser - implements HasUsername, HasAtPath, Comparable { +public interface ApplicationUser + extends HasUsername, HasAtPath, Comparable { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationUser"; public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA; public static final String TABLE = "ApplicationUser"; - @UtilityClass - public static class Nq { + public static final class Nq { public static final String FIND_BY_USERNAME = LOGICAL_TYPE_NAME + ".findByUsername"; public static final String FIND_BY_EMAIL_ADDRESS = LOGICAL_TYPE_NAME + ".findByEmailAddress"; public static final String FIND = LOGICAL_TYPE_NAME + ".find"; @@ -105,41 +101,25 @@ public static class LayoutUiEvent extends CausewayModuleExtSecmanApplib.LayoutUi public static abstract class PropertyDomainEvent extends CausewayModuleExtSecmanApplib.PropertyDomainEvent {} public static abstract class CollectionDomainEvent extends CausewayModuleExtSecmanApplib.CollectionDomainEvent {} - @Inject private transient ApplicationUserRepository applicationUserRepository; - @Inject private transient ApplicationPermissionRepository applicationPermissionRepository; - @Inject private transient UserService userService; - @Inject private transient PermissionsEvaluationService permissionsEvaluationService; - @Inject private transient CausewayConfiguration config; - - @Programmatic protected ApplicationUserRepository getApplicationUserRepository() { - return applicationUserRepository; - } - - @Programmatic protected ApplicationPermissionRepository getApplicationPermissionRepository() { - return applicationPermissionRepository; - } - - @Programmatic protected UserService getUserService() { - return userService; - } - + ApplicationUserRepository applicationUserRepository(); + ApplicationPermissionRepository applicationPermissionRepository(); + UserService userService(); /** * Optional service, if configured then is used to evaluate permissions within * {@link ApplicationPermissionValueSet#evaluate(ApplicationFeatureId, ApplicationPermissionMode)} * else will fallback to a default implementation. */ - @Programmatic protected PermissionsEvaluationService getPermissionsEvaluationService() { - return permissionsEvaluationService; - } + PermissionsEvaluationService permissionsEvaluationService(); + CausewayConfiguration config(); - @Programmatic protected Secman getSecmanConfig() { - return config.extensions().secman(); + @Programmatic default Secman getSecmanConfig() { + return config().extensions().secman(); } - @ObjectSupport public String title() { + @ObjectSupport default String title() { return getName(); } - @ObjectSupport public String iconName() { + @ObjectSupport default String iconName() { return ApplicationUserStatus.isUnlocked(getStatus()) ? "unlocked" : "locked"; } @@ -161,7 +141,7 @@ class DomainEvent extends PropertyDomainEvent {} } @Name - public String getName() { + default String getName() { final StringBuilder buf = new StringBuilder(); if(getFamilyName() != null) { if(getKnownAs() != null) { @@ -199,8 +179,8 @@ class DomainEvent extends PropertyDomainEvent {} } @Override @Username - public abstract String getUsername(); - public abstract void setUsername(String username); + String getUsername(); + void setUsername(String username); // -- FAMILY NAME @@ -231,8 +211,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @FamilyName - public abstract String getFamilyName(); - public abstract void setFamilyName(String familyName); + String getFamilyName(); + void setFamilyName(String familyName); // -- GIVEN NAME @@ -263,8 +243,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @GivenName - public abstract String getGivenName(); - public abstract void setGivenName(String givenName); + String getGivenName(); + void setGivenName(String givenName); // -- KNOWN AS @@ -295,8 +275,8 @@ class KnownAsDomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @KnownAs - public abstract String getKnownAs(); - public abstract void setKnownAs(String knownAs); + String getKnownAs(); + void setKnownAs(String knownAs); // -- EMAIL ADDRESS @@ -326,8 +306,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @EmailAddress - public abstract String getEmailAddress(); - public abstract void setEmailAddress(String emailAddress); + String getEmailAddress(); + void setEmailAddress(String emailAddress); // -- PHONE NUMBER @@ -356,8 +336,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @PhoneNumber - public abstract String getPhoneNumber(); - public abstract void setPhoneNumber(String phoneNumber); + String getPhoneNumber(); + void setPhoneNumber(String phoneNumber); // -- FAX NUMBER @@ -388,8 +368,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @FaxNumber - public abstract String getFaxNumber(); - public abstract void setFaxNumber(String faxNumber); + String getFaxNumber(); + void setFaxNumber(String faxNumber); // -- LOCALEs @@ -424,8 +404,8 @@ class DomainEvent extends Locale.DomainEvent {} String ALLOWS_NULL = Locale.ALLOWS_NULL; } @Language - public abstract java.util.Locale getLanguage(); - public abstract void setLanguage(java.util.Locale locale); + java.util.Locale getLanguage(); + void setLanguage(java.util.Locale locale); @Property( domainEvent = NumberFormat.DomainEvent.class @@ -439,8 +419,8 @@ class DomainEvent extends Locale.DomainEvent {} String ALLOWS_NULL = Locale.ALLOWS_NULL; } @NumberFormat - public abstract java.util.Locale getNumberFormat(); - public abstract void setNumberFormat(java.util.Locale locale); + java.util.Locale getNumberFormat(); + void setNumberFormat(java.util.Locale locale); @Property( domainEvent = TimeFormat.DomainEvent.class @@ -454,8 +434,8 @@ class DomainEvent extends Locale.DomainEvent {} String ALLOWS_NULL = Locale.ALLOWS_NULL; } @TimeFormat - public abstract java.util.Locale getTimeFormat(); - public abstract void setTimeFormat(java.util.Locale locale); + java.util.Locale getTimeFormat(); + void setTimeFormat(java.util.Locale locale); @Property( domainEvent = AtPath.DomainEvent.class @@ -475,8 +455,8 @@ class DomainEvent extends PropertyDomainEvent {} } @Override @AtPath - public abstract String getAtPath(); - public abstract void setAtPath(String atPath); + String getAtPath(); + void setAtPath(String atPath); // -- ACCOUNT TYPE @@ -542,8 +522,8 @@ class DomainEvent extends PropertyDomainEvent {} @EncryptedPassword public abstract String getEncryptedPassword(); public abstract void setEncryptedPassword(String encryptedPassword); - @MemberSupport public boolean hideEncryptedPassword() { - return !getApplicationUserRepository().isPasswordFeatureEnabled(this); + @MemberSupport default boolean hideEncryptedPassword() { + return !applicationUserRepository().isPasswordFeatureEnabled(this); } // -- HAS PASSWORD @@ -564,11 +544,11 @@ class DomainEvent extends PropertyDomainEvent {} } @HasPassword - public boolean isHasPassword() { + default boolean isHasPassword() { return _Strings.isNotEmpty(getEncryptedPassword()); } - @MemberSupport public boolean hideHasPassword() { - return !getApplicationUserRepository().isPasswordFeatureEnabled(this); + @MemberSupport default boolean hideHasPassword() { + return !applicationUserRepository().isPasswordFeatureEnabled(this); } // ROLES @@ -594,39 +574,29 @@ class Persistence { } @Roles - public abstract Set getRoles(); + Set getRoles(); // -- PERMISSION SET - // short-term caching - private transient ApplicationPermissionValueSet cachedPermissionSet; - - @Programmatic public ApplicationPermissionValueSet getPermissionSet() { - if(cachedPermissionSet != null) { - return cachedPermissionSet; - } - List permissions; - if(userService.isImpersonating()) { - permissions = getApplicationPermissionRepository().findByUserMemento(userService.getUser()); - } else { - permissions = getApplicationPermissionRepository().findByUser(this); - } - return cachedPermissionSet = - new ApplicationPermissionValueSet( - _Lists.map(_Casts.uncheckedCast(permissions), ApplicationPermission.Functions.AS_VALUE), - getPermissionsEvaluationService()); + @Programmatic default ApplicationPermissionValueSet getPermissionSet() { + var permissions = userService().isImpersonating() + ? applicationPermissionRepository().findByUserMemento(userService().getUser()) + : applicationPermissionRepository().findByUser(this); + return new ApplicationPermissionValueSet( + _Lists.map(permissions, ApplicationPermission.Functions.AS_VALUE), + permissionsEvaluationService()); } // -- IS FOR SELF OR RUN AS ADMINISTRATOR - @Programmatic public boolean isForSelf() { + @Programmatic default boolean isForSelf() { var currentUser = currentUser(); var currentUserName = currentUser.name(); var forSelf = Objects.equals(getUsername(), currentUserName); return forSelf; } - @Programmatic public boolean isRunAsAdministrator() { + @Programmatic default boolean isRunAsAdministrator() { var currentUser = currentUser(); var adminRoleName = getAdminRoleName(); // is guarded to not be empty var adminRoleSuffix = ":" + adminRoleName; @@ -641,14 +611,14 @@ class Persistence { return false; } - @Programmatic public boolean isForSelfOrRunAsAdministrator() { + @Programmatic default boolean isForSelfOrRunAsAdministrator() { return isForSelf() || isRunAsAdministrator(); } // -- HELPERS - @Programmatic public boolean isLocalAccount() { + @Programmatic default boolean isLocalAccount() { return getAccountType() == org.apache.causeway.extensions.secman.applib.user.dom.AccountType.LOCAL; } @@ -660,33 +630,17 @@ class Persistence { } @Programmatic private UserMemento currentUser() { - return getUserService().currentUserElseFail(); + return userService().currentUserElseFail(); } // -- equals, hashCode, compareTo, toString - private static final String propertyNames = "username"; - - private static final ObjectContracts.ObjectContract contract = - ObjectContracts.parse(ApplicationUser.class, propertyNames); - - @Override - public int compareTo(final org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser other) { - return contract.compare(this, other); - } - @Override - public boolean equals(final Object obj) { - return contract.equals(this, obj); - } - - @Override - public int hashCode() { - return contract.hashCode(this); - } + static final ObjectContracts.ObjectContract CONTRACT = + ObjectContracts.parse(ApplicationUser.class, "username"); @Override - public String toString() { - return contract.toString(this); + public default int compareTo(final org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser other) { + return CONTRACT.compare(this, other); } } diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java index 4a72676d0a9..2dad347adc5 100644 --- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java +++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.TreeSet; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; @@ -43,15 +44,23 @@ import org.apache.causeway.applib.annotation.BookmarkPolicy; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainObjectLayout; +import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.jaxb.PersistentEntityAdapter; +import org.apache.causeway.applib.services.user.UserService; import org.apache.causeway.commons.internal.base._Casts; +import org.apache.causeway.core.config.CausewayConfiguration; +import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository; +import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionValueSet; +import org.apache.causeway.extensions.secman.applib.permission.spi.PermissionsEvaluationService; import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser.Nq; +import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository; import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserStatus; import org.apache.causeway.extensions.secman.jpa.role.dom.ApplicationRole; import org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; @Entity @Table( @@ -97,7 +106,25 @@ bookmarking = BookmarkPolicy.AS_ROOT ) public class ApplicationUser - extends org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser { + implements org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser { + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient ApplicationUserRepository applicationUserRepository; + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient ApplicationPermissionRepository applicationPermissionRepository; + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient UserService userService; + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient PermissionsEvaluationService permissionsEvaluationService; + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient CausewayConfiguration config; + + // short-term caching + private transient ApplicationPermissionValueSet cachedPermissionSet; @Id @GeneratedValue @@ -193,4 +220,29 @@ public void setAccountType(final org.apache.causeway.extensions.secman.applib.us public Set getRoles() { return _Casts.uncheckedCast(roles); } + + @Programmatic + @Override + public ApplicationPermissionValueSet getPermissionSet() { + if(cachedPermissionSet != null) + return cachedPermissionSet; + return this.cachedPermissionSet = + org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser + .super.getPermissionSet(); + } + + @Override + public boolean equals(final Object obj) { + return CONTRACT.equals(this, obj); + } + + @Override + public int hashCode() { + return CONTRACT.hashCode(this); + } + + @Override + public String toString() { + return CONTRACT.toString(this); + } } From 728c3168cdad1c2a83a64d4098aae3d8dab3eed6 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 29 Jan 2026 07:07:06 +0100 Subject: [PATCH 3/6] CAUSEWAY-3959: fixes permission mapping in prev. commits --- .../secman/applib/role/dom/ApplicationRole.java | 15 +++++++++++++++ .../secman/applib/user/dom/ApplicationUser.java | 8 ++++---- .../secman/jpa/user/dom/ApplicationUser.java | 10 ++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java index 53131cef71b..56f9418357d 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/role/dom/ApplicationRole.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Set; +import jakarta.inject.Named; + import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.applib.annotation.Editing; @@ -46,6 +48,19 @@ /** * @since 2.0 {@index} */ +@Named(ApplicationRole.LOGICAL_TYPE_NAME) // required for permission mapping +/* not allowed on interfaces ... +@DomainObject( + autoCompleteRepository = ApplicationRoleRepository.class, + autoCompleteMethod = "findMatching" + ) +@DomainObjectLayout( + titleUiEvent = ApplicationRole.TitleUiEvent.class, + iconUiEvent = ApplicationRole.IconUiEvent.class, + cssClassUiEvent = ApplicationRole.CssClassUiEvent.class, + layoutUiEvent = ApplicationRole.LayoutUiEvent.class +) +*/ public interface ApplicationRole extends Comparable { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationRole"; diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java index 094a63cca37..31c4b7b7f15 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/user/dom/ApplicationUser.java @@ -29,8 +29,6 @@ import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.CollectionLayout; -import org.apache.causeway.applib.annotation.DomainObject; -import org.apache.causeway.applib.annotation.DomainObjectLayout; import org.apache.causeway.applib.annotation.Editing; import org.apache.causeway.applib.annotation.MemberSupport; import org.apache.causeway.applib.annotation.ObjectSupport; @@ -66,8 +64,9 @@ /** * @since 2.0 {@index} */ -@Named(ApplicationUser.LOGICAL_TYPE_NAME) -@DomainObject( +@Named(ApplicationUser.LOGICAL_TYPE_NAME) // required for permission mapping +/* not allowed on interfaces ... + @DomainObject( autoCompleteRepository = ApplicationUserRepository.class, autoCompleteMethod = "findMatching" ) @@ -77,6 +76,7 @@ cssClassUiEvent = ApplicationUser.CssClassUiEvent.class, layoutUiEvent = ApplicationUser.LayoutUiEvent.class ) + */ public interface ApplicationUser extends HasUsername, HasAtPath, Comparable { diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java index 2dad347adc5..315a1cac093 100644 --- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java +++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/user/dom/ApplicationUser.java @@ -100,11 +100,13 @@ @Named(ApplicationUser.LOGICAL_TYPE_NAME) @DomainObject( autoCompleteRepository = ApplicationUserRepository.class, - autoCompleteMethod = "findMatching" - ) + autoCompleteMethod = "findMatching") @DomainObjectLayout( - bookmarking = BookmarkPolicy.AS_ROOT - ) + bookmarking = BookmarkPolicy.AS_ROOT, + titleUiEvent = ApplicationUser.TitleUiEvent.class, + iconUiEvent = ApplicationUser.IconUiEvent.class, + cssClassUiEvent = ApplicationUser.CssClassUiEvent.class, + layoutUiEvent = ApplicationUser.LayoutUiEvent.class) public class ApplicationUser implements org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser { From 07464ca632823df343f238a434b950dfcf1eeb14 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 29 Jan 2026 07:21:51 +0100 Subject: [PATCH 4/6] CAUSEWAY-3959: converts abstract ApplicationPermission to an interface --- .../permission/dom/ApplicationPermission.java | 67 +++++++------------ .../permission/dom/ApplicationPermission.java | 30 ++++++++- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/permission/dom/ApplicationPermission.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/permission/dom/ApplicationPermission.java index f427b90139d..a78b7318f43 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/permission/dom/ApplicationPermission.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/permission/dom/ApplicationPermission.java @@ -27,11 +27,8 @@ import java.util.Optional; import java.util.function.Function; -import jakarta.inject.Inject; import jakarta.inject.Named; -import org.apache.causeway.applib.annotation.DomainObject; -import org.apache.causeway.applib.annotation.DomainObjectLayout; import org.apache.causeway.applib.annotation.Editing; import org.apache.causeway.applib.annotation.ObjectSupport; import org.apache.causeway.applib.annotation.Optionality; @@ -83,7 +80,8 @@ * * @since 2.0 {@index} */ -@Named(ApplicationPermission.LOGICAL_TYPE_NAME) +@Named(ApplicationPermission.LOGICAL_TYPE_NAME) // required for permission mapping +/* not allowed on interfaces ... @DomainObject @DomainObjectLayout( titleUiEvent = ApplicationPermission.TitleUiEvent.class, @@ -91,14 +89,14 @@ cssClassUiEvent = ApplicationPermission.CssClassUiEvent.class, layoutUiEvent = ApplicationPermission.LayoutUiEvent.class ) -public abstract class ApplicationPermission implements Comparable { +*/ +public interface ApplicationPermission extends Comparable { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationPermission"; public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA; public static final String TABLE = "ApplicationPermission"; - @UtilityClass - public static class Nq { + public final static class Nq { public static final String FIND_BY_FEATURE = LOGICAL_TYPE_NAME + ".findByFeature"; public static final String FIND_BY_ROLE = LOGICAL_TYPE_NAME + ".findByRole"; public static final String FIND_BY_ROLE_RULE_FEATURE = LOGICAL_TYPE_NAME + ".findByRoleAndRuleAndFeature"; @@ -117,11 +115,11 @@ public static class LayoutUiEvent extends CausewayModuleExtSecmanApplib.LayoutUi public static abstract class PropertyDomainEvent extends CausewayModuleExtSecmanApplib.PropertyDomainEvent {} public static abstract class CollectionDomainEvent extends CausewayModuleExtSecmanApplib.CollectionDomainEvent {} - @Inject transient ApplicationFeatureRepository featureRepository; + ApplicationFeatureRepository featureRepository(); // -- MODEL - @ObjectSupport public String title() { + @ObjectSupport default String title() { var buf = new StringBuilder(); buf.append(getRole().getName()).append(":") // admin: .append(" ").append(getRule().toString()) // Allow|Veto @@ -180,8 +178,8 @@ class DomainEvent extends PropertyDomainEvent {} } @Role - public abstract ApplicationRole getRole(); - public abstract void setRole(ApplicationRole applicationRole); + ApplicationRole getRole(); + void setRole(ApplicationRole applicationRole); // -- RULE @@ -205,8 +203,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "false"; } @Rule - public abstract ApplicationPermissionRule getRule(); - public abstract void setRule(ApplicationPermissionRule rule); + ApplicationPermissionRule getRule(); + void setRule(ApplicationPermissionRule rule); // -- MODE @@ -230,8 +228,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "false"; } @Mode - public abstract ApplicationPermissionMode getMode(); - public abstract void setMode(ApplicationPermissionMode mode); + ApplicationPermissionMode getMode(); + void setMode(ApplicationPermissionMode mode); // -- SORT @@ -254,7 +252,7 @@ class DomainEvent extends PropertyDomainEvent {} int TYPICAL_LENGTH = 7; // ApplicationFeatureType.PACKAGE is longest } @Sort - public String getSort() { + default String getSort() { final Enum e = getFeatureSort() != ApplicationFeatureSort.MEMBER ? getFeatureSort() : getMemberSort().orElse(null); @@ -282,8 +280,8 @@ public String getSort() { * @see #getFeatureFqn() */ @Programmatic - public abstract ApplicationFeatureSort getFeatureSort(); - public abstract void setFeatureSort(ApplicationFeatureSort featureSort); + ApplicationFeatureSort getFeatureSort(); + void setFeatureSort(ApplicationFeatureSort featureSort); // -- FQN @@ -314,13 +312,13 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "false"; } @FeatureFqn - public abstract String getFeatureFqn(); - public abstract void setFeatureFqn(String featureFqn); + String getFeatureFqn(); + void setFeatureFqn(String featureFqn); // -- FIND FEATURE - @Programmatic public ApplicationFeature findFeature(final ApplicationFeatureId featureId) { - return featureRepository.findFeature(featureId); + @Programmatic default ApplicationFeature findFeature(final ApplicationFeatureId featureId) { + return featureRepository().findFeature(featureId); } @Programmatic private Optional getMemberSort() { @@ -333,16 +331,16 @@ class DomainEvent extends PropertyDomainEvent {} .map(this::findFeature); } - // -- HELPER + // -- UTIL - @Programmatic Optional asFeatureId() { + @Programmatic default Optional asFeatureId() { return Optional.ofNullable(getFeatureSort()) .map(featureSort -> ApplicationFeatureId.newFeature(featureSort, getFeatureFqn())); } // -- CONTRACT - private static final ObjectContracts.ObjectContract contract = + static final ObjectContracts.ObjectContract CONTRACT = ObjectContracts.contract(ApplicationPermission.class) .thenUse("role", ApplicationPermission::getRole) .thenUse("featureSort", ApplicationPermission::getFeatureSort) @@ -350,23 +348,8 @@ class DomainEvent extends PropertyDomainEvent {} .thenUse("mode", ApplicationPermission::getMode); @Override - public int compareTo(final org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission other) { - return contract.compare(this, other); - } - - @Override - public boolean equals(final Object other) { - return contract.equals(this, other); - } - - @Override - public int hashCode() { - return contract.hashCode(this); - } - - @Override - public String toString() { - return contract.toString(this); + default int compareTo(final org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission other) { + return CONTRACT.compare(this, other); } public static class DefaultComparator implements Comparator { diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/permission/dom/ApplicationPermission.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/permission/dom/ApplicationPermission.java index 4a0f61a3993..afd49ac35c3 100644 --- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/permission/dom/ApplicationPermission.java +++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/permission/dom/ApplicationPermission.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.extensions.secman.jpa.permission.dom; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -38,7 +39,9 @@ import org.apache.causeway.applib.annotation.BookmarkPolicy; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainObjectLayout; +import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.jaxb.PersistentEntityAdapter; +import org.apache.causeway.applib.services.appfeat.ApplicationFeatureRepository; import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort; import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission.Nq; @@ -49,6 +52,7 @@ import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; @Entity @Table( @@ -103,10 +107,17 @@ @Named(ApplicationPermission.LOGICAL_TYPE_NAME) @DomainObject @DomainObjectLayout( - bookmarking = BookmarkPolicy.AS_CHILD + bookmarking = BookmarkPolicy.AS_CHILD, + titleUiEvent = ApplicationPermission.TitleUiEvent.class, + iconUiEvent = ApplicationPermission.IconUiEvent.class, + cssClassUiEvent = ApplicationPermission.CssClassUiEvent.class, + layoutUiEvent = ApplicationPermission.LayoutUiEvent.class ) public class ApplicationPermission - extends org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission { + implements org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission { + + @Inject @Programmatic @Getter(onMethod_ = {@Override}) @Accessors(fluent = true) + private transient ApplicationFeatureRepository featureRepository; @Id @GeneratedValue @@ -146,4 +157,19 @@ public void setRole(final ApplicationRole applicationRole) { @Getter @Setter private String featureFqn; + @Override + public boolean equals(final Object other) { + return CONTRACT.equals(this, other); + } + + @Override + public int hashCode() { + return CONTRACT.hashCode(this); + } + + @Override + public String toString() { + return CONTRACT.toString(this); + } + } From a9a1e899be34d315fabab823d11febecae002a16 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 29 Jan 2026 07:31:15 +0100 Subject: [PATCH 5/6] CAUSEWAY-3959: converts abstract ApplicationTenancy to an interface --- .../tenancy/dom/ApplicationTenancy.java | 53 +++++++------------ .../jpa/tenancy/dom/ApplicationTenancy.java | 29 +++++++--- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java index f473a31e194..71f971c0346 100644 --- a/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java +++ b/extensions/security/secman/applib/src/main/java/org/apache/causeway/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java @@ -28,8 +28,6 @@ import jakarta.inject.Named; import org.apache.causeway.applib.annotation.CollectionLayout; -import org.apache.causeway.applib.annotation.DomainObject; -import org.apache.causeway.applib.annotation.DomainObjectLayout; import org.apache.causeway.applib.annotation.Editing; import org.apache.causeway.applib.annotation.ObjectSupport; import org.apache.causeway.applib.annotation.Optionality; @@ -50,7 +48,8 @@ /** * @since 2.0 {@index} */ -@Named(ApplicationTenancy.LOGICAL_TYPE_NAME) +@Named(ApplicationTenancy.LOGICAL_TYPE_NAME) // required for permission mapping +/* not allowed on interfaces ... @DomainObject( autoCompleteRepository = ApplicationTenancyRepository.class, autoCompleteMethod = "findMatching" @@ -61,7 +60,8 @@ cssClassUiEvent = ApplicationTenancy.CssClassUiEvent.class, layoutUiEvent = ApplicationTenancy.LayoutUiEvent.class ) -public abstract class ApplicationTenancy implements Comparable { +*/ +public interface ApplicationTenancy extends Comparable { public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationTenancy"; public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA; @@ -87,7 +87,7 @@ public static abstract class ActionDomainEvent extends CausewayModuleExtSecmanAp // -- MODEL - @ObjectSupport public String title() { + @ObjectSupport default String title() { return getName(); } @@ -120,8 +120,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "false"; } @Name - public abstract String getName(); - public abstract void setName(String name); + String getName(); + void setName(String name); // -- PATH @@ -142,8 +142,8 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "false"; } @Path - public abstract String getPath(); - public abstract void setPath(String path); + String getPath(); + void setPath(String path); // -- PARENT @@ -165,11 +165,11 @@ class DomainEvent extends PropertyDomainEvent {} String ALLOWS_NULL = "true"; } @Parent - public abstract ApplicationTenancy getParent(); - public abstract void setParent(ApplicationTenancy parent); + ApplicationTenancy getParent(); + void setParent(ApplicationTenancy parent); @Programmatic - public boolean isRoot() { + default boolean isRoot() { return getParent()==null; } @@ -188,41 +188,26 @@ class DomainEvent extends CollectionDomainEvent {} String MAPPED_BY = "parent"; } @Children - public abstract Collection getChildren(); + Collection getChildren(); // -- CONTRACT - private static final Equality equality = + static final Equality EQUALITY = ObjectContracts.checkEquals(ApplicationTenancy::getPath); - private static final Hashing hashing = + static final Hashing HASHING = ObjectContracts.hashing(ApplicationTenancy::getPath); - private static final ToString toString = + static final ToString TOSTRING = ObjectContracts.toString("path", ApplicationTenancy::getPath) .thenToString("name", ApplicationTenancy::getName); - private static final Comparator comparator = + static final Comparator COMPARATOR = Comparator.comparing(ApplicationTenancy::getPath); @Override - public int compareTo(final org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy other) { - return comparator.compare(this, other); - } - - @Override - public boolean equals(final Object other) { - return equality.equals(this, other); - } - - @Override - public int hashCode() { - return hashing.hashCode(this); - } - - @Override - public String toString() { - return toString.toString(this); + default int compareTo(final org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy other) { + return COMPARATOR.compare(this, other); } } diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java index bf8b104c029..9a5014813db 100644 --- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java +++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/causeway/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java @@ -77,13 +77,15 @@ @Named(ApplicationTenancy.LOGICAL_TYPE_NAME) @DomainObject( autoCompleteRepository = ApplicationTenancyRepository.class, - autoCompleteMethod = "findMatching" - ) + autoCompleteMethod = "findMatching") @DomainObjectLayout( - bookmarking = BookmarkPolicy.AS_ROOT - ) + bookmarking = BookmarkPolicy.AS_ROOT, + titleUiEvent = ApplicationTenancy.TitleUiEvent.class, + iconUiEvent = ApplicationTenancy.IconUiEvent.class, + cssClassUiEvent = ApplicationTenancy.CssClassUiEvent.class, + layoutUiEvent = ApplicationTenancy.LayoutUiEvent.class) public class ApplicationTenancy - extends org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy { + implements org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy { @Version private Long version; @@ -105,7 +107,7 @@ public class ApplicationTenancy @Getter private ApplicationTenancy parent; @Override - public void setParent(org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy parent) { + public void setParent(final org.apache.causeway.extensions.secman.applib.tenancy.dom.ApplicationTenancy parent) { this.parent = _Casts.uncheckedCast(parent); } @@ -125,4 +127,19 @@ public void removeFromChildren(final org.apache.causeway.extensions.secman.appli getChildren().remove(applicationTenancy); } + @Override + public boolean equals(final Object other) { + return EQUALITY.equals(this, other); + } + + @Override + public int hashCode() { + return HASHING.hashCode(this); + } + + @Override + public String toString() { + return TOSTRING.toString(this); + } + } From b012d3ae2078f0b3c32ece887ba87bfdd2c56f5e Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 29 Jan 2026 13:51:06 +0100 Subject: [PATCH 6/6] CAUSEWAY-3959: adds weaving profile --- parent/pom.xml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/parent/pom.xml b/parent/pom.xml index 6a40340bfa7..f3f71037181 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -64,6 +64,7 @@ 3.14.1 + 3.0.2 3.3.1 3.4.0 3.5.4 @@ -162,6 +163,19 @@ + + com.ethlo.persistence.tools + eclipselink-maven-plugin + ${ethlo-eclipselink-maven-plugin.version} + + + process-classes + + weave + + + + org.jvnet.jaxb2.maven2 maven-jaxb2-plugin @@ -1046,6 +1060,24 @@ + + jpa-weave + + + jpa-weave + true + + + + + + com.ethlo.persistence.tools + eclipselink-maven-plugin + + + + + src