You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2022/05/23 18:03:07 UTC

[isis] branch ISIS-3062 created (now 9487682bc0)

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a change to branch ISIS-3062
in repository https://gitbox.apache.org/repos/asf/isis.git


      at 9487682bc0 ISIS-3062: introduces Nq utility class, also for secman

This branch includes the following new commits:

     new 75aecc0714 ISIS-3062: adds in JDO impl of SessionLogEntry etc
     new 9487682bc0 ISIS-3062: introduces Nq utility class, also for secman

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[isis] 01/02: ISIS-3062: adds in JDO impl of SessionLogEntry etc

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-3062
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 75aecc071411a9917732c83c435183642d635e05
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon May 23 18:50:40 2022 +0100

    ISIS-3062: adds in JDO impl of SessionLogEntry etc
---
 ...nLoggingService.java => SessionLogService.java} |   2 +-
 .../session/SessionLoggingServiceLogging.java      |   2 +-
 extensions/pom.xml                                 |   7 +
 .../{audit-trail => audittrail}/adoc/antora.yml    |   0
 .../adoc/modules/audit-trail/nav.adoc              |   0
 .../adoc/modules/audit-trail/pages/about.adoc      |   0
 .../modules/audit-trail/partials/module-nav.adoc   |   0
 .../impl/src/main/java/META-INF/persistence.xml    |  23 +
 .../org/isisaddons/module/audit/AuditModule.java   |  30 ++
 .../isisaddons/module/audit/dom/AuditEntry.java    | 395 +++++++++++++++++
 .../audit/dom/AuditEntry.layout.fallback.xml       |  60 +++
 .../org/isisaddons/module/audit/dom/AuditEntry.png | Bin 0 -> 477 bytes
 .../module/audit/dom/AuditerServiceUsingJdo.java   |  56 +++
 .../module/audit/dom/AuditingServiceMenu.java      |  84 ++++
 .../audit/dom/AuditingServiceRepository.java       | 163 +++++++
 ...HasTransactionId_auditEntriesInTransaction.java |  51 +++
 .../audit/dom/Object_recentAuditEntries.java       |  96 +++++
 ...itledEnumContractForIncodeModuleTest_title.java |  16 +
 ...rableContractForIncodeModuleTest_compareTo.java |  18 +
 ...codeModuleTest_hasJdoUniqueIndexAnnotation.java |  13 +
 ...rableContractForIncodeModuleTest_compareTo.java |  18 +
 ...codeModuleTest_hasJdoUniqueIndexAnnotation.java |  13 +
 ...rableContractForIncodeModuleTest_compareTo.java |  19 +
 ...codeModuleTest_hasJdoUniqueIndexAnnotation.java |  13 +
 ...rableContractForIncodeModuleTest_compareTo.java |  18 +
 ...codeModuleTest_hasJdoUniqueIndexAnnotation.java |  13 +
 ...rableContractForIncodeModuleTest_compareTo.java |  18 +
 ...codeModuleTest_hasJdoUniqueIndexAnnotation.java |  13 +
 extensions/security/audittrail/pom.xml             | 173 ++++++++
 .../secman/applib/IsisModuleExtSecmanApplib.java   |   1 +
 .../permission/dom/ApplicationPermission.java      |   2 +
 .../secman/applib/role/dom/ApplicationRole.java    |   2 +
 .../applib/tenancy/dom/ApplicationTenancy.java     |   2 +
 .../secman/applib/user/dom/ApplicationUser.java    |   3 +
 .../jdo/permission/dom/ApplicationPermission.java  |   4 +-
 .../secman/jdo/role/dom/ApplicationRole.java       |   5 +-
 .../secman/jdo/tenancy/dom/ApplicationTenancy.java |   4 +-
 .../secman/jdo/user/dom/ApplicationUser.java       |   5 +-
 .../{session-log => sessionlog}/adoc/antora.yml    |   0
 .../adoc/modules/session-log/nav.adoc              |   0
 .../adoc/modules/session-log/pages/about.adoc      |   0
 .../modules/session-log/partials/module-nav.adoc   |   0
 extensions/security/sessionlog/applib/pom.xml      |  57 +++
 .../applib/IsisModuleExtSessionLogApplib.java      |  25 ++
 .../security/sessionlog/persistence-jdo/pom.xml    |  62 +++
 .../src/main/java/META-INF/persistence.xml         |  23 +
 .../jdo/IsisModuleExtSessionLogPersistenceJdo.java |  29 ++
 .../isis/sessionlog/jdo/app/SessionLogMenu.java    |  82 ++++
 .../HasUsername_recentSessionsForUser.java         |  50 +++
 .../sessionlog/jdo/dom/SessionLogEntry-expired.png | Bin 0 -> 630 bytes
 .../sessionlog/jdo/dom/SessionLogEntry-login.png   | Bin 0 -> 468 bytes
 .../sessionlog/jdo/dom/SessionLogEntry-logout.png  | Bin 0 -> 457 bytes
 .../isis/sessionlog/jdo/dom/SessionLogEntry.java   | 467 +++++++++++++++++++++
 .../jdo/dom/SessionLogEntry.layout.fallback.xml    |  58 +++
 .../jdo/dom/SessionLogEntryRepository.java         | 163 +++++++
 .../jdo/spiimpl/SessionLoggingServiceDefault.java  |  55 +++
 extensions/security/sessionlog/pom.xml             |  87 ++++
 .../AuthenticatedWebSessionForIsis.java            |  18 +-
 ...uthenticatedWebSessionForIsis_Authenticate.java |   4 +-
 .../AuthenticatedWebSessionForIsis_SignIn.java     |   4 +-
 ...uthenticatedWebSessionForIsis_TestAbstract.java |   4 +-
 61 files changed, 2504 insertions(+), 26 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingService.java b/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLogService.java
similarity index 97%
rename from api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingService.java
rename to api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLogService.java
index 1fdc88cf9b..a450716b52 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingService.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLogService.java
@@ -30,7 +30,7 @@ import java.util.Date;
  *
  * @since 1.x {@index}
  */
-public interface SessionLoggingService {
+public interface SessionLogService {
 
     enum Type {
         LOGIN,
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingServiceLogging.java b/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingServiceLogging.java
index eef5a1a98c..cbfa504eb8 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingServiceLogging.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/session/SessionLoggingServiceLogging.java
@@ -38,7 +38,7 @@ import lombok.extern.log4j.Log4j2;
 @Priority(PriorityPrecedence.LATE)
 @Qualifier("logging")
 @Log4j2
-public class SessionLoggingServiceLogging implements SessionLoggingService {
+public class SessionLoggingServiceLogging implements SessionLogService {
 
     @Override
     public void log(
diff --git a/extensions/pom.xml b/extensions/pom.xml
index a20fd6a0dc..1ff523d815 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -228,6 +228,12 @@
 				<version>2.0.0-SNAPSHOT</version>
 			</dependency>
 
+			<dependency>
+				<groupId>org.apache.isis.extensions</groupId>
+				<artifactId>isis-extensions-sessionlog-impl</artifactId>
+				<version>2.0.0-SNAPSHOT</version>
+			</dependency>
+
 			<dependency>
 				<groupId>org.apache.isis.extensions</groupId>
 				<artifactId>isis-extensions-cors-impl</artifactId>
@@ -265,6 +271,7 @@
 		<module>security/secman</module>
 		<module>security/shiro-realm-ldap</module>
 		<module>security/spring-oauth2</module>
+		<module>security/sessionlog</module>
 
 		<module>vro/cors</module>
 
diff --git a/extensions/security/audit-trail/adoc/antora.yml b/extensions/security/audittrail/adoc/antora.yml
similarity index 100%
rename from extensions/security/audit-trail/adoc/antora.yml
rename to extensions/security/audittrail/adoc/antora.yml
diff --git a/extensions/security/audit-trail/adoc/modules/audit-trail/nav.adoc b/extensions/security/audittrail/adoc/modules/audit-trail/nav.adoc
similarity index 100%
rename from extensions/security/audit-trail/adoc/modules/audit-trail/nav.adoc
rename to extensions/security/audittrail/adoc/modules/audit-trail/nav.adoc
diff --git a/extensions/security/audit-trail/adoc/modules/audit-trail/pages/about.adoc b/extensions/security/audittrail/adoc/modules/audit-trail/pages/about.adoc
similarity index 100%
rename from extensions/security/audit-trail/adoc/modules/audit-trail/pages/about.adoc
rename to extensions/security/audittrail/adoc/modules/audit-trail/pages/about.adoc
diff --git a/extensions/security/audit-trail/adoc/modules/audit-trail/partials/module-nav.adoc b/extensions/security/audittrail/adoc/modules/audit-trail/partials/module-nav.adoc
similarity index 100%
rename from extensions/security/audit-trail/adoc/modules/audit-trail/partials/module-nav.adoc
rename to extensions/security/audittrail/adoc/modules/audit-trail/partials/module-nav.adoc
diff --git a/extensions/security/audittrail/impl/src/main/java/META-INF/persistence.xml b/extensions/security/audittrail/impl/src/main/java/META-INF/persistence.xml
new file mode 100644
index 0000000000..c2abfdaf54
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/META-INF/persistence.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Copyright 2013~2014 Dan Haywood
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      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.
+-->
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
+
+    <persistence-unit name="org-isisaddons-module-audit-dom">
+    </persistence-unit>
+</persistence>
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/AuditModule.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/AuditModule.java
new file mode 100644
index 0000000000..313e32e3ac
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/AuditModule.java
@@ -0,0 +1,30 @@
+package org.isisaddons.module.audit;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.isis.applib.ModuleAbstract;
+import org.apache.isis.applib.fixturescripts.FixtureScript;
+import org.apache.isis.applib.fixturescripts.teardown.TeardownFixtureAbstract;
+
+import org.isisaddons.module.audit.dom.AuditEntry;
+
+@XmlRootElement(name = "module")
+public class AuditModule extends ModuleAbstract {
+
+    public abstract static class ActionDomainEvent<S> extends org.apache.isis.applib.services.eventbus.ActionDomainEvent<S> { }
+
+    public abstract static class CollectionDomainEvent<S,T> extends org.apache.isis.applib.services.eventbus.CollectionDomainEvent<S,T> { }
+
+    public abstract static class PropertyDomainEvent<S,T> extends org.apache.isis.applib.services.eventbus.PropertyDomainEvent<S,T> { }
+
+    @Override
+    public FixtureScript getTeardownFixture() {
+        return new TeardownFixtureAbstract() {
+            @Override
+            protected void execute(final FixtureScript.ExecutionContext executionContext) {
+                deleteFrom(AuditEntry.class);
+            }
+        };
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.java
new file mode 100644
index 0000000000..e2e1996f3d
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.java
@@ -0,0 +1,395 @@
+package org.isisaddons.module.audit.dom;
+
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.UUID;
+
+import javax.jdo.annotations.IdentityType;
+
+import org.apache.isis.applib.Identifier;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.PropertyLayout;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.services.HasTransactionId;
+import org.apache.isis.applib.services.HasUsername;
+import org.apache.isis.applib.util.ObjectContracts;
+import org.apache.isis.applib.util.TitleBuffer;
+import org.apache.isis.objectstore.jdo.applib.service.DomainChangeJdoAbstract;
+import org.apache.isis.objectstore.jdo.applib.service.JdoColumnLength;
+import org.apache.isis.objectstore.jdo.applib.service.Util;
+
+import org.isisaddons.module.audit.AuditModule;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@javax.jdo.annotations.PersistenceCapable(
+        identityType=IdentityType.DATASTORE,
+        schema = "isisaudit",
+        table="AuditEntry")
+@javax.jdo.annotations.DatastoreIdentity(
+        strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
+        column="id")
+@javax.jdo.annotations.Queries( {
+    @javax.jdo.annotations.Query(
+            name="findFirstByTarget", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "ORDER BY timestamp ASC "
+                    + "RANGE 0,2"),
+    @javax.jdo.annotations.Query(
+            name="findRecentByTarget", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "ORDER BY timestamp DESC "
+                    + "RANGE 0,100"),
+    @javax.jdo.annotations.Query(
+            name="findRecentByTargetAndPropertyId", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "&&    propertyId == :propertyId "
+                    + "ORDER BY timestamp DESC "
+                    + "RANGE 0,30"),
+    @javax.jdo.annotations.Query(
+            name="findByTransactionId", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE transactionId == :transactionId"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampBetween", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "&&    timestamp >= :from "
+                    + "&&    timestamp <= :to "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampAfter", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "&&    timestamp >= :from "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampBefore", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "&&    timestamp <= :to "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTarget", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE targetStr == :targetStr "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampBetween", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE timestamp >= :from "
+                    + "&&    timestamp <= :to "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampAfter", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE timestamp >= :from "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampBefore", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "WHERE timestamp <= :to "
+                    + "ORDER BY timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="find", language="JDOQL",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.audit.dom.AuditEntry "
+                    + "ORDER BY timestamp DESC")
+})
+//@Indices({
+//    @Index(name="AuditEntry_ak", unique="true",
+//            columns={
+//                @javax.jdo.annotations.Column(name="transactionId"),
+//                @javax.jdo.annotations.Column(name="sequence"),
+//                @javax.jdo.annotations.Column(name="target"),
+//                @javax.jdo.annotations.Column(name="propertyId")
+//                }),
+//    @Index(name="AuditEntry_target_ts_IDX", unique="false",
+//            members={ "targetStr", "timestamp" }),
+//})
+@DomainObject(
+        editing = Editing.DISABLED,
+        objectType = "isisaudit.AuditEntry"
+)
+public class AuditEntry extends DomainChangeJdoAbstract implements HasTransactionId, HasUsername {
+
+    //region > domain events
+    public static abstract class PropertyDomainEvent<T> extends AuditModule.PropertyDomainEvent<AuditEntry, T> {
+    }
+    //endregion
+
+    public AuditEntry() {
+        super(ChangeType.AUDIT_ENTRY);
+    }
+
+    //region > title
+
+    public String title() {
+
+        // nb: not thread-safe
+        // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
+        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+        final TitleBuffer buf = new TitleBuffer();
+        buf.append(format.format(getTimestamp()));
+        buf.append(" ").append(getMemberIdentifier());
+        return buf.toString();
+    }
+
+    //endregion
+
+    //region > user (property)
+    public static class UserDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="false", length=JdoColumnLength.USER_NAME)
+    @Property(
+            domainEvent = UserDomainEvent.class
+    )
+    @PropertyLayout(
+            hidden = Where.PARENTED_TABLES
+    )
+    @Getter @Setter
+    private String user;
+
+    @Programmatic
+    public String getUsername() {
+        return getUser();
+    }
+    //endregion
+
+    //region > timestamp (property)
+
+    public static class TimestampDomainEvent extends PropertyDomainEvent<Timestamp> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="false")
+    @Property(
+            domainEvent = TimestampDomainEvent.class
+    )
+    @PropertyLayout(
+            hidden = Where.PARENTED_TABLES
+    )
+    @Getter @Setter
+    private Timestamp timestamp;
+
+    //endregion
+
+    //region > transactionId (property)
+
+    public static class TransactionIdDomainEvent extends PropertyDomainEvent<UUID> {
+    }
+
+    /**
+     * The unique identifier (a GUID) of the interaction in which this audit entry was persisted.
+     *
+     * <p>
+     * The combination of (({@link #getTransactionId() transactionId}, {@link #getSequence() sequence}) makes up the
+     * unique transaction identifier.
+     * </p>
+     *
+     * <p>
+     * The combination of ({@link #getTransactionId() transactionId}, {@link #getSequence()}, {@link #getTargetStr() target}, {@link #getPropertyId() propertyId} ) makes up the
+     * alternative key.
+     * </p>
+     */
+    @javax.jdo.annotations.Column(allowsNull="false", length=JdoColumnLength.TRANSACTION_ID)
+    @Property(
+            domainEvent = TransactionIdDomainEvent.class,
+            editing = Editing.DISABLED
+    )
+    @PropertyLayout(
+            hidden=Where.PARENTED_TABLES,
+            typicalLength = 36
+    )
+    @Getter @Setter
+    private UUID transactionId;
+
+    //endregion
+
+    //region > sequence (property)
+
+    public static class SequenceDomainEvent extends PropertyDomainEvent<UUID> {
+    }
+
+    /**
+     * The 0-based sequence number of the transaction in which this audit entry was persisted.
+     *
+     * <p>
+     * The combination of (({@link #getTransactionId() transactionId}, {@link #getSequence() sequence}) makes up the
+     * unique transaction identifier.
+     * </p>
+     *
+     * <p>
+     * The combination of (({@link #getTransactionId() transactionId}, {@link #getSequence() sequence}, {@link #getTargetStr() target}, {@link #getPropertyId() propertyId} ) makes up the
+     * alternative key.
+     * </p>
+     */
+    @javax.jdo.annotations.Column(allowsNull="false")
+    @Property(
+            domainEvent = SequenceDomainEvent.class,
+            editing = Editing.DISABLED
+    )
+    @PropertyLayout(
+            hidden=Where.PARENTED_TABLES
+    )
+    @Getter @Setter
+    private int sequence;
+
+    //endregion
+
+    //region > targetClass (property)
+
+    public static class TargetClassDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.TARGET_CLASS)
+    @Property(
+            domainEvent = TargetClassDomainEvent.class
+    )
+    @PropertyLayout(
+            named = "Class",
+            typicalLength = 30
+    )
+    @Getter
+    private String targetClass;
+
+    public void setTargetClass(final String targetClass) {
+        this.targetClass = Util.abbreviated(targetClass, JdoColumnLength.TARGET_CLASS);
+    }
+
+    //endregion
+
+    //region > targetStr (property)
+
+    public static class TargetStrDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.BOOKMARK, name="target")
+    @Property(
+            domainEvent = TargetStrDomainEvent.class
+    )
+    @PropertyLayout(
+            named = "Object"
+    )
+    @Getter @Setter
+    private String targetStr;
+    //endregion
+
+    //region > memberIdentifier (property)
+
+    public static class MemberIdentifierDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    /**
+     * This is the fully-qualified class and property Id, as per
+     * {@link Identifier#toClassAndNameIdentityString()}.
+     */
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.MEMBER_IDENTIFIER)
+    @Property(
+            domainEvent = MemberIdentifierDomainEvent.class
+    )
+    @PropertyLayout(
+            typicalLength = 60,
+            hidden = Where.ALL_TABLES
+    )
+    @Getter
+    private String memberIdentifier;
+
+    public void setMemberIdentifier(final String memberIdentifier) {
+        this.memberIdentifier = Util.abbreviated(memberIdentifier, JdoColumnLength.MEMBER_IDENTIFIER);
+    }
+    //endregion
+
+    //region > propertyId (property)
+
+    public static class PropertyIdDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    /**
+     * This is the property name (without the class).
+     */
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.AuditEntry.PROPERTY_ID)
+    @Property(
+            domainEvent = PropertyIdDomainEvent.class
+    )
+    @PropertyLayout(
+            hidden = Where.NOWHERE
+    )
+    @Getter
+    private String propertyId;
+
+    public void setPropertyId(final String propertyId) {
+        this.propertyId = Util.abbreviated(propertyId, JdoColumnLength.AuditEntry.PROPERTY_ID);
+    }
+
+    //endregion
+
+    //region > preValue (property)
+
+    public static class PreValueDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.AuditEntry.PROPERTY_VALUE)
+    @Property(
+            domainEvent = PreValueDomainEvent.class
+    )
+    @PropertyLayout(
+            hidden = Where.NOWHERE
+    )
+    @Getter
+    private String preValue;
+
+    public void setPreValue(final String preValue) {
+        this.preValue = Util.abbreviated(preValue, JdoColumnLength.AuditEntry.PROPERTY_VALUE);
+    }
+    //endregion
+
+    //region > postValue (property)
+
+    public static class PostValueDomainEvent extends PropertyDomainEvent<String> {
+    }
+
+    @javax.jdo.annotations.Column(allowsNull="true", length=JdoColumnLength.AuditEntry.PROPERTY_VALUE)
+    @Property(
+            domainEvent = PostValueDomainEvent.class
+    )
+    @PropertyLayout(
+            hidden = Where.NOWHERE
+    )
+    @Getter
+    private String postValue;
+
+    public void setPostValue(final String postValue) {
+        this.postValue = Util.abbreviated(postValue, JdoColumnLength.AuditEntry.PROPERTY_VALUE);
+    }
+
+    //endregion
+
+    //region > helpers: toString
+
+    @Override
+    public String toString() {
+        return ObjectContracts.toString(this, "timestamp,user,targetStr,memberIdentifier");
+    }
+    //endregion
+
+}
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.layout.fallback.xml b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.layout.fallback.xml
new file mode 100644
index 0000000000..9e19319063
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.layout.fallback.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<bs3:grid xsi:schemaLocation="http://isis.apache.org/applib/layout/component http://isis.apache.org/applib/layout/component/component.xsd http://isis.apache.org/applib/layout/links http://isis.apache.org/applib/layout/links/links.xsd http://isis.apache.org/applib/layout/grid/bootstrap3 http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="http://isis.apache.org/applib/layout/component" xmlns:lnk="h [...]
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <cpt:domainObject/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="4">
+            <cpt:fieldSet name="Identifiers" id="identifiers" unreferencedProperties="true">
+                <cpt:action id="recentAuditEntries" position="PANEL_DROPDOWN"/>
+                <cpt:action id="findChangesByDate" position="PANEL_DROPDOWN"/>
+                <cpt:action id="recentChanges" position="PANEL_DROPDOWN"/>
+                <cpt:action id="clearHints" position="PANEL_DROPDOWN"/>
+                <cpt:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
+                <cpt:action id="downloadJdoMetadata" position="PANEL_DROPDOWN"/>
+                <cpt:action id="rebuildMetamodel" position="PANEL_DROPDOWN"/>
+                <cpt:property id="type"/>
+                <cpt:property id="transactionId"/>
+                <cpt:property id="memberIdentifier"/>
+                <cpt:property id="user"/>
+                <cpt:property id="timestamp"/>
+                <cpt:property id="sequence"/>
+            </cpt:fieldSet>
+            <cpt:fieldSet name="Target" id="target">
+                <cpt:property id="targetClass"/>
+                <cpt:property id="targetAction"/>
+                <cpt:property id="propertyId"/>
+                <cpt:property id="targetStr"/>
+            </cpt:fieldSet>
+        </bs3:col>
+        <bs3:col span="4">
+            <cpt:fieldSet name="Arguments" id="arguments">
+                <cpt:property id="preValue"/>
+                <cpt:property id="postValue"/>
+            </cpt:fieldSet>
+        </bs3:col>
+        <bs3:col span="4">
+            <cpt:collection id="auditEntriesInTransaction"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12">
+            <bs3:tabGroup>
+                <bs3:tab name="Publishing">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:collection id="publishedEventsInTransaction"/>
+                            <cpt:collection id="statusMessagesInTransaction"/>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+            </bs3:tabGroup>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12" unreferencedCollections="true">
+        </bs3:col>
+    </bs3:row>
+</bs3:grid>
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.png b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.png
new file mode 100644
index 0000000000..4e4352c474
Binary files /dev/null and b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditEntry.png differ
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditerServiceUsingJdo.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditerServiceUsingJdo.java
new file mode 100644
index 0000000000..8126386f97
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditerServiceUsingJdo.java
@@ -0,0 +1,56 @@
+package org.isisaddons.module.audit.dom;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.services.audit.AuditerService;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.repository.RepositoryService;
+
+@DomainService(
+        nature = NatureOfService.DOMAIN
+)
+public class AuditerServiceUsingJdo implements AuditerService {
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+
+    @Programmatic
+    public void audit(
+            final UUID transactionId,
+            final int sequence,
+            String targetClass, final Bookmark target,
+            String memberIdentifier, final String propertyId, 
+            final String preValue, final String postValue, 
+            final String user, final java.sql.Timestamp timestamp) {
+        
+        final AuditEntry auditEntry = repositoryService.instantiate(AuditEntry.class);
+        
+        auditEntry.setTimestamp(timestamp);
+        auditEntry.setUser(user);
+        auditEntry.setTransactionId(transactionId);
+        auditEntry.setSequence(sequence);
+
+        auditEntry.setTargetClass(targetClass);
+        auditEntry.setTarget(target);
+        
+        auditEntry.setMemberIdentifier(memberIdentifier);
+        auditEntry.setPropertyId(propertyId);
+        
+        auditEntry.setPreValue(preValue);
+        auditEntry.setPostValue(postValue);
+        
+        repositoryService.persist(auditEntry);
+    }
+
+
+    @Inject
+    RepositoryService repositoryService;
+
+}
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceMenu.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceMenu.java
new file mode 100644
index 0000000000..3ac0713960
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceMenu.java
@@ -0,0 +1,84 @@
+package org.isisaddons.module.audit.dom;
+
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Parameter;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.clock.ClockService;
+
+@DomainService(
+        nature = NatureOfService.VIEW_MENU_ONLY,
+        objectType = "isisaudit.AuditingServiceMenu"
+)
+@DomainServiceLayout(
+        named = "Activity",
+        menuBar = DomainServiceLayout.MenuBar.SECONDARY,
+        menuOrder = "30"
+)
+public class AuditingServiceMenu {
+
+    //region > domain events
+    public static abstract class PropertyDomainEvent<T>
+            extends org.apache.isis.applib.services.eventbus.PropertyDomainEvent<AuditingServiceMenu, T> {
+    }
+
+    public static abstract class CollectionDomainEvent<T>
+            extends org.apache.isis.applib.services.eventbus.CollectionDomainEvent<AuditingServiceMenu, T> {
+    }
+
+    public static abstract class ActionDomainEvent
+            extends org.apache.isis.applib.services.eventbus.ActionDomainEvent<AuditingServiceMenu> {
+    }
+    //endregion
+
+    //region > findAuditEntries (action)
+    public static class FindAuditEntriesDomainEvent extends ActionDomainEvent { }
+
+    @Action(
+            domainEvent = FindAuditEntriesDomainEvent.class,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(
+            cssClassFa = "fa-search"
+    )
+    @MemberOrder(sequence="10")
+    public List<AuditEntry> findAuditEntries(
+            @Parameter(optionality= Optionality.OPTIONAL)
+            @ParameterLayout(named="From")
+            final LocalDate from,
+            @Parameter(optionality=Optionality.OPTIONAL)
+            @ParameterLayout(named="To")
+            final LocalDate to) {
+        return auditingServiceRepository.findByFromAndTo(from, to);
+    }
+    public boolean hideFindAuditEntries() {
+        return auditingServiceRepository == null;
+    }
+    public LocalDate default0FindAuditEntries() {
+        return clockService.now().minusDays(7);
+    }
+    public LocalDate default1FindAuditEntries() {
+        return clockService.now();
+    }
+    //endregion
+
+    //region > injected services
+    @javax.inject.Inject
+    private AuditingServiceRepository auditingServiceRepository;
+    
+    @javax.inject.Inject
+    private ClockService clockService;
+    //endregion
+
+}
+
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceRepository.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceRepository.java
new file mode 100644
index 0000000000..806f9b781a
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/AuditingServiceRepository.java
@@ -0,0 +1,163 @@
+package org.isisaddons.module.audit.dom;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.LocalDate;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.query.Query;
+import org.apache.isis.applib.query.QueryDefault;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.repository.RepositoryService;
+
+/**
+ * Provides supporting functionality for querying {@link AuditEntry audit entry} entities.
+ *
+ * <p>
+ * This supporting service with no UI and no side-effects, and there are no other implementations of the service,
+ * thus has been annotated with {@link org.apache.isis.applib.annotation.DomainService}.  This means that there is no
+ * need to explicitly register it as a service (eg in <tt>isis.properties</tt>).
+ */
+@DomainService(
+        nature = NatureOfService.DOMAIN
+)
+public class AuditingServiceRepository {
+
+    @Programmatic
+    public AuditEntry findFirstByTarget(final Bookmark target) {
+        final String targetStr = target.toString();
+        return findFirstByTarget(targetStr);
+    }
+
+    @Programmatic
+    public AuditEntry findFirstByTarget(final String targetStr) {
+        final List<AuditEntry> matches = repositoryService.allMatches(
+                new QueryDefault<>(AuditEntry.class,
+                        "findFirstByTarget",
+                        "targetStr", targetStr
+                ));
+        return matches.isEmpty() ? null : matches.get(0);
+    }
+
+    @Programmatic
+    public List<AuditEntry> findRecentByTarget(final Bookmark target) {
+        final String targetStr = target.toString();
+        return findRecentByTarget(targetStr);
+    }
+
+    @Programmatic
+    public List<AuditEntry> findRecentByTarget(final String targetStr) {
+        return repositoryService.allMatches(
+                new QueryDefault<>(AuditEntry.class,
+                        "findRecentByTarget",
+                        "targetStr", targetStr
+                ));
+    }
+
+    @Programmatic
+    public List<AuditEntry> findRecentByTargetAndPropertyId(
+            final Bookmark target,
+            final String propertyId) {
+        final String targetStr = target.toString();
+        return repositoryService.allMatches(
+                new QueryDefault<>(AuditEntry.class,
+                        "findRecentByTargetAndPropertyId",
+                        "targetStr", targetStr,
+                        "propertyId", propertyId
+                    ));
+    }
+
+    @Programmatic
+    public List<AuditEntry> findByTransactionId(final UUID transactionId) {
+        return repositoryService.allMatches(
+                new QueryDefault<>(AuditEntry.class,
+                        "findByTransactionId", 
+                        "transactionId", transactionId));
+    }
+
+    @Programmatic
+    public List<AuditEntry> findByTargetAndFromAndTo(
+            final Bookmark target, 
+            final LocalDate from, 
+            final LocalDate to) {
+        final String targetStr = target.toString();
+        final Timestamp fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        final Timestamp toTs = toTimestampStartOfDayWithOffset(to, 1);
+        
+        final Query<AuditEntry> query;
+        if(from != null) {
+            if(to != null) {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTargetAndTimestampBetween", 
+                        "targetStr", targetStr,
+                        "from", fromTs,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTargetAndTimestampAfter", 
+                        "targetStr", targetStr,
+                        "from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTargetAndTimestampBefore", 
+                        "targetStr", targetStr,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTarget", 
+                        "targetStr", targetStr);
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+    @Programmatic
+    public List<AuditEntry> findByFromAndTo(
+            final LocalDate from, 
+            final LocalDate to) {
+        final Timestamp fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        final Timestamp toTs = toTimestampStartOfDayWithOffset(to, 1);
+        
+        final Query<AuditEntry> query;
+        if(from != null) {
+            if(to != null) {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTimestampBetween", 
+                        "from", fromTs,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTimestampAfter", 
+                        "from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "findByTimestampBefore", 
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(AuditEntry.class,
+                        "find");
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+    private static Timestamp toTimestampStartOfDayWithOffset(final LocalDate dt, final int daysOffset) {
+        return dt!=null
+                ?new java.sql.Timestamp(dt.toDateTimeAtStartOfDay().plusDays(daysOffset).getMillis())
+                :null;
+    }
+
+    @Inject
+    RepositoryService repositoryService;
+
+}
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/HasTransactionId_auditEntriesInTransaction.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/HasTransactionId_auditEntriesInTransaction.java
new file mode 100644
index 0000000000..7d178150a0
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/HasTransactionId_auditEntriesInTransaction.java
@@ -0,0 +1,51 @@
+package org.isisaddons.module.audit.dom;
+
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.Contributed;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Mixin;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.HasTransactionId;
+
+import org.isisaddons.module.audit.AuditModule;
+
+@Mixin
+public class HasTransactionId_auditEntriesInTransaction {
+
+    public static class ActionDomainEvent extends AuditModule.ActionDomainEvent<HasTransactionId_auditEntriesInTransaction> {
+    }
+
+    private final HasTransactionId hasTransactionId;
+
+    public HasTransactionId_auditEntriesInTransaction(HasTransactionId hasTransactionId) {
+        this.hasTransactionId = hasTransactionId;
+    }
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            domainEvent = ActionDomainEvent.class
+    )
+    @ActionLayout(
+            contributed = Contributed.AS_ASSOCIATION
+    )
+    @CollectionLayout(
+            defaultView = "table"
+    )
+    @MemberOrder(sequence = "50.100")
+    public List<AuditEntry> $$() {
+        return auditEntryRepository.findByTransactionId(hasTransactionId.getTransactionId());
+    }
+
+    //endregion
+
+    //region > injected services
+
+    @javax.inject.Inject
+    private AuditingServiceRepository auditEntryRepository;
+    //endregion
+
+}
diff --git a/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/Object_recentAuditEntries.java b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/Object_recentAuditEntries.java
new file mode 100644
index 0000000000..5c9334f6fb
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/main/java/org/isisaddons/module/audit/dom/Object_recentAuditEntries.java
@@ -0,0 +1,96 @@
+package org.isisaddons.module.audit.dom;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.Contributed;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Mixin;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.HasTransactionId;
+import org.apache.isis.applib.services.appfeat.ApplicationFeatureRepository;
+import org.apache.isis.applib.services.appfeat.ApplicationMemberType;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService2;
+import org.apache.isis.applib.services.metamodel.MetaModelService2;
+import org.apache.isis.applib.services.metamodel.MetaModelService3;
+
+@Mixin(method="act")
+public class Object_recentAuditEntries {
+
+    private final Object domainObject;
+
+    public Object_recentAuditEntries(final Object domainObject) {
+        this.domainObject = domainObject;
+    }
+
+    public static class ActionDomainEvent
+            extends org.apache.isis.applib.services.eventbus.ActionDomainEvent<Object_recentAuditEntries> {
+    }
+
+    @Action(
+            domainEvent = ActionDomainEvent.class,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(
+            contributed = Contributed.AS_ACTION
+    )
+    @MemberOrder(name = "Metadata", sequence = "10")
+    public List<AuditEntry> act(
+            @ParameterLayout(named = "Object property")
+            final String propertyName) {
+        final Bookmark target = bookmarkService.bookmarkFor(domainObject);
+        return auditingServiceRepository.findRecentByTargetAndPropertyId(target, propertyName);
+    }
+    public List<String> choices0Act() {
+        final Class<?> domainClass = domainObject.getClass();
+        final String packageName = domainClass.getPackage().getName();
+        final String className = domainClass.getSimpleName();
+        return Lists.newArrayList(
+                FluentIterable.from(
+                        applicationFeatureRepository
+                                .memberNamesOf(packageName, className, ApplicationMemberType.PROPERTY)
+                ).filter(Predicates.not(excludedProperties)));
+    }
+    public String default0Act() {
+        final List<String> choices = choices0Act();
+        return choices.size() == 1 ? choices.get(0): null;
+    }
+
+    public boolean hideAct() {
+        MetaModelService2.Sort sort = metaModelService3.sortOf(domainObject.getClass(), MetaModelService3.Mode.RELAXED);
+        return !sort.isJdoEntity() || domainObject instanceof HasTransactionId;
+    }
+
+    static final Predicate<String> excludedProperties = new Predicate<String>() {
+        private List<String> excluded = Lists.newArrayList(
+                "datanucleusIdLong", "datanucleusVersionLong", "datanucleusVersionTimestamp"
+        );
+        @Override
+        public boolean apply(@Nullable final String propertyName) {
+            return excluded.contains(propertyName);
+        }
+    };
+
+    @javax.inject.Inject
+    private MetaModelService3 metaModelService3;
+
+    @Inject
+    ApplicationFeatureRepository applicationFeatureRepository;
+
+    @Inject
+    AuditingServiceRepository auditingServiceRepository;
+
+    @Inject
+    BookmarkService2 bookmarkService;
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/titled/TitledEnumContractForIncodeModuleTest_title.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/titled/TitledEnumContractForIncodeModuleTest_title.java
new file mode 100644
index 0000000000..28ea2566cf
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/titled/TitledEnumContractForIncodeModuleTest_title.java
@@ -0,0 +1,16 @@
+package org.isisaddons.module.audit.dom.contracttests.titled;
+
+import org.incode.module.base.dom.TitledEnum;
+import org.incode.module.base.dom.titled.TitledEnumContractTestAbstract_title;
+
+/**
+ * Automatically tests all enums implementing {@link TitledEnum}.
+ */
+public class TitledEnumContractForIncodeModuleTest_title extends TitledEnumContractTestAbstract_title {
+
+    public TitledEnumContractForIncodeModuleTest_title() {
+        super("org.isisaddons.module.audit");
+    }
+}
+
+
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeComparableContractForIncodeModuleTest_compareTo.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeComparableContractForIncodeModuleTest_compareTo.java
new file mode 100644
index 0000000000..5ab0f2fbab
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeComparableContractForIncodeModuleTest_compareTo.java
@@ -0,0 +1,18 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.incode.module.base.dom.with.ComparableByCodeContractTestAbstract_compareTo;
+import org.incode.module.base.dom.with.WithCodeComparable;
+
+/**
+ * Automatically tests all domain objects implementing {@link WithCodeComparable}.
+ */
+public class WithCodeComparableContractForIncodeModuleTest_compareTo extends
+        ComparableByCodeContractTestAbstract_compareTo {
+
+    public WithCodeComparableContractForIncodeModuleTest_compareTo() {
+        super("org.isisaddons.module.audit", ImmutableMap.<Class<?>,Class<?>>of());
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
new file mode 100644
index 0000000000..a0816f0bb8
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithCodeUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
@@ -0,0 +1,13 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import org.incode.module.base.dom.with.WithCodeUnique;
+import org.incode.module.base.dom.with.WithFieldUniqueContractTestAllAbstract;
+
+public class WithCodeUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation extends
+        WithFieldUniqueContractTestAllAbstract<WithCodeUnique> {
+
+    public WithCodeUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation() {
+        super("org.isisaddons.module.audit", "code", WithCodeUnique.class);
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionComparableContractForIncodeModuleTest_compareTo.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionComparableContractForIncodeModuleTest_compareTo.java
new file mode 100644
index 0000000000..ec709fbfcc
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionComparableContractForIncodeModuleTest_compareTo.java
@@ -0,0 +1,18 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.incode.module.base.dom.with.ComparableByDescriptionContractTestAbstract_compareTo;
+import org.incode.module.base.dom.with.WithDescriptionComparable;
+
+/**
+ * Automatically tests all domain objects implementing {@link WithDescriptionComparable}.
+ */
+public class WithDescriptionComparableContractForIncodeModuleTest_compareTo extends
+        ComparableByDescriptionContractTestAbstract_compareTo {
+
+    public WithDescriptionComparableContractForIncodeModuleTest_compareTo() {
+        super("org.isisaddons.module.audit", ImmutableMap.<Class<?>,Class<?>>of());
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
new file mode 100644
index 0000000000..e6cfec0506
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithDescriptionUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
@@ -0,0 +1,13 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import org.incode.module.base.dom.with.WithDescriptionUnique;
+import org.incode.module.base.dom.with.WithFieldUniqueContractTestAllAbstract;
+
+public class WithDescriptionUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation extends
+        WithFieldUniqueContractTestAllAbstract<WithDescriptionUnique> {
+
+    public WithDescriptionUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation() {
+        super("org.isisaddons.module.audit", "description", WithDescriptionUnique.class);
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameComparableContractForIncodeModuleTest_compareTo.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameComparableContractForIncodeModuleTest_compareTo.java
new file mode 100644
index 0000000000..b8e76ea80e
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameComparableContractForIncodeModuleTest_compareTo.java
@@ -0,0 +1,19 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.incode.module.base.dom.with.ComparableByNameContractTestAbstract_compareTo;
+import org.incode.module.base.dom.with.WithNameComparable;
+
+/**
+ * Automatically tests all domain objects implementing
+ * {@link WithNameComparable}.
+ */
+public class WithNameComparableContractForIncodeModuleTest_compareTo extends
+        ComparableByNameContractTestAbstract_compareTo {
+
+    public WithNameComparableContractForIncodeModuleTest_compareTo() {
+        super("org.isisaddons.module.audit", ImmutableMap.<Class<?>, Class<?>>of());
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
new file mode 100644
index 0000000000..8d9da4cdac
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithNameUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
@@ -0,0 +1,13 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import org.incode.module.base.dom.with.WithFieldUniqueContractTestAllAbstract;
+import org.incode.module.base.dom.with.WithNameUnique;
+
+public class WithNameUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation extends
+        WithFieldUniqueContractTestAllAbstract<WithNameUnique> {
+
+    public WithNameUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation() {
+        super("org.isisaddons.module.audit", "name", WithNameUnique.class);
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceComparableContractForIncodeModuleTest_compareTo.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceComparableContractForIncodeModuleTest_compareTo.java
new file mode 100644
index 0000000000..28a9d93c6f
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceComparableContractForIncodeModuleTest_compareTo.java
@@ -0,0 +1,18 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.incode.module.base.dom.with.ComparableByReferenceContractTestAbstract_compareTo;
+import org.incode.module.base.dom.with.WithReferenceComparable;
+
+/**
+ * Automatically tests all domain objects implementing {@link WithReferenceComparable}.
+ */
+public class WithReferenceComparableContractForIncodeModuleTest_compareTo extends
+        ComparableByReferenceContractTestAbstract_compareTo {
+
+    public WithReferenceComparableContractForIncodeModuleTest_compareTo() {
+        super("org.isisaddons.module.audit", ImmutableMap.<Class<?>, Class<?>>of());
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
new file mode 100644
index 0000000000..c74f12c00d
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithReferenceUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
@@ -0,0 +1,13 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import org.incode.module.base.dom.with.WithFieldUniqueContractTestAllAbstract;
+import org.incode.module.base.dom.with.WithReferenceUnique;
+
+public class WithReferenceUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation extends
+        WithFieldUniqueContractTestAllAbstract<WithReferenceUnique> {
+
+    public WithReferenceUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation() {
+        super("org.isisaddons.module.audit", "reference", WithReferenceUnique.class);
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleComparableContractForIncodeModuleTest_compareTo.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleComparableContractForIncodeModuleTest_compareTo.java
new file mode 100644
index 0000000000..0117122e2a
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleComparableContractForIncodeModuleTest_compareTo.java
@@ -0,0 +1,18 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.incode.module.base.dom.with.ComparableByTitleContractTestAbstract_compareTo;
+import org.incode.module.base.dom.with.WithTitleComparable;
+
+/**
+ * Automatically tests all domain objects implementing {@link WithTitleComparable}.
+ */
+public class WithTitleComparableContractForIncodeModuleTest_compareTo extends
+        ComparableByTitleContractTestAbstract_compareTo {
+
+    public WithTitleComparableContractForIncodeModuleTest_compareTo() {
+        super("org.isisaddons.module.audit", ImmutableMap.<Class<?>,Class<?>>of());
+    }
+
+}
diff --git a/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
new file mode 100644
index 0000000000..20425954cf
--- /dev/null
+++ b/extensions/security/audittrail/impl/src/test/java/org/isisaddons/module/audit/dom/contracttests/with/WithTitleUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation.java
@@ -0,0 +1,13 @@
+package org.isisaddons.module.audit.dom.contracttests.with;
+
+import org.incode.module.base.dom.with.WithFieldUniqueContractTestAllAbstract;
+import org.incode.module.base.dom.with.WithTitleUnique;
+
+public class WithTitleUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation extends
+        WithFieldUniqueContractTestAllAbstract<WithTitleUnique> {
+
+    public WithTitleUniqueContractForIncodeModuleTest_hasJdoUniqueIndexAnnotation() {
+        super("org.isisaddons.module.audit", "title", WithTitleUnique.class);
+    }
+
+}
diff --git a/extensions/security/audittrail/pom.xml b/extensions/security/audittrail/pom.xml
new file mode 100644
index 0000000000..57d89501f3
--- /dev/null
+++ b/extensions/security/audittrail/pom.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.incode</groupId>
+        <artifactId>incode-parent</artifactId>
+        <version>${revision}</version>
+        <relativePath>../../../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.isisaddons.module.audit</groupId>
+    <artifactId>isis-module-audit-dom</artifactId>
+    <name>Incode Platform SPI Audit Impl</name>
+
+    <description>
+        A module providing audit persistence, enabling profiling, background execution and
+        extended auditing capabilities.
+    </description>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.incode.platform.spi.audit</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/incode/platform/spi/audit</git-plugin.propertiesDir>
+
+        <git-plugin.gitDir>${project.basedir}/../../../../.git</git-plugin.gitDir>
+    </properties>
+
+    <build>
+        <resources>
+            <resource>
+                <filtering>false</filtering>
+                <directory>impl/src/main/java</directory>
+                <includes>
+                    <include>**</include>
+                </includes>
+                <excludes>
+                    <exclude>**/*.java</exclude>
+                </excludes>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>com.github.odavid.maven.plugins</groupId>
+                <artifactId>mixin-maven-plugin</artifactId>
+                <version>0.1-alpha-39</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <mixins>
+                        <mixin>
+                            <groupId>com.danhaywood.mavenmixin</groupId>
+                            <artifactId>datanucleusenhance</artifactId>
+                        </mixin>
+                        <mixin>
+                            <groupId>com.danhaywood.mavenmixin</groupId>
+                            <artifactId>enforcerrelaxed</artifactId>
+                        </mixin>
+                        <mixin>
+                            <groupId>com.danhaywood.mavenmixin</groupId>
+                            <artifactId>standard</artifactId>
+                        </mixin>
+                        <mixin>
+                            <groupId>com.danhaywood.mavenmixin</groupId>
+                            <artifactId>sourceandjavadoc</artifactId>
+                        </mixin>
+                    </mixins>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-core-applib</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.incode.module.base</groupId>
+            <artifactId>incode-module-base-dom</artifactId>
+        </dependency>
+
+        <!-- test -->
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-core-unittestsupport</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.incode.module.base</groupId>
+            <artifactId>incode-module-base-dom</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+
+        <dependency>
+            <groupId>org.datanucleus</groupId>
+            <artifactId>datanucleus-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- provided -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>git</id>
+            <activation>
+                <property>
+                    <name>!skip.git</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>pl.project13.maven</groupId>
+                        <artifactId>git-commit-id-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>flatten</id>
+            <activation>
+                <property>
+                    <name>revision</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>flatten-maven-plugin</artifactId>
+                        <version>1.0.0</version>
+                        <executions>
+                            <execution>
+                                <id>flatten</id>
+                                <phase>process-resources</phase>
+                                <goals>
+                                    <goal>flatten</goal>
+                                </goals>
+                                <configuration>
+                                    <flattenMode>defaults</flattenMode>
+                                    <updatePomFile>true</updatePomFile>
+                                    <pomElements>
+                                        <name>resolve</name>
+                                        <description>resolve</description>
+                                        <dependencies>resolve</dependencies>
+                                    </pomElements>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>flatten.clean</id>
+                                <phase>clean</phase>
+                                <goals>
+                                    <goal>clean</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/IsisModuleExtSecmanApplib.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/IsisModuleExtSecmanApplib.java
index 3ba3907e5c..09a71f9691 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/IsisModuleExtSecmanApplib.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/IsisModuleExtSecmanApplib.java
@@ -186,6 +186,7 @@ import org.apache.isis.testing.fixtures.applib.IsisModuleTestingFixturesApplib;
 public class IsisModuleExtSecmanApplib {
 
     public static final String NAMESPACE = "isis.ext.secman";
+    public static final String SCHEMA = "isisExtensionsSecman";
 
     public abstract static class ActionDomainEvent<S>
     extends org.apache.isis.applib.events.domain.ActionDomainEvent<S> {}
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
index 93e1a5ba57..f284451c39 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
@@ -86,6 +86,8 @@ import lombok.experimental.UtilityClass;
 public abstract class ApplicationPermission implements Comparable<ApplicationPermission> {
 
     public static final String LOGICAL_TYPE_NAME = IsisModuleExtSecmanApplib.NAMESPACE + ".ApplicationPermission";
+    public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
+    public static final String TABLE = "ApplicationPermission";
 
     public static final String NAMED_QUERY_FIND_BY_FEATURE = "ApplicationPermission.findByFeature";
     public static final String NAMED_QUERY_FIND_BY_ROLE = "ApplicationPermission.findByRole";
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
index 987e242864..c408a4d868 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
@@ -56,6 +56,8 @@ import org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser;
 public abstract class ApplicationRole implements Comparable<ApplicationRole> {
 
     public static final String LOGICAL_TYPE_NAME = IsisModuleExtSecmanApplib.NAMESPACE + ".ApplicationRole";
+    public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
+    public static final String TABLE = "ApplicationRole";
 
     public static final String NAMED_QUERY_FIND_BY_NAME = "ApplicationRole.findByName";
     public static final String NAMED_QUERY_FIND_BY_NAME_CONTAINING = "ApplicationRole.findByNameContaining";
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
index a6821ee30a..264659e6ab 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
@@ -50,6 +50,8 @@ import org.apache.isis.extensions.secman.applib.IsisModuleExtSecmanApplib;
 public abstract class ApplicationTenancy implements Comparable<ApplicationTenancy> {
 
     public static final String LOGICAL_TYPE_NAME = IsisModuleExtSecmanApplib.NAMESPACE + ".ApplicationTenancy";
+    public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
+    public static final String TABLE = "ApplicationTenancy";
 
     public static final String NAMED_QUERY_FIND_BY_NAME = "ApplicationTenancy.findByName";
     public static final String NAMED_QUERY_FIND_BY_PATH = "ApplicationTenancy.findByPath";
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
index e563958101..27c12b0393 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
@@ -74,6 +74,9 @@ public abstract class ApplicationUser
         implements HasUsername, HasAtPath, Comparable<ApplicationUser> {
 
     public static final String LOGICAL_TYPE_NAME = IsisModuleExtSecmanApplib.NAMESPACE + ".ApplicationUser";
+    public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
+    public static final String TABLE = "ApplicationUser";
+
 
     @Inject private transient ApplicationUserRepository applicationUserRepository;
     @Inject private transient ApplicationPermissionRepository applicationPermissionRepository;
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
index e9b15c05c7..0a43517161 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
@@ -44,8 +44,8 @@ import org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole;
 
 @PersistenceCapable(
         identityType = IdentityType.DATASTORE,
-        schema = "isisExtensionsSecman",
-        table = "ApplicationPermission")
+        schema = ApplicationPermission.SCHEMA,
+        table = ApplicationPermission.TABLE)
 @Inheritance(
         strategy = InheritanceStrategy.NEW_TABLE)
 @DatastoreIdentity(
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
index 57ab64bf00..7e511239a8 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
@@ -44,8 +44,8 @@ import org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser;
 
 @PersistenceCapable(
         identityType = IdentityType.DATASTORE,
-        schema = "isisExtensionsSecman",
-        table = "ApplicationRole")
+        schema = ApplicationRole.SCHEMA,
+        table = ApplicationRole.TABLE)
 @Inheritance(
         strategy = InheritanceStrategy.NEW_TABLE)
 @DatastoreIdentity(
@@ -81,7 +81,6 @@ public class ApplicationRole
     protected final static String FQCN = "org.apache.isis.extensions.secman.jdo.role.dom.ApplicationRole";
 
 
-
     // -- NAME
 
     @Column(allowsNull = "false", length = Name.MAX_LENGTH)
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
index 1e30abb8d8..99263f0537 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
@@ -45,8 +45,8 @@ import org.apache.isis.commons.internal.base._Casts;
 
 @PersistenceCapable(
         identityType = IdentityType.APPLICATION,
-        schema = "isisExtensionsSecman",
-        table = "ApplicationTenancy")
+        schema = ApplicationTenancy.SCHEMA,
+        table = ApplicationTenancy.TABLE)
 @Inheritance(
         strategy = InheritanceStrategy.NEW_TABLE)
 @DatastoreIdentity(
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
index df7069fcee..519c8773a6 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
@@ -51,8 +51,8 @@ import lombok.Setter;
 
 @PersistenceCapable(
         identityType = IdentityType.DATASTORE,
-        schema = "isisExtensionsSecman",
-        table = "ApplicationUser")
+        schema = ApplicationUser.SCHEMA,
+        table = ApplicationUser.TABLE)
 @Inheritance(
         strategy = InheritanceStrategy.NEW_TABLE)
 @DatastoreIdentity(
@@ -105,6 +105,7 @@ public class ApplicationUser
     protected final static String FQCN = "org.apache.isis.extensions.secman.jdo.user.dom.ApplicationUser";
 
 
+
     // -- USERNAME
 
     @Column(allowsNull = "false", length = Username.MAX_LENGTH)
diff --git a/extensions/security/session-log/adoc/antora.yml b/extensions/security/sessionlog/adoc/antora.yml
similarity index 100%
rename from extensions/security/session-log/adoc/antora.yml
rename to extensions/security/sessionlog/adoc/antora.yml
diff --git a/extensions/security/session-log/adoc/modules/session-log/nav.adoc b/extensions/security/sessionlog/adoc/modules/session-log/nav.adoc
similarity index 100%
rename from extensions/security/session-log/adoc/modules/session-log/nav.adoc
rename to extensions/security/sessionlog/adoc/modules/session-log/nav.adoc
diff --git a/extensions/security/session-log/adoc/modules/session-log/pages/about.adoc b/extensions/security/sessionlog/adoc/modules/session-log/pages/about.adoc
similarity index 100%
rename from extensions/security/session-log/adoc/modules/session-log/pages/about.adoc
rename to extensions/security/sessionlog/adoc/modules/session-log/pages/about.adoc
diff --git a/extensions/security/session-log/adoc/modules/session-log/partials/module-nav.adoc b/extensions/security/sessionlog/adoc/modules/session-log/partials/module-nav.adoc
similarity index 100%
rename from extensions/security/session-log/adoc/modules/session-log/partials/module-nav.adoc
rename to extensions/security/sessionlog/adoc/modules/session-log/partials/module-nav.adoc
diff --git a/extensions/security/sessionlog/applib/pom.xml b/extensions/security/sessionlog/applib/pom.xml
new file mode 100644
index 0000000000..22fc7ee998
--- /dev/null
+++ b/extensions/security/sessionlog/applib/pom.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.isis.extensions</groupId>
+        <artifactId>isis-extensions-sessionlog</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>isis-extensions-sessionlog-applib</artifactId>
+    <name>Apache Isis Ext - Session Log Applib</name>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.isis.extensions.sessionlog.applib</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/isis/extensions/sessionlog/applib</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.isis.persistence</groupId>
+            <artifactId>isis-persistence-jdo-datanucleus</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-core-runtime</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.testing</groupId>
+            <artifactId>isis-testing-fixtures-applib</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/extensions/security/sessionlog/applib/src/main/java/org/apache/isis/sessionlog/applib/IsisModuleExtSessionLogApplib.java b/extensions/security/sessionlog/applib/src/main/java/org/apache/isis/sessionlog/applib/IsisModuleExtSessionLogApplib.java
new file mode 100644
index 0000000000..241074c03f
--- /dev/null
+++ b/extensions/security/sessionlog/applib/src/main/java/org/apache/isis/sessionlog/applib/IsisModuleExtSessionLogApplib.java
@@ -0,0 +1,25 @@
+package org.apache.isis.sessionlog.applib;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+
+@Configuration
+@Import({
+
+})
+public class IsisModuleExtSessionLogApplib {
+
+    public static final String NAMESPACE = "isis.ext.sessionlog";
+    public static final String SCHEMA = "isisExtSessionLog";
+
+    public abstract static class ActionDomainEvent<S>
+    extends org.apache.isis.applib.events.domain.ActionDomainEvent<S> {}
+
+    public abstract static class CollectionDomainEvent<S, T>
+    extends org.apache.isis.applib.events.domain.CollectionDomainEvent<S, T> {}
+
+    public abstract static class PropertyDomainEvent<S, T>
+    extends org.apache.isis.applib.events.domain.PropertyDomainEvent<S, T> {}
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/pom.xml b/extensions/security/sessionlog/persistence-jdo/pom.xml
new file mode 100644
index 0000000000..28ebe5d114
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.isis.extensions</groupId>
+        <artifactId>isis-extensions-sessionlog</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>sis-extensions-sessionlog-persistence-jdo</artifactId>
+    <name>Apache Isis Ext - Session Log Persistence (using JDO)</name>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.isis.extensions.sessionlog.jdo</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/isis/extensions/sessionlog/jdo</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.isis.extensions</groupId>
+            <artifactId>isis-extensions-sessionlog-applib</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.persistence</groupId>
+            <artifactId>isis-persistence-jdo-datanucleus</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-core-runtime</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.testing</groupId>
+            <artifactId>isis-testing-fixtures-applib</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/META-INF/persistence.xml b/extensions/security/sessionlog/persistence-jdo/src/main/java/META-INF/persistence.xml
new file mode 100644
index 0000000000..b8820bfd01
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/META-INF/persistence.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Copyright 2013~2014 Dan Haywood
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      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.
+-->
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
+
+    <persistence-unit name="org-isisaddons-module-sessionlogger-dom">
+    </persistence-unit>
+</persistence>
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/IsisModuleExtSessionLogPersistenceJdo.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/IsisModuleExtSessionLogPersistenceJdo.java
new file mode 100644
index 0000000000..cf97369814
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/IsisModuleExtSessionLogPersistenceJdo.java
@@ -0,0 +1,29 @@
+package org.apache.isis.sessionlog.jdo;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import org.apache.isis.sessionlog.applib.IsisModuleExtSessionLogApplib;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntry;
+import org.apache.isis.testing.fixtures.applib.fixturescripts.FixtureScript;
+import org.apache.isis.testing.fixtures.applib.modules.ModuleWithFixtures;
+import org.apache.isis.testing.fixtures.applib.teardown.jdo.TeardownFixtureJdoAbstract;
+
+
+@Configuration
+@Import({
+    IsisModuleExtSessionLogApplib.class
+})
+public class IsisModuleExtSessionLogPersistenceJdo implements ModuleWithFixtures {
+
+    @Override
+    public FixtureScript getTeardownFixture() {
+        return new TeardownFixtureJdoAbstract() {
+            @Override
+            protected void execute(final ExecutionContext executionContext) {
+                deleteFrom(SessionLogEntry.class);
+            }
+        };
+    }
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/app/SessionLogMenu.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/app/SessionLogMenu.java
new file mode 100644
index 0000000000..9e5aeb3709
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/app/SessionLogMenu.java
@@ -0,0 +1,82 @@
+package org.apache.isis.sessionlog.jdo.app;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.joda.time.LocalDate;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.BookmarkPolicy;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.annotation.MemberSupport;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.sessionlog.applib.IsisModuleExtSessionLogApplib;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntry;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntryRepository;
+
+
+/**
+ * This service exposes a &lt;Sessions&gt; menu to the secondary menu bar for searching for sessions.
+ */
+@DomainService(nature = NatureOfService.VIEW)
+@DomainServiceLayout(
+        menuBar = DomainServiceLayout.MenuBar.SECONDARY,
+        named = "Activity"
+)
+@Named("isissessionlogger.SessionLoggingServiceMenu")
+public class SessionLogMenu {
+
+    public static abstract class ActionDomainEvent<T> extends IsisModuleExtSessionLogApplib.ActionDomainEvent<T> { }
+
+    @Action(
+            domainEvent = activeSessions.ActiveEvent.class,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(
+            bookmarking = BookmarkPolicy.AS_ROOT,
+            cssClassFa = "fa-bolt"
+    )
+    public class activeSessions {
+
+        public class ActiveEvent extends ActionDomainEvent<activeSessions> { }
+
+        @MemberSupport public List<SessionLogEntry> act() {
+            return sessionLogEntryRepository.findActiveSessions();
+        }
+    }
+
+
+
+    public class findSessions {
+        public class ActionEvent extends ActionDomainEvent<findSessions> { }
+
+        @Action(
+                domainEvent = findSessions.ActionEvent.class,
+                semantics = SemanticsOf.SAFE
+        )
+        @ActionLayout(
+                cssClassFa = "fa-search"
+        )
+        public List<SessionLogEntry> act(
+                final @Nullable String user,
+                final @Nullable LocalDate from,
+                final @Nullable LocalDate to) {
+
+            if(user == null) {
+                return sessionLogEntryRepository.findByFromAndTo(from, to);
+            } else {
+                return sessionLogEntryRepository.findByUserAndFromAndTo(user, from, to);
+            }
+        }
+    }
+
+
+    @Inject SessionLogEntryRepository sessionLogEntryRepository;
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/contributions/HasUsername_recentSessionsForUser.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/contributions/HasUsername_recentSessionsForUser.java
new file mode 100644
index 0000000000..f232b37033
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/contributions/HasUsername_recentSessionsForUser.java
@@ -0,0 +1,50 @@
+package org.apache.isis.sessionlog.jdo.contributions;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.MemberSupport;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.mixins.security.HasUsername;
+
+
+import org.apache.isis.sessionlog.applib.IsisModuleExtSessionLogApplib;
+import org.apache.isis.sessionlog.jdo.IsisModuleExtSessionLogPersistenceJdo;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntry;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntryRepository;
+
+import lombok.RequiredArgsConstructor;
+
+
+@Action(
+        semantics = SemanticsOf.SAFE,
+        domainEvent = HasUsername_recentSessionsForUser.ActionDomainEvent.class
+)
+@ActionLayout(
+        fieldSetId = "username"
+)
+@RequiredArgsConstructor
+public class HasUsername_recentSessionsForUser {
+
+    public static class ActionDomainEvent
+            extends IsisModuleExtSessionLogApplib.ActionDomainEvent<HasUsername_recentSessionsForUser> { }
+
+    private final HasUsername hasUsername;
+
+    @MemberSupport public List<SessionLogEntry> act() {
+        if(hasUsername == null || hasUsername.getUsername() == null) {
+            return Collections.emptyList();
+        }
+        return sessionLogEntryRepository.findRecentByUser(hasUsername.getUsername());
+    }
+    @MemberSupport public boolean hideAct() {
+        return hasUsername == null || hasUsername.getUsername() == null;
+    }
+
+    @Inject SessionLogEntryRepository sessionLogEntryRepository;
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-expired.png b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-expired.png
new file mode 100644
index 0000000000..0f2baed241
Binary files /dev/null and b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-expired.png differ
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-login.png b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-login.png
new file mode 100644
index 0000000000..720d7ad699
Binary files /dev/null and b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-login.png differ
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-logout.png b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-logout.png
new file mode 100644
index 0000000000..aa4a3d2d7f
Binary files /dev/null and b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry-logout.png differ
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java
new file mode 100644
index 0000000000..0030d78472
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java
@@ -0,0 +1,467 @@
+package org.apache.isis.sessionlog.jdo.dom;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.IdentityType;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.PrimaryKey;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.DomainObjectLayout;
+import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.MemberSupport;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Parameter;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.PropertyLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.layout.component.CssClassFaPosition;
+import org.apache.isis.applib.mixins.security.HasUsername;
+import org.apache.isis.applib.services.factory.FactoryService;
+import org.apache.isis.applib.services.session.SessionLogService;
+import org.apache.isis.applib.util.ObjectContracts;
+
+import org.apache.isis.sessionlog.applib.IsisModuleExtSessionLogApplib;
+
+import lombok.val;
+import lombok.experimental.UtilityClass;
+
+@PersistenceCapable(
+        identityType=IdentityType.APPLICATION,
+        schema = SessionLogEntry.SCHEMA,
+        table = SessionLogEntry.TABLE)
+@Queries( {
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_SESSION_ID,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                      + "WHERE sessionId == :sessionId"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_BETWEEN,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "&& loginTimestamp >= :from "
+                        + "&& logoutTimestamp <= :to "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_AFTER,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "&& loginTimestamp >= :from "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_BEFORE,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "&& loginTimestamp <= :from "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_TIMESTAMP_BETWEEN,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE loginTimestamp >= :from "
+                        + "&&    logoutTimestamp <= :to "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_TIMESTAMP_AFTER,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE loginTimestamp >= :from "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_TIMESTAMP_BEFORE,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE loginTimestamp <= :to "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_STRICTLY_BEFORE,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "&& loginTimestamp < :from "
+                        + "ORDER BY loginTimestamp DESC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_STRICTLY_AFTER,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "&& loginTimestamp > :from "
+                        + "ORDER BY loginTimestamp ASC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_ACTIVE_SESSIONS,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                      + "WHERE logoutTimestamp == null "
+                      + "ORDER BY loginTimestamp ASC"),
+        @Query(
+                name= SessionLogEntry.Nq.FIND_RECENT_BY_USER,
+                value="SELECT "
+                        + "FROM " + SessionLogEntry.FQCN + " "
+                        + "WHERE user == :user "
+                        + "ORDER BY loginTimestamp DESC "
+                        + "RANGE 0,10")
+})
+@DomainObject(
+        logicalTypeName = SessionLogEntry.LOGICAL_TYPE_NAME,
+        editing = Editing.DISABLED
+)
+public class SessionLogEntry implements HasUsername, Comparable<SessionLogEntry> {
+
+    public static final String LOGICAL_TYPE_NAME = IsisModuleExtSessionLogApplib.NAMESPACE + ".SessionLogEntry";
+    public static final String FQCN = "org.apache.isis.sessionlog.jdo.dom.SessionLogEntry";
+    public static final String SCHEMA = IsisModuleExtSessionLogApplib.SCHEMA;
+    public static final String TABLE = "SessionLogEntry";
+
+    @UtilityClass
+    static class Nq {
+        static final String FIND_BY_SESSION_ID = "findBySessionId";
+        static final String FIND_BY_USER_AND_TIMESTAMP_BETWEEN = "findByUserAndTimestampBetween";
+        static final String FIND_BY_USER_AND_TIMESTAMP_AFTER = "findByUserAndTimestampAfter";
+        static final String FIND_BY_USER_AND_TIMESTAMP_BEFORE = "findByUserAndTimestampBefore";
+        static final String FIND_BY_USER = "findByUser";
+        static final String FIND_BY_TIMESTAMP_BETWEEN = "findByTimestampBetween";
+        static final String FIND_BY_TIMESTAMP_AFTER = "findByTimestampAfter";
+        static final String FIND_BY_TIMESTAMP_BEFORE = "findByTimestampBefore";
+        static final String FIND = "find";
+        static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_BEFORE = "findByUserAndTimestampStrictlyBefore";
+        static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_AFTER = "findByUserAndTimestampStrictlyAfter";
+        static final String FIND_ACTIVE_SESSIONS = "findActiveSessions";
+        static final String FIND_RECENT_BY_USER = "findRecentByUser";
+    }
+
+    public static abstract class PropertyDomainEvent<T> extends IsisModuleExtSessionLogApplib.PropertyDomainEvent<SessionLogEntry, T> { }
+
+    public static abstract class CollectionDomainEvent<T> extends IsisModuleExtSessionLogApplib.CollectionDomainEvent<SessionLogEntry, T> { }
+
+    public static abstract class ActionDomainEvent extends IsisModuleExtSessionLogApplib.ActionDomainEvent<SessionLogEntry> { }
+
+    public SessionLogEntry(
+            final String sessionId,
+            final String username,
+            final SessionLogService.CausedBy causedBy,
+            final Timestamp loginTimestamp) {
+        this.sessionId = sessionId;
+        this.username = username;
+        this.causedBy = causedBy;
+        this.loginTimestamp = loginTimestamp;
+    }
+
+
+    public String title() {
+
+        // nb: not thread-safe
+        // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
+        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        return String.format("%s: %s logged %s %s",
+                format.format(getLoginTimestamp()),
+                getUsername(),
+                getLogoutTimestamp() == null ? "in": "out",
+                getCausedBy() == SessionLogService.CausedBy.SESSION_EXPIRATION ? "(session expired)" : "");
+    }
+
+    public String cssClass() {
+        return "sessionLogEntry-" + iconName();
+    }
+
+    public String iconName() {
+        return getLogoutTimestamp() == null
+                ? "login"
+                :getCausedBy() != SessionLogService.CausedBy.SESSION_EXPIRATION
+                    ? "logout"
+                    : "expired";
+    }
+
+
+
+    @Property(
+            domainEvent = SessionId.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = SessionId.MAX_LENGTH
+    )
+    @PropertyLayout(
+            fieldSetId="identity",
+            hidden = Where.PARENTED_TABLES,
+            sequence = "1"
+    )
+    @Parameter(
+            maxLength = SessionId.MAX_LENGTH
+    )
+    @ParameterLayout(
+            named = "Session Id"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface SessionId {
+        int MAX_LENGTH = 15;
+
+        class DomainEvent extends PropertyDomainEvent<String> {}
+    }
+
+    @PrimaryKey
+    @Column(allowsNull="false", length=15)
+    private String sessionId;
+
+    @SessionId
+    public String getSessionId() {
+        return sessionId;
+    }
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
+
+
+
+
+    @Property(
+            domainEvent = Username.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = Username.MAX_LENGTH
+    )
+    @PropertyLayout(
+            fieldSetId="Identity",
+            hidden = Where.PARENTED_TABLES,
+            sequence = "2"
+    )
+    @Parameter(
+            maxLength = Username.MAX_LENGTH
+    )
+    @ParameterLayout(
+            named = "Username"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface Username {
+        int MAX_LENGTH = 120;
+
+        class DomainEvent extends PropertyDomainEvent<String> {}
+    }
+
+    @Column(allowsNull = "false", length = Username.MAX_LENGTH)
+    private String username;
+
+    @Username
+    public String getUsername() {
+        return username;
+    }
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+
+
+
+    @Property(
+            domainEvent = LoginTimestamp.DomainEvent.class,
+            editing = Editing.DISABLED
+    )
+    @PropertyLayout(
+            fieldSetId="Identity",
+            hidden = Where.PARENTED_TABLES,
+            sequence = "3"
+    )
+    @ParameterLayout(
+            named = "Login timestamp"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface LoginTimestamp {
+        class DomainEvent extends PropertyDomainEvent<String> {}
+    }
+
+    @Column(allowsNull="false")
+    private Timestamp loginTimestamp;
+
+    @LoginTimestamp
+    public Timestamp getLoginTimestamp() {
+        return loginTimestamp;
+    }
+
+    public void setLoginTimestamp(Timestamp loginTimestamp) {
+        this.loginTimestamp = loginTimestamp;
+    }
+
+
+
+    @Property(
+            domainEvent = LogoutTimestamp.DomainEvent.class,
+            editing = Editing.DISABLED,
+            optionality = Optionality.OPTIONAL
+    )
+    @PropertyLayout(
+            fieldSetId="Identity",
+            hidden = Where.PARENTED_TABLES,
+            sequence = "3"
+    )
+    @Parameter(
+            optionality = Optionality.OPTIONAL
+    )
+    @ParameterLayout(
+            named = "Logout timestamp"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface LogoutTimestamp {
+        class DomainEvent extends PropertyDomainEvent<String> {}
+    }
+
+
+    @Column(allowsNull="true")
+    private Timestamp logoutTimestamp;
+
+    @LogoutTimestamp
+    public Timestamp getLogoutTimestamp() {
+        return logoutTimestamp;
+    }
+
+    public void setLogoutTimestamp(Timestamp logoutTimestamp) {
+        this.logoutTimestamp = logoutTimestamp;
+    }
+
+
+
+
+
+    @Property(
+            domainEvent = CausedBy.DomainEvent.class,
+            editing = Editing.DISABLED
+    )
+    @PropertyLayout(
+            fieldSetId="Details",
+            sequence = "1"
+    )
+    @ParameterLayout(
+            named = "Caused by"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface CausedBy {
+        class DomainEvent extends PropertyDomainEvent<String> {}
+
+    }
+
+    @Column(allowsNull = "false")
+    private SessionLogService.CausedBy causedBy;
+
+    @CausedBy
+    public SessionLogService.CausedBy getCausedBy() {
+        return causedBy;
+    }
+
+    public void setCausedBy(SessionLogService.CausedBy causedBy) {
+        this.causedBy = causedBy;
+    }
+
+
+
+
+    @Action(
+            domainEvent = next.DomainEvent.class,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(
+            cssClassFa = "fa-step-forward",
+            cssClassFaPosition = CssClassFaPosition.RIGHT
+    )
+    public class next {
+
+        public class DomainEvent extends ActionDomainEvent {
+        }
+
+        @MemberSupport public SessionLogEntry act() {
+            final List<SessionLogEntry> after = sessionLogEntryRepository.findByUserAndStrictlyAfter(getUsername(), getLoginTimestamp());
+            return !after.isEmpty() ? after.get(0) : SessionLogEntry.this;
+        }
+
+        @MemberSupport public String disableNext() {
+            val next = factoryService.mixin(next.class, SessionLogEntry.this);
+            return next.act() == SessionLogEntry.this ? "None after": null;
+        }
+
+        @Inject FactoryService factoryService;
+    }
+
+
+
+    @Action(
+            domainEvent = previous.DomainEvent.class,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(
+            cssClassFa = "fa-step-backward",
+            cssClassFaPosition = CssClassFaPosition.RIGHT
+    )
+    public class previous {
+
+        public class DomainEvent extends ActionDomainEvent {
+        }
+
+        @MemberSupport public SessionLogEntry act() {
+            final List<SessionLogEntry> before = sessionLogEntryRepository.findByUserAndStrictlyBefore(getUsername(), getLoginTimestamp());
+            return !before.isEmpty() ? before.get(0) : SessionLogEntry.this;
+        }
+
+        @MemberSupport public String disablePrevious() {
+            val previous = factoryService.mixin(previous.class, SessionLogEntry.this);
+            return previous.act() == SessionLogEntry.this ? "None before": null;
+        }
+
+        @Inject FactoryService factoryService;
+    }
+
+
+
+    private static final ObjectContracts.ObjectContract<SessionLogEntry> contract	=
+            ObjectContracts.contract(SessionLogEntry.class)
+                    .thenUse("loginTimestamp", SessionLogEntry::getLoginTimestamp)
+                    .thenUse("username", SessionLogEntry::getUsername)
+                    .thenUse("sessionId", SessionLogEntry::getSessionId)
+                    .thenUse("logoutTimestamp", SessionLogEntry::getLogoutTimestamp)
+                    .thenUse("causedBy", SessionLogEntry::getCausedBy)
+            ;
+
+
+    @Override
+    public String toString() {
+        return contract.toString(SessionLogEntry.this);
+    }
+
+    @Override
+    public int compareTo(final SessionLogEntry other) {
+        return contract.compare(this,other);
+    }
+
+
+    @Inject SessionLogEntryRepository sessionLogEntryRepository;
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.layout.fallback.xml b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.layout.fallback.xml
new file mode 100644
index 0000000000..92cbd65e72
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.layout.fallback.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<bs3:grid xsi:schemaLocation="http://isis.apache.org/applib/layout/component http://isis.apache.org/applib/layout/component/component.xsd http://isis.apache.org/applib/layout/links http://isis.apache.org/applib/layout/links/links.xsd http://isis.apache.org/applib/layout/grid/bootstrap3 http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="http://isis.apache.org/applib/layout/component" xmlns:lnk="h [...]
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <cpt:domainObject/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="6">
+            <bs3:tabGroup>
+                <bs3:tab name="Identifier">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:fieldSet name="Identifiers" id="identifiers" unreferencedProperties="true">
+                                <cpt:action id="previous"/>
+                                <cpt:action id="next"/>
+                                <cpt:property id="username"/>
+                                <cpt:property id="loginTimestamp"/>
+                            </cpt:fieldSet>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+                <bs3:tab name="Metadata">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:fieldSet name="Metadata" id="metadata">
+                                <cpt:action id="recentAuditEntries" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="findChangesByDate" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="recentChanges" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="clearHints" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="downloadJdoMetadata" position="PANEL_DROPDOWN"/>
+                                <cpt:action id="rebuildMetamodel" position="PANEL_DROPDOWN"/>
+                                <cpt:property id="sessionId"/>
+                            </cpt:fieldSet>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+            </bs3:tabGroup>
+        </bs3:col>
+        <bs3:col span="6">
+            <bs3:tabGroup collapseIfOne="false">
+                <bs3:tab name="Logout">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:fieldSet name="Timestamps" id="logout">
+                                <cpt:property id="loginTimestamp"/>
+                                <cpt:property id="logoutTimestamp"/>
+                                <cpt:property id="causedBy2"/>
+                            </cpt:fieldSet>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+            </bs3:tabGroup>
+        </bs3:col>
+        <bs3:col span="12" unreferencedCollections="true"/>
+    </bs3:row>
+</bs3:grid>
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntryRepository.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntryRepository.java
new file mode 100644
index 0000000000..6b5078507c
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntryRepository.java
@@ -0,0 +1,163 @@
+package org.apache.isis.sessionlog.jdo.dom;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.joda.time.LocalDate;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.query.Query;
+import org.apache.isis.applib.services.repository.RepositoryService;
+import org.apache.isis.applib.services.session.SessionLogService;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+/**
+ * Provides supporting functionality for querying {@link SessionLogEntry session log entry} entities.
+ */
+@Service
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class SessionLogEntryRepository {
+
+    final RepositoryService repositoryService;
+
+    public void logoutAllSessions(final Timestamp logoutTimestamp) {
+
+        val allSessions = repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_ACTIVE_SESSIONS));
+        for (val activeEntry : allSessions) {
+            activeEntry.setCausedBy(SessionLogService.CausedBy.RESTART);
+            activeEntry.setLogoutTimestamp(logoutTimestamp);
+        }
+    }
+
+    public SessionLogEntry create(
+            final String username,
+            final String sessionId,
+            final SessionLogService.CausedBy causedBy,
+            final Timestamp timestamp) {
+        return repositoryService.persistAndFlush(new SessionLogEntry(sessionId, username, causedBy, timestamp));
+    }
+
+    public Optional<SessionLogEntry> findBySessionId(final String sessionId) {
+        return repositoryService.firstMatch(
+                Query.named(SessionLogEntry.class,  SessionLogEntry.Nq.FIND_BY_SESSION_ID)
+                     .withParameter("sessionId", sessionId));
+    }
+
+
+    public List<SessionLogEntry> findByUser(final String username) {
+        return repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, "findByUsername")
+                     .withParameter("username", username));
+    }
+
+
+    public List<SessionLogEntry> findByUserAndFromAndTo(
+            final String user,
+            final LocalDate from,
+            final LocalDate to) {
+        val fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        val toTs = toTimestampStartOfDayWithOffset(to, 1);
+
+        final Query<SessionLogEntry> query;
+        if(from != null) {
+            if(to != null) {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_BETWEEN)
+                        .withParameter("user", user)
+                        .withParameter("from", fromTs)
+                        .withParameter("to", toTs);
+            } else {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_AFTER)
+                        .withParameter("user", user)
+                        .withParameter("from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_BEFORE)
+                        .withParameter("user", user)
+                        .withParameter("to", toTs);
+            } else {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER)
+                        .withParameter("user", user);
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+
+    public List<SessionLogEntry> findByFromAndTo(
+            final LocalDate from,
+            final LocalDate to) {
+        val fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        val toTs = toTimestampStartOfDayWithOffset(to, 1);
+
+        final Query<SessionLogEntry> query;
+        if(from != null) {
+            if(to != null) {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_TIMESTAMP_BETWEEN)
+                        .withParameter("from", fromTs)
+                        .withParameter("to", toTs);
+            } else {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_TIMESTAMP_AFTER)
+                        .withParameter("from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_TIMESTAMP_BEFORE)
+                        .withParameter("to", toTs);
+            } else {
+                query = Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND);
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+
+    public List<SessionLogEntry> findByUserAndStrictlyBefore(
+            final String user,
+            final Timestamp from) {
+
+        return repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_STRICTLY_BEFORE)
+                    .withParameter("user", user)
+                    .withParameter("from", from));
+    }
+
+
+    public List<SessionLogEntry> findByUserAndStrictlyAfter(
+            final String user,
+            final Timestamp from) {
+        return repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_BY_USER_AND_TIMESTAMP_STRICTLY_AFTER)
+                    .withParameter("user", user)
+                    .withParameter("from", from));
+    }
+
+
+
+    public List<SessionLogEntry> findActiveSessions() {
+        return repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_ACTIVE_SESSIONS));
+    }
+
+
+
+    public List<SessionLogEntry> findRecentByUser(final String user) {
+        return repositoryService.allMatches(
+                Query.named(SessionLogEntry.class, SessionLogEntry.Nq.FIND_RECENT_BY_USER)
+                        .withParameter("user", user));
+
+    }
+
+    private static Timestamp toTimestampStartOfDayWithOffset(final LocalDate dt, final int daysOffset) {
+        return dt!=null
+                ?new Timestamp(dt.toDateTimeAtStartOfDay().plusDays(daysOffset).getMillis())
+                :null;
+    }
+
+}
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/spiimpl/SessionLoggingServiceDefault.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/spiimpl/SessionLoggingServiceDefault.java
new file mode 100644
index 0000000000..59fa08dfce
--- /dev/null
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/spiimpl/SessionLoggingServiceDefault.java
@@ -0,0 +1,55 @@
+package org.apache.isis.sessionlog.jdo.spiimpl;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.services.clock.ClockService;
+import org.apache.isis.applib.services.session.SessionLogService;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntry;
+import org.apache.isis.sessionlog.jdo.dom.SessionLogEntryRepository;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Date;
+
+import org.springframework.stereotype.Service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+/**
+ * Implementation of the Isis {@link SessionLogService} creates a log
+ * entry to the database (the {@link SessionLogEntry} entity) each time a
+ * user either logs on or logs out, or if their session expires.
+ */
+@Service
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class SessionLoggingServiceDefault implements SessionLogService {
+
+    final SessionLogEntryRepository sessionLogEntryRepository;
+    final ClockService clockService;
+
+    @PostConstruct
+    public void init() {
+        val timestamp = clockService.getClock().nowAsJavaSqlTimestamp();
+        sessionLogEntryRepository.logoutAllSessions(timestamp);
+    }
+
+    @Programmatic
+    @Override
+    public void log(final Type type, final String username, final Date date, final CausedBy causedBy, final String sessionId) {
+        val timestamp = clockService.getClock().nowAsJavaSqlTimestamp();
+        if (type == Type.LOGIN) {
+            sessionLogEntryRepository.create(username, sessionId, causedBy, timestamp);
+        } else {
+            sessionLogEntryRepository.findBySessionId(sessionId)
+                    .ifPresent(entry -> {
+                        entry.setLogoutTimestamp(timestamp);
+                        entry.setCausedBy(causedBy);
+                    }
+            );
+        }
+    }
+
+}
diff --git a/extensions/security/sessionlog/pom.xml b/extensions/security/sessionlog/pom.xml
new file mode 100644
index 0000000000..8ba18f30f7
--- /dev/null
+++ b/extensions/security/sessionlog/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.isis.extensions</groupId>
+        <artifactId>isis-extensions</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>isis-extensions-sessionlog</artifactId>
+    <name>Apache Isis Ext - Session Log</name>
+
+    <packaging>pom</packaging>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.isis.extensions.sessionlog</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/isis/extensions/sessionlog</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.isis.extensions</groupId>
+                <artifactId>isis-extensions-sessionlog-applib</artifactId>
+                <version>2.0.0-SNAPSHOT</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.isis.extensions</groupId>
+                <artifactId>sis-extensions-sessionlog-persistence-jpa</artifactId>
+                <version>2.0.0-SNAPSHOT</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.isis.extensions</groupId>
+                <artifactId>sis-extensions-sessionlog-persistence-jdo</artifactId>
+                <version>2.0.0-SNAPSHOT</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.isis.testing</groupId>
+                <artifactId>isis-testing-integtestsupport</artifactId>
+                <version>2.0.0-SNAPSHOT</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+
+		<dependency>
+			<groupId>org.apache.isis.core</groupId>
+			<artifactId>isis-core-metamodel</artifactId>
+		</dependency>
+
+    </dependencies>
+
+    <modules>
+    	<module>applib</module>
+    	<module>persistence-jdo</module>
+<!--
+    	<module>persistence-jpa</module>
+-->
+    </modules>
+</project>
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis.java
index 223f7a2f41..49cfc338b5 100644
--- a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis.java
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis.java
@@ -28,7 +28,7 @@ import org.apache.isis.applib.clock.VirtualClock;
 import org.apache.isis.applib.services.clock.ClockService;
 import org.apache.isis.applib.services.iactnlayer.InteractionContext;
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
-import org.apache.isis.applib.services.session.SessionLoggingService;
+import org.apache.isis.applib.services.session.SessionLogService;
 import org.apache.isis.applib.services.user.ImpersonatedUserHolder;
 import org.apache.isis.applib.services.user.UserMemento;
 import org.apache.isis.applib.services.user.UserMemento.AuthenticationSource;
@@ -88,7 +88,7 @@ implements BreadcrumbModelProvider, BookmarkedPagesModelProvider, HasCommonConte
         authenticationRequest.addRole(UserMemento.AUTHORIZED_USER_ROLE);
         this.authentication = getAuthenticationManager().authenticate(authenticationRequest);
         if (this.authentication != null) {
-            log(SessionLoggingService.Type.LOGIN, username, null);
+            log(SessionLogService.Type.LOGIN, username, null);
             return true;
         } else {
             return false;
@@ -124,11 +124,11 @@ implements BreadcrumbModelProvider, BookmarkedPagesModelProvider, HasCommonConte
         super.onInvalidate();
 
         val causedBy = RequestCycle.get() != null
-                ? SessionLoggingService.CausedBy.USER
-                : SessionLoggingService.CausedBy.SESSION_EXPIRATION;
+                ? SessionLogService.CausedBy.USER
+                : SessionLogService.CausedBy.SESSION_EXPIRATION;
 
 
-        log(SessionLoggingService.Type.LOGOUT, userName, causedBy);
+        log(SessionLogService.Type.LOGOUT, userName, causedBy);
     }
 
     /**
@@ -223,9 +223,9 @@ implements BreadcrumbModelProvider, BookmarkedPagesModelProvider, HasCommonConte
     }
 
     private void log(
-            final SessionLoggingService.Type type,
+            final SessionLogService.Type type,
             final String username,
-            final SessionLoggingService.CausedBy causedBy) {
+            final SessionLogService.CausedBy causedBy) {
 
 
         val interactionFactory = getInteractionService();
@@ -249,8 +249,8 @@ implements BreadcrumbModelProvider, BookmarkedPagesModelProvider, HasCommonConte
         }
     }
 
-    protected Can<SessionLoggingService> getSessionLoggingServices() {
-        return commonContext.getServiceRegistry().select(SessionLoggingService.class);
+    protected Can<SessionLogService> getSessionLoggingServices() {
+        return commonContext.getServiceRegistry().select(SessionLogService.class);
     }
 
     protected InteractionService getInteractionService() {
diff --git a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_Authenticate.java b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_Authenticate.java
index c1bab48667..f9990c8894 100644
--- a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_Authenticate.java
+++ b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_Authenticate.java
@@ -37,7 +37,7 @@ import static org.hamcrest.Matchers.nullValue;
 import org.apache.isis.applib.services.iactnlayer.InteractionLayerTracker;
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.applib.services.session.SessionLoggingService;
+import org.apache.isis.applib.services.session.SessionLogService;
 import org.apache.isis.applib.services.user.ImpersonatedUserHolder;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.functional.ThrowingRunnable;
@@ -83,7 +83,7 @@ public class AuthenticatedWebSessionForIsis_Authenticate {
                 allowing(mockCommonContext).getServiceRegistry();
                 will(returnValue(mockServiceRegistry));
 
-                allowing(mockServiceRegistry).select(SessionLoggingService.class);
+                allowing(mockServiceRegistry).select(SessionLogService.class);
                 will(returnValue(Can.empty()));
 
                 allowing(mockCommonContext).lookupServiceElseFail(InteractionService.class);
diff --git a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_SignIn.java b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_SignIn.java
index 5477562dc0..471af28755 100644
--- a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_SignIn.java
+++ b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_SignIn.java
@@ -35,7 +35,7 @@ import static org.hamcrest.Matchers.is;
 
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.applib.services.session.SessionLoggingService;
+import org.apache.isis.applib.services.session.SessionLogService;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.functional.ThrowingRunnable;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
@@ -80,7 +80,7 @@ public class AuthenticatedWebSessionForIsis_SignIn {
                 allowing(mockCommonContext).getServiceRegistry();
                 will(returnValue(mockServiceRegistry));
 
-                allowing(mockServiceRegistry).select(SessionLoggingService.class);
+                allowing(mockServiceRegistry).select(SessionLogService.class);
                 will(returnValue(Can.empty()));
 
                 allowing(mockCommonContext).lookupServiceElseFail(InteractionService.class);
diff --git a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_TestAbstract.java b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_TestAbstract.java
index 74cf1d99d6..088ba76301 100644
--- a/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_TestAbstract.java
+++ b/viewers/wicket/viewer/src/test/java/org/apache/isis/viewer/wicket/viewer/integration/AuthenticatedWebSessionForIsis_TestAbstract.java
@@ -28,7 +28,7 @@ import org.junit.Rule;
 
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.applib.services.session.SessionLoggingService;
+import org.apache.isis.applib.services.session.SessionLogService;
 import org.apache.isis.commons.functional.ThrowingRunnable;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2.Mode;
@@ -55,7 +55,7 @@ public abstract class AuthenticatedWebSessionForIsis_TestAbstract {
                 allowing(mockCommonContext).getServiceRegistry();
                 will(returnValue(mockServiceRegistry));
 
-                allowing(mockServiceRegistry).lookupService(SessionLoggingService.class);
+                allowing(mockServiceRegistry).lookupService(SessionLogService.class);
                 will(returnValue(Optional.empty()));
 
                 allowing(mockCommonContext).lookupServiceElseFail(InteractionService.class);


[isis] 02/02: ISIS-3062: introduces Nq utility class, also for secman

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-3062
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 9487682bc0dab524278a5c8de2ded4048d127286
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon May 23 19:02:56 2022 +0100

    ISIS-3062: introduces Nq utility class, also for secman
---
 .../permission/dom/ApplicationPermission.java      | 15 +++++++-----
 .../ApplicationPermissionRepositoryAbstract.java   | 12 +++++-----
 .../secman/applib/role/dom/ApplicationRole.java    |  9 +++++--
 .../dom/ApplicationRoleRepositoryAbstract.java     |  4 ++--
 .../applib/tenancy/dom/ApplicationTenancy.java     | 11 ++++++---
 .../dom/ApplicationTenancyRepositoryAbstract.java  |  6 ++---
 .../secman/applib/user/dom/ApplicationUser.java    | 14 ++++++-----
 .../dom/ApplicationUserRepositoryAbstract.java     |  8 +++----
 .../jdo/permission/dom/ApplicationPermission.java  | 13 +++++-----
 .../secman/jdo/role/dom/ApplicationRole.java       |  4 ++--
 .../secman/jdo/tenancy/dom/ApplicationTenancy.java |  6 ++---
 .../secman/jdo/user/dom/ApplicationUser.java       |  8 +++----
 .../jpa/permission/dom/ApplicationPermission.java  | 12 +++++-----
 .../secman/jpa/role/dom/ApplicationRole.java       |  4 ++--
 .../secman/jpa/tenancy/dom/ApplicationTenancy.java |  6 ++---
 .../secman/jpa/user/dom/ApplicationUser.java       |  8 +++----
 .../isis/sessionlog/jdo/dom/SessionLogEntry.java   | 28 +++++++++++-----------
 17 files changed, 92 insertions(+), 76 deletions(-)

diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
index f284451c39..7a2d928110 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermission.java
@@ -89,12 +89,15 @@ public abstract class ApplicationPermission implements Comparable<ApplicationPer
     public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
     public static final String TABLE = "ApplicationPermission";
 
-    public static final String NAMED_QUERY_FIND_BY_FEATURE = "ApplicationPermission.findByFeature";
-    public static final String NAMED_QUERY_FIND_BY_ROLE = "ApplicationPermission.findByRole";
-    public static final String NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE = "ApplicationPermission.findByRoleAndRuleAndFeature";
-    public static final String NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE_FQN = "ApplicationPermission.findByRoleAndRuleAndFeatureAndFqn";
-    public static final String NAMED_QUERY_FIND_BY_USER = "ApplicationPermission.findByUser";
-    public static final String NAMED_QUERY_FIND_BY_ROLE_NAMES = "ApplicationPermission.findByRoleNames";
+    @UtilityClass
+    public static class Nq {
+        public static final String FIND_BY_FEATURE = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByFeature";
+        public static final String FIND_BY_ROLE = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByRole";
+        public static final String FIND_BY_ROLE_RULE_FEATURE = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByRoleAndRuleAndFeature";
+        public static final String FIND_BY_ROLE_RULE_FEATURE_FQN = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByRoleAndRuleAndFeatureAndFqn";
+        public static final String FIND_BY_USER = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByUser";
+        public static final String FIND_BY_ROLE_NAMES = ApplicationPermission.LOGICAL_TYPE_NAME + ".findByRoleNames";
+    }
 
 
     @Inject transient ApplicationFeatureRepository featureRepository;
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepositoryAbstract.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepositoryAbstract.java
index b56f0a5200..4035605680 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepositoryAbstract.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepositoryAbstract.java
@@ -80,7 +80,7 @@ implements ApplicationPermissionRepository {
     public List<ApplicationPermission> findByRole(final @NonNull ApplicationRole role) {
         return _Casts.uncheckedCast(
                 repository.allMatches(
-                Query.named(this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE)
+                Query.named(this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_ROLE)
                     .withParameter("role", role))
         );
     }
@@ -106,7 +106,7 @@ implements ApplicationPermissionRepository {
     public List<ApplicationPermission> findByRoleNames(final @NonNull List<String> roleNames) {
         return _Casts.uncheckedCast(
                 repository.allMatches(
-                        Query.named(this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_NAMES)
+                        Query.named(this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_ROLE_NAMES)
                                 .withParameter("roleNames", roleNames))
         );
     }
@@ -114,7 +114,7 @@ implements ApplicationPermissionRepository {
     private List<ApplicationPermission> findByUser(final String username) {
         return _Casts.uncheckedCast(
                 repository.allMatches(
-                Query.named(this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_USER)
+                Query.named(this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_USER)
                     .withParameter("username", username))
         );
     }
@@ -179,7 +179,7 @@ implements ApplicationPermissionRepository {
             final ApplicationPermissionRule rule,
             final ApplicationFeatureSort featureSort) {
         return repository.allMatches(Query.named(
-                        this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE)
+                        this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_ROLE_RULE_FEATURE)
                     .withParameter("role", role)
                     .withParameter("rule", rule)
                     .withParameter("featureSort", featureSort))
@@ -210,7 +210,7 @@ implements ApplicationPermissionRepository {
         return _Casts.uncheckedCast(
                 repository
                 .uniqueMatch(Query.named(
-                                this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE_FQN)
+                                this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_ROLE_RULE_FEATURE_FQN)
                         .withParameter("role", role)
                         .withParameter("rule", rule)
                         .withParameter("featureSort", featureSort)
@@ -231,7 +231,7 @@ implements ApplicationPermissionRepository {
     public Collection<ApplicationPermission> findByFeature(final ApplicationFeatureId featureId) {
         return repository.allMatches(
                 Query.named(
-                        this.applicationPermissionClass, ApplicationPermission.NAMED_QUERY_FIND_BY_FEATURE)
+                        this.applicationPermissionClass, ApplicationPermission.Nq.FIND_BY_FEATURE)
                 .withParameter("featureSort", featureId.getSort())
                 .withParameter("featureFqn", featureId.getFullyQualifiedName()))
                 .stream()
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
index c408a4d868..7ca7df7207 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRole.java
@@ -47,6 +47,8 @@ import org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermis
 import org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermissionRepository;
 import org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser;
 
+import lombok.experimental.UtilityClass;
+
 /**
  * @since 2.0 {@index}
  */
@@ -59,8 +61,11 @@ public abstract class ApplicationRole implements Comparable<ApplicationRole> {
     public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
     public static final String TABLE = "ApplicationRole";
 
-    public static final String NAMED_QUERY_FIND_BY_NAME = "ApplicationRole.findByName";
-    public static final String NAMED_QUERY_FIND_BY_NAME_CONTAINING = "ApplicationRole.findByNameContaining";
+    @UtilityClass
+    public static class Nq {
+        public static final String FIND_BY_NAME = ApplicationRole.LOGICAL_TYPE_NAME + ".findByName";
+        public static final String FIND_BY_NAME_CONTAINING = ApplicationRole.LOGICAL_TYPE_NAME + ".findByNameContaining";
+    }
 
     @Inject transient private ApplicationPermissionRepository applicationPermissionRepository;
 
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRoleRepositoryAbstract.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRoleRepositoryAbstract.java
index 60b96b766a..434fb3076f 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRoleRepositoryAbstract.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/role/dom/ApplicationRoleRepositoryAbstract.java
@@ -83,7 +83,7 @@ implements ApplicationRoleRepository {
             return Optional.empty();
         }
         return _Casts.uncheckedCast(
-                repository.uniqueMatch(Query.named(applicationRoleClass, ApplicationRole.NAMED_QUERY_FIND_BY_NAME)
+                repository.uniqueMatch(Query.named(applicationRoleClass, ApplicationRole.Nq.FIND_BY_NAME)
                 .withParameter("name", name))
         );
     }
@@ -94,7 +94,7 @@ implements ApplicationRoleRepository {
         if(search != null && search.length() > 0) {
             val nameRegex = regexReplacer.asRegex(search);
             return repository.allMatches(
-                    Query.named(applicationRoleClass, ApplicationRole.NAMED_QUERY_FIND_BY_NAME_CONTAINING)
+                    Query.named(applicationRoleClass, ApplicationRole.Nq.FIND_BY_NAME_CONTAINING)
                     .withParameter("regex", nameRegex))
                     .stream()
                     .collect(_Sets.toUnmodifiableSorted());
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
index 264659e6ab..026ce820ab 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancy.java
@@ -41,6 +41,8 @@ import org.apache.isis.applib.util.ObjectContracts;
 import org.apache.isis.applib.util.ToString;
 import org.apache.isis.extensions.secman.applib.IsisModuleExtSecmanApplib;
 
+import lombok.experimental.UtilityClass;
+
 /**
  * @since 2.0 {@index}
  */
@@ -53,9 +55,12 @@ public abstract class ApplicationTenancy implements Comparable<ApplicationTenanc
     public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
     public static final String TABLE = "ApplicationTenancy";
 
-    public static final String NAMED_QUERY_FIND_BY_NAME = "ApplicationTenancy.findByName";
-    public static final String NAMED_QUERY_FIND_BY_PATH = "ApplicationTenancy.findByPath";
-    public static final String NAMED_QUERY_FIND_BY_NAME_OR_PATH_MATCHING = "ApplicationTenancy.findByNameOrPathMatching";
+    @UtilityClass
+    public static class Nq {
+        public static final String FIND_BY_NAME = ApplicationTenancy.LOGICAL_TYPE_NAME + ".findByName";
+        public static final String FIND_BY_PATH = ApplicationTenancy.LOGICAL_TYPE_NAME + ".findByPath";
+        public static final String FIND_BY_NAME_OR_PATH_MATCHING = ApplicationTenancy.LOGICAL_TYPE_NAME + ".findByNameOrPathMatching";
+    }
 
 
     // -- DOMAIN EVENTS
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancyRepositoryAbstract.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancyRepositoryAbstract.java
index 81381b6e95..bc441a40a4 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancyRepositoryAbstract.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/tenancy/dom/ApplicationTenancyRepositoryAbstract.java
@@ -70,7 +70,7 @@ implements ApplicationTenancyRepository {
             return Collections.emptySortedSet();
         }
         val regex = regexReplacer.asRegex(search);
-        return repository.allMatches(Query.named(this.applicationTenancyClass, ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME_OR_PATH_MATCHING)
+        return repository.allMatches(Query.named(this.applicationTenancyClass, ApplicationTenancy.Nq.FIND_BY_NAME_OR_PATH_MATCHING)
                 .withParameter("regex", regex))
                 .stream()
                 .collect(_Sets.toUnmodifiableSorted());
@@ -86,7 +86,7 @@ implements ApplicationTenancyRepository {
     }
 
     public ApplicationTenancy findByName(final String name) {
-        return repository.uniqueMatch(Query.named(this.applicationTenancyClass, ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME)
+        return repository.uniqueMatch(Query.named(this.applicationTenancyClass, ApplicationTenancy.Nq.FIND_BY_NAME)
                 .withParameter("name", name)).orElse(null);
     }
 
@@ -105,7 +105,7 @@ implements ApplicationTenancyRepository {
         if (path == null) {
             return null;
         }
-        return repository.uniqueMatch(Query.named(this.applicationTenancyClass, ApplicationTenancy.NAMED_QUERY_FIND_BY_PATH)
+        return repository.uniqueMatch(Query.named(this.applicationTenancyClass, ApplicationTenancy.Nq.FIND_BY_PATH)
                 .withParameter("path", path))
                 .orElse(null);
     }
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
index 27c12b0393..d692c6f5fa 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUser.java
@@ -63,6 +63,7 @@ import org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole;
 import org.apache.isis.extensions.secman.applib.tenancy.dom.HasAtPath;
 
 import lombok.val;
+import lombok.experimental.UtilityClass;
 
 /**
  * @since 2.0 {@index}
@@ -77,6 +78,13 @@ public abstract class ApplicationUser
     public static final String SCHEMA = IsisModuleExtSecmanApplib.SCHEMA;
     public static final String TABLE = "ApplicationUser";
 
+    @UtilityClass
+    public static class Nq {
+        public static final String FIND_BY_USERNAME = ApplicationUser.LOGICAL_TYPE_NAME + ".findByUsername";
+        public static final String FIND_BY_EMAIL_ADDRESS = ApplicationUser.LOGICAL_TYPE_NAME + ".findByEmailAddress";
+        public static final String FIND = ApplicationUser.LOGICAL_TYPE_NAME + ".find";
+        public static final String FIND_BY_ATPATH = ApplicationUser.LOGICAL_TYPE_NAME + ".findByAtPath";
+    }
 
     @Inject private transient ApplicationUserRepository applicationUserRepository;
     @Inject private transient ApplicationPermissionRepository applicationPermissionRepository;
@@ -109,12 +117,6 @@ public abstract class ApplicationUser
         return config.getExtensions().getSecman();
     }
 
-    // -- CONSTANTS
-
-    public static final String NAMED_QUERY_FIND_BY_USERNAME = "ApplicationUser.findByUsername";
-    public static final String NAMED_QUERY_FIND_BY_EMAIL_ADDRESS = "ApplicationUser.findByEmailAddress";
-    public static final String NAMED_QUERY_FIND = "ApplicationUser.find";
-    public static final String NAMED_QUERY_FIND_BY_ATPATH = "ApplicationUser.findByAtPath";
 
     // -- DOMAIN EVENTS
 
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUserRepositoryAbstract.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUserRepositoryAbstract.java
index c89325853f..e8e0664c43 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUserRepositoryAbstract.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/user/dom/ApplicationUserRepositoryAbstract.java
@@ -104,7 +104,7 @@ implements ApplicationUserRepository {
     @Override
     public Optional<ApplicationUser> findByUsername(final String username) {
         return _Casts.uncheckedCast(
-                repository.uniqueMatch(Query.named(this.applicationUserClass, ApplicationUser.NAMED_QUERY_FIND_BY_USERNAME)
+                repository.uniqueMatch(Query.named(this.applicationUserClass, ApplicationUser.Nq.FIND_BY_USERNAME)
                 .withParameter("username", username))
         );
     }
@@ -119,7 +119,7 @@ implements ApplicationUserRepository {
     @Override
     public Optional<ApplicationUser> findByEmailAddress(final String emailAddress) {
         return _Casts.uncheckedCast(
-                repository.uniqueMatch(Query.named(this.applicationUserClass, ApplicationUser.NAMED_QUERY_FIND_BY_EMAIL_ADDRESS)
+                repository.uniqueMatch(Query.named(this.applicationUserClass, ApplicationUser.Nq.FIND_BY_EMAIL_ADDRESS)
                 .withParameter("emailAddress", emailAddress))
         );
     }
@@ -129,7 +129,7 @@ implements ApplicationUserRepository {
     @Override
     public Collection<ApplicationUser> find(final @Nullable String _search) {
         val regex = regexReplacer.asRegex(_search);
-        return repository.allMatches(Query.named(this.applicationUserClass, ApplicationUser.NAMED_QUERY_FIND)
+        return repository.allMatches(Query.named(this.applicationUserClass, ApplicationUser.Nq.FIND)
                 .withParameter("regex", regex))
                 .stream()
                 .collect(_Sets.toUnmodifiableSorted());
@@ -140,7 +140,7 @@ implements ApplicationUserRepository {
 
     @Override
     public Collection<ApplicationUser> findByAtPath(final String atPath) {
-        return repository.allMatches(Query.named(this.applicationUserClass, ApplicationUser.NAMED_QUERY_FIND_BY_ATPATH)
+        return repository.allMatches(Query.named(this.applicationUserClass, ApplicationUser.Nq.FIND_BY_ATPATH)
                 .withParameter("atPath", atPath))
                 .stream()
                 .collect(_Sets.toUnmodifiableSorted());
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
index 0a43517161..7059e8d12a 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/permission/dom/ApplicationPermission.java
@@ -38,6 +38,7 @@ import org.apache.isis.applib.annotation.DomainObjectLayout;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.services.appfeat.ApplicationFeatureSort;
 import org.apache.isis.commons.internal.base._Casts;
+import org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq;
 import org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermissionMode;
 import org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermissionRule;
 import org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole;
@@ -55,29 +56,29 @@ import org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole;
         column = "version")
 @Queries( {
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE,
+            name = Nq.FIND_BY_ROLE,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE role == :role"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_USER,
+            name = Nq.FIND_BY_USER,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE (u.roles.contains(role) && u.username == :username) "
                     + "VARIABLES org.apache.isis.extensions.secman.jdo.user.dom.ApplicationUser u"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_NAMES,
+            name = Nq.FIND_BY_ROLE_NAMES,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE :roleNames.contains(role.name) "),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_FEATURE,
+            name = Nq.FIND_BY_FEATURE,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE featureSort == :featureSort "
                     + "   && featureFqn == :featureFqn"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE_FQN,
+            name = Nq.FIND_BY_ROLE_RULE_FEATURE_FQN,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE role == :role "
@@ -85,7 +86,7 @@ import org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole;
                     + "   && featureSort == :featureSort "
                     + "   && featureFqn == :featureFqn "),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE,
+            name = Nq.FIND_BY_ROLE_RULE_FEATURE,
             value = "SELECT "
                     + "FROM " + ApplicationPermission.FQCN
                     + " WHERE role == :role "
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
index 7e511239a8..40c92d7047 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/role/dom/ApplicationRole.java
@@ -56,12 +56,12 @@ import org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser;
 })
 @Queries({
     @Query(
-            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.NAMED_QUERY_FIND_BY_NAME,
+            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.Nq.FIND_BY_NAME,
             value = "SELECT "
                     + "FROM " + ApplicationRole.FQCN
                     + " WHERE name == :name"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.NAMED_QUERY_FIND_BY_NAME_CONTAINING,
+            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.Nq.FIND_BY_NAME_CONTAINING,
             value = "SELECT "
                     + "FROM " + ApplicationRole.FQCN
                     + " WHERE name.matches(:regex) ")
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
index 99263f0537..d3a4c2f9c6 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/tenancy/dom/ApplicationTenancy.java
@@ -60,17 +60,17 @@ import org.apache.isis.commons.internal.base._Casts;
 })
 @Queries( {
     @Query(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_PATH,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_PATH,
             value = "SELECT "
                     + "FROM " + ApplicationTenancy.FQCN
                     + " WHERE path == :path"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_NAME,
             value = "SELECT "
                     + "FROM " + ApplicationTenancy.FQCN
                     + " WHERE name == :name"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME_OR_PATH_MATCHING,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_NAME_OR_PATH_MATCHING,
             value = "SELECT "
                     + "FROM " + ApplicationTenancy.FQCN
                     + " WHERE name.matches(:regex) || path.matches(:regex) ")})
diff --git a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
index 519c8773a6..ffb017db28 100644
--- a/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
+++ b/extensions/security/secman/persistence-jdo/src/main/java/org/apache/isis/extensions/secman/jdo/user/dom/ApplicationUser.java
@@ -67,22 +67,22 @@ import lombok.Setter;
 })
 @Queries( {
     @Query(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_USERNAME,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.FIND_BY_USERNAME,
             value = "SELECT "
                     + "FROM " + ApplicationUser.FQCN
                     + " WHERE username == :username"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_EMAIL_ADDRESS,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.FIND_BY_EMAIL_ADDRESS,
             value = "SELECT "
                     + "FROM " + ApplicationUser.FQCN
                     + " WHERE emailAddress == :emailAddress"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_ATPATH,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.FIND_BY_ATPATH,
             value = "SELECT "
                     + "FROM " + ApplicationUser.FQCN
                     + " WHERE atPath == :atPath"),
     @Query(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.FIND,
             value = "SELECT "
                     + "FROM " + ApplicationUser.FQCN
                     + " WHERE username.matches(:regex)"
diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/permission/dom/ApplicationPermission.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/permission/dom/ApplicationPermission.java
index f53e04dc95..34a55e88b8 100644
--- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/permission/dom/ApplicationPermission.java
+++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/permission/dom/ApplicationPermission.java
@@ -55,31 +55,31 @@ import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
 )
 @NamedQueries({
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_ROLE,
             query = "SELECT p "
                   + "  FROM ApplicationPermission p "
                   + " WHERE p.role = :role"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_USER,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_USER,
             query = "SELECT perm "
                   + "FROM ApplicationPermission perm "
                   + "JOIN perm.role role "
                   + "JOIN role.users user "
                   + "WHERE user.username = :username"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_NAMES,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_ROLE_NAMES,
             query = "SELECT perm "
                   + "FROM ApplicationPermission perm "
                   + "JOIN perm.role role "
                   + "WHERE role.name IN :roleNames"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_FEATURE,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_FEATURE,
             query = "SELECT p "
                   + "  FROM ApplicationPermission p "
                   + " WHERE p.featureSort = :featureSort "
                   + "   AND p.featureFqn = :featureFqn"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE_FQN,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_ROLE_RULE_FEATURE_FQN,
             query = "SELECT p "
                   + "  FROM ApplicationPermission p "
                   + " WHERE p.role = :role "
@@ -87,7 +87,7 @@ import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
                   + "   AND p.featureSort = :featureSort "
                   + "   AND p.featureFqn = :featureFqn "),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.NAMED_QUERY_FIND_BY_ROLE_RULE_FEATURE,
+            name = org.apache.isis.extensions.secman.applib.permission.dom.ApplicationPermission.Nq.FIND_BY_ROLE_RULE_FEATURE,
             query = "SELECT p "
                   + "  FROM ApplicationPermission p "
                   + " WHERE p.role = :role "
diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/role/dom/ApplicationRole.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/role/dom/ApplicationRole.java
index 0155444c67..c2af986b04 100644
--- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/role/dom/ApplicationRole.java
+++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/role/dom/ApplicationRole.java
@@ -53,12 +53,12 @@ import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
 )
 @NamedQueries({
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.NAMED_QUERY_FIND_BY_NAME,
+            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.Nq.FIND_BY_NAME,
             query = "SELECT r "
                   + "FROM ApplicationRole r "
                   + "WHERE r.name = :name"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.NAMED_QUERY_FIND_BY_NAME_CONTAINING,
+            name = org.apache.isis.extensions.secman.applib.role.dom.ApplicationRole.Nq.FIND_BY_NAME_CONTAINING,
             query = "SELECT r "
                   + "FROM ApplicationRole r "
                   + "WHERE r.name LIKE :regex"),
diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java
index 1254d41d11..9638edf051 100644
--- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java
+++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/tenancy/dom/ApplicationTenancy.java
@@ -51,17 +51,17 @@ import org.apache.isis.commons.internal.base._Casts;
 )
 @NamedQueries({
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_PATH,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_PATH,
             query = "SELECT t "
                   + "  FROM ApplicationTenancy t "
                   + " WHERE t.path = :path"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_NAME,
             query = "SELECT t "
                   + "  FROM ApplicationTenancy t "
                   + " WHERE t.name = :name"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.NAMED_QUERY_FIND_BY_NAME_OR_PATH_MATCHING,
+            name = org.apache.isis.extensions.secman.applib.tenancy.dom.ApplicationTenancy.Nq.FIND_BY_NAME_OR_PATH_MATCHING,
             query = "SELECT t "
                   + "  FROM ApplicationTenancy t "
                   + " WHERE t.name LIKE :regex "
diff --git a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/user/dom/ApplicationUser.java b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/user/dom/ApplicationUser.java
index 724ffe16c9..5db24e9554 100644
--- a/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/user/dom/ApplicationUser.java
+++ b/extensions/security/secman/persistence-jpa/src/main/java/org/apache/isis/extensions/secman/jpa/user/dom/ApplicationUser.java
@@ -61,22 +61,22 @@ import lombok.Setter;
 )
 @NamedQueries({
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_USERNAME,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.NAMED_QUERY_FIND_BY_USERNAME,
             query = "SELECT u "
                   + "  FROM ApplicationUser u "
                   + " WHERE u.username = :username"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_EMAIL_ADDRESS,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.NAMED_QUERY_FIND_BY_EMAIL_ADDRESS,
             query = "SELECT u "
                   + "  FROM ApplicationUser u "
                   + " WHERE u.emailAddress = :emailAddress"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND_BY_ATPATH,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.NAMED_QUERY_FIND_BY_ATPATH,
             query = "SELECT u "
                   + "  FROM ApplicationUser u "
                   + " WHERE u.atPath = :atPath"),
     @NamedQuery(
-            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.NAMED_QUERY_FIND,
+            name = org.apache.isis.extensions.secman.applib.user.dom.ApplicationUser.Nq.NAMED_QUERY_FIND,
             query = "SELECT u "
                   + "  FROM ApplicationUser u "
                   + " WHERE u.username LIKE :regex"
diff --git a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java
index 0030d78472..b94397c6f6 100644
--- a/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java
+++ b/extensions/security/sessionlog/persistence-jdo/src/main/java/org/apache/isis/sessionlog/jdo/dom/SessionLogEntry.java
@@ -142,20 +142,20 @@ public class SessionLogEntry implements HasUsername, Comparable<SessionLogEntry>
     public static final String TABLE = "SessionLogEntry";
 
     @UtilityClass
-    static class Nq {
-        static final String FIND_BY_SESSION_ID = "findBySessionId";
-        static final String FIND_BY_USER_AND_TIMESTAMP_BETWEEN = "findByUserAndTimestampBetween";
-        static final String FIND_BY_USER_AND_TIMESTAMP_AFTER = "findByUserAndTimestampAfter";
-        static final String FIND_BY_USER_AND_TIMESTAMP_BEFORE = "findByUserAndTimestampBefore";
-        static final String FIND_BY_USER = "findByUser";
-        static final String FIND_BY_TIMESTAMP_BETWEEN = "findByTimestampBetween";
-        static final String FIND_BY_TIMESTAMP_AFTER = "findByTimestampAfter";
-        static final String FIND_BY_TIMESTAMP_BEFORE = "findByTimestampBefore";
-        static final String FIND = "find";
-        static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_BEFORE = "findByUserAndTimestampStrictlyBefore";
-        static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_AFTER = "findByUserAndTimestampStrictlyAfter";
-        static final String FIND_ACTIVE_SESSIONS = "findActiveSessions";
-        static final String FIND_RECENT_BY_USER = "findRecentByUser";
+    public static class Nq {
+        public static final String FIND_BY_SESSION_ID =  SessionLogEntry.LOGICAL_TYPE_NAME + ".findBySessionId";
+        public static final String FIND_BY_USER_AND_TIMESTAMP_BETWEEN = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUserAndTimestampBetween";
+        public static final String FIND_BY_USER_AND_TIMESTAMP_AFTER = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUserAndTimestampAfter";
+        public static final String FIND_BY_USER_AND_TIMESTAMP_BEFORE = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUserAndTimestampBefore";
+        public static final String FIND_BY_USER = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUser";
+        public static final String FIND_BY_TIMESTAMP_BETWEEN = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByTimestampBetween";
+        public static final String FIND_BY_TIMESTAMP_AFTER = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByTimestampAfter";
+        public static final String FIND_BY_TIMESTAMP_BEFORE = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByTimestampBefore";
+        public static final String FIND = SessionLogEntry.LOGICAL_TYPE_NAME + ".find";
+        public static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_BEFORE = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUserAndTimestampStrictlyBefore";
+        public static final String FIND_BY_USER_AND_TIMESTAMP_STRICTLY_AFTER = SessionLogEntry.LOGICAL_TYPE_NAME + ".findByUserAndTimestampStrictlyAfter";
+        public static final String FIND_ACTIVE_SESSIONS = SessionLogEntry.LOGICAL_TYPE_NAME + ".findActiveSessions";
+        public static final String FIND_RECENT_BY_USER = SessionLogEntry.LOGICAL_TYPE_NAME + ".findRecentByUser";
     }
 
     public static abstract class PropertyDomainEvent<T> extends IsisModuleExtSessionLogApplib.PropertyDomainEvent<SessionLogEntry, T> { }