You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2020/02/19 12:29:21 UTC

[isis] branch master updated (5e966dd -> 6e5cbdc)

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

ahuber pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git.


    from 5e966dd  ISIS-2158: SecMan: remove some unknown action ids from layout
     new b9d946b  ISIS-2158 minor: refactoring base64 utils into _String
     new 6e5cbdc  ISIS-2292: fallback to base64 if object identifier is not url safe

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.


Summary of changes:
 .../core/commons/internal/assertions/_Assert.java  |  25 ++
 .../isis/core/commons/internal/base/_Strings.java  |   9 +
 .../appfeat/ApplicationFeatureFactory.java         |   4 +-
 .../services/appfeat/ApplicationFeatureId.java     |  15 +-
 .../ApplicationFeatureRepositoryDefault.java       |  31 +--
 .../ApplicationFeatureRepositoryDefaultTest.java   |  39 ++-
 .../persistence/spi/JdoObjectIdDecoder.java        |  90 +++++++
 .../spi/JdoObjectIdEncoder.java}                   |  46 ++--
 .../persistence/spi/JdoObjectIdSerializer.java     | 280 ++++++++++-----------
 9 files changed, 325 insertions(+), 214 deletions(-)
 create mode 100644 persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdDecoder.java
 copy persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/{valuemappings/ByteArrayBlobRdbmsMapping.java => persistence/spi/JdoObjectIdEncoder.java} (50%)


[isis] 02/02: ISIS-2292: fallback to base64 if object identifier is not url safe

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

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 6e5cbdca916e08af9086548a60017384ef9c0f00
Author: Andi Huber <ah...@apache.org>
AuthorDate: Wed Feb 19 13:29:10 2020 +0100

    ISIS-2292: fallback to base64 if object identifier is not url safe
---
 .../persistence/spi/JdoObjectIdDecoder.java        |  90 +++++++
 .../persistence/spi/JdoObjectIdEncoder.java        |  53 ++++
 .../persistence/spi/JdoObjectIdSerializer.java     | 280 ++++++++++-----------
 3 files changed, 282 insertions(+), 141 deletions(-)

diff --git a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdDecoder.java b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdDecoder.java
new file mode 100644
index 0000000..1557c3e
--- /dev/null
+++ b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdDecoder.java
@@ -0,0 +1,90 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.persistence.jdo.datanucleus5.datanucleus.persistence.spi;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.jdo.annotations.IdentityType;
+
+import org.apache.isis.core.commons.handler.ChainOfResponsibility;
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.persistence.jdo.datanucleus5.datanucleus.persistence.spi.JdoObjectIdDecoder.JdoObjectIdDecodingRequest;
+import org.apache.isis.persistence.jdo.datanucleus5.metamodel.facets.object.persistencecapable.JdoPersistenceCapableFacet;
+
+import lombok.NonNull;
+import lombok.Value;
+import lombok.val;
+
+@Value(staticConstructor = "of")
+class JdoObjectIdDecoder implements ChainOfResponsibility.Handler<JdoObjectIdDecodingRequest, Object> {
+
+    @Value(staticConstructor = "of")
+    static class JdoObjectIdDecodingRequest {
+        final ObjectSpecification spec;
+        final String distinguisher;
+        final String keyStr;
+        
+        boolean isApplicationIdentity() {
+            val jdoPersistenceCapableFacet = spec.getFacet(JdoPersistenceCapableFacet.class);
+            return jdoPersistenceCapableFacet != null 
+                    && jdoPersistenceCapableFacet.getIdentityType() == IdentityType.APPLICATION;
+        }
+
+        public static JdoObjectIdDecodingRequest parse(ObjectSpecification spec, String idStr) {
+            idStr = _Strings.nullToEmpty(idStr);
+            final int separatorIdx = idStr.indexOf(JdoObjectIdSerializer.SEPARATOR);
+            return separatorIdx != -1
+                ? of(
+                        spec,
+                        idStr.substring(0, separatorIdx), 
+                        idStr.substring(separatorIdx + 1))
+                : of(spec, "", idStr);
+        }
+    }
+
+    @NonNull private final Predicate<JdoObjectIdDecodingRequest> filter;
+    @NonNull private final Function<JdoObjectIdDecodingRequest, Object> parser;
+    
+    @Override
+    public boolean isHandling(JdoObjectIdDecodingRequest request) {
+        return filter.test(request);
+    }
+
+    @Override
+    public Object handle(JdoObjectIdDecodingRequest request) {
+        return parser.apply(request);
+    }
+
+    public static Predicate<JdoObjectIdDecodingRequest> filter(@NonNull final String token) {
+        return request->token.equals(request.getDistinguisher());
+    }
+    
+    public static Function<JdoObjectIdDecodingRequest, Object> parser(
+            @NonNull final Function<String, ? extends Object> appIdFactory,
+            @NonNull final BiFunction<Class<?>, String, Object> jdoObjectIdFactory) {
+        return request->request.isApplicationIdentity()
+                ? appIdFactory.apply(request.getKeyStr())
+                : jdoObjectIdFactory.apply(request.getSpec().getCorrespondingClass(), request.getKeyStr());
+    }
+
+
+}
diff --git a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdEncoder.java b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdEncoder.java
new file mode 100644
index 0000000..94be169
--- /dev/null
+++ b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdEncoder.java
@@ -0,0 +1,53 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.persistence.jdo.datanucleus5.datanucleus.persistence.spi;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.apache.isis.core.commons.handler.ChainOfResponsibility;
+
+import lombok.NonNull;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+class JdoObjectIdEncoder implements ChainOfResponsibility.Handler<Object, String> {
+
+    @NonNull private final Predicate<Object> filter;
+    @NonNull private final Function<Object, String> keyStringifier;
+    
+    @Override
+    public boolean isHandling(final Object jdoOid) {
+        return filter.test(jdoOid);
+    }
+    
+    @Override
+    public String handle(final Object jdoOid) {
+        return keyStringifier.apply(jdoOid);
+    }
+    
+    public static Predicate<Object> filter(Class<?> jdoKeyType) {
+        return jdoOid->jdoKeyType.isAssignableFrom(jdoOid.getClass());
+    }
+    
+    public static Function<Object, String> stringifier(String token) {
+        return jdoOid->token + JdoObjectIdSerializer.SEPARATOR + jdoOid;
+    }
+
+}
diff --git a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdSerializer.java b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdSerializer.java
index dc835b0..03c6189 100644
--- a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdSerializer.java
+++ b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/datanucleus/persistence/spi/JdoObjectIdSerializer.java
@@ -25,8 +25,8 @@ import java.math.BigInteger;
 import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
+import java.util.function.Function;
 
-import javax.jdo.annotations.IdentityType;
 import javax.jdo.identity.ByteIdentity;
 import javax.jdo.identity.IntIdentity;
 import javax.jdo.identity.LongIdentity;
@@ -35,173 +35,171 @@ import javax.jdo.identity.StringIdentity;
 
 import org.datanucleus.identity.DatastoreId;
 
+import org.apache.isis.core.commons.handler.ChainOfResponsibility;
+import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.core.commons.internal.context._Context;
+import org.apache.isis.core.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.metamodel.adapter.oid.RootOid;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
-import org.apache.isis.persistence.jdo.datanucleus5.metamodel.facets.object.persistencecapable.JdoPersistenceCapableFacet;
+import org.apache.isis.persistence.jdo.datanucleus5.datanucleus.persistence.spi.JdoObjectIdDecoder.JdoObjectIdDecodingRequest;
 
 import lombok.val;
+import lombok.experimental.UtilityClass;
 
+@UtilityClass
 public final class JdoObjectIdSerializer {
 
-    private static final char SEPARATOR = '_';
-
-    private JdoObjectIdSerializer(){}
-
-    public static class Exception extends RuntimeException {
-        private static final long serialVersionUID = 1L;
-
-        public Exception(final java.lang.Exception ex) {
-            super(ex);
-        }
+    static final char SEPARATOR = '_';
+    
+    private static List<JdoObjectIdEncoder> encoders() {
+        
+        final List<String> nonSafeUrlChars = Arrays.asList("/", "\\");
+        
+        val encoders = Arrays.asList(
+                // Byte
+                JdoObjectIdEncoder.of(
+                        JdoObjectIdEncoder.filter(ByteIdentity.class),   
+                        JdoObjectIdEncoder.stringifier("b")),
+                // Int
+                JdoObjectIdEncoder.of(
+                        JdoObjectIdEncoder.filter(IntIdentity.class),   
+                        JdoObjectIdEncoder.stringifier("i")),
+                // Long
+                JdoObjectIdEncoder.of(
+                        JdoObjectIdEncoder.filter(LongIdentity.class),   
+                        JdoObjectIdEncoder.stringifier("l")),
+                // String
+                JdoObjectIdEncoder.of(
+                        JdoObjectIdEncoder.filter(StringIdentity.class),   
+                        jdoOid->{
+                            val stringified = "" + jdoOid;
+                            if(nonSafeUrlChars.stream()
+                                    .anyMatch(stringified::contains)) {
+                                return "base64" + SEPARATOR + _Strings.base64UrlEncode(stringified);
+                            }
+                            return "s" + SEPARATOR + stringified;
+                        }),
+                // UUID
+                JdoObjectIdEncoder.of(
+                        jdoOid->{
+                            if(jdoOid instanceof javax.jdo.identity.ObjectIdentity) {
+                                val id = (ObjectIdentity) jdoOid;
+                                return id.getKeyAsObject() instanceof UUID;
+                            }
+                            return false;            
+                        },
+                        jdoOid->{
+                            val id = (ObjectIdentity) jdoOid;
+                            val uuid = (UUID) id.getKeyAsObject();
+                            return "u" + SEPARATOR + uuid.toString();
+                        }),
+                // DatastoreId
+                JdoObjectIdEncoder.of( 
+                        jdoOid->{
+                            if(jdoOid instanceof DatastoreId) {
+                                final DatastoreId dnOid = (DatastoreId) jdoOid;
+                                final Object keyValue = dnOid.getKeyAsObject();
+                                // prettier handling of common datatypes if possible ?
+                                if( keyValue instanceof String ||
+                                        keyValue instanceof Long ||
+                                        keyValue instanceof BigDecimal || // 1.8.0 did not support BigDecimal
+                                        keyValue instanceof BigInteger ||
+                                        keyValue instanceof Integer) {
+                                    return true;
+                                }
+                            }
+                            return false;            
+                        },
+                        jdoOid->{
+                            final DatastoreId dnOid = (DatastoreId) jdoOid;
+                            // no separator
+                            return "" + dnOid.getKeyAsObject();
+                        })
+                );
+        return encoders;
+        
+    }
+    
+    private static List<JdoObjectIdDecoder> decoders() {
+        
+        final List<String> dnPrefixes = Arrays.asList("S", "I", "L", "M", "B");
+        
+        val decoders = Arrays.asList(
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("b"),
+                        JdoObjectIdDecoder.parser(Byte::parseByte, ByteIdentity::new)),
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("i"),
+                        JdoObjectIdDecoder.parser(Integer::parseInt, IntIdentity::new)),
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("l"),
+                        JdoObjectIdDecoder.parser(Long::parseLong, LongIdentity::new)),
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("s"),
+                        JdoObjectIdDecoder.parser(Function.identity(), StringIdentity::new)),
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("base64"),
+                        JdoObjectIdDecoder.parser(
+                                _Strings::base64UrlDecode, 
+                                (type, keyStr)-> new StringIdentity(type, _Strings.base64UrlDecode(keyStr)) )),
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter("u"),
+                        JdoObjectIdDecoder.parser(
+                                UUID::fromString, 
+                                (type, keyStr)-> new ObjectIdentity(type, UUID.fromString(keyStr)) )),
+                // if there is no separator, the identifier is for
+                // @javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE)
+                JdoObjectIdDecoder.of(
+                        JdoObjectIdDecoder.filter(""),
+                        JdoObjectIdDecodingRequest::getKeyStr),
+                JdoObjectIdDecoder.of(
+                        request->dnPrefixes.contains(request.getDistinguisher()),
+                        request->request.getKeyStr() + "[OID]" + request.getSpec().getFullIdentifier())
+                
+                );
+        return decoders;
     }
 
+    private final static ChainOfResponsibility<Object, String> 
+        encodingChain = ChainOfResponsibility.of(encoders());
+    
+    private final static ChainOfResponsibility<JdoObjectIdDecodingRequest, Object> 
+        decodingChain = ChainOfResponsibility.of(decoders());
+    
     public static String toOidIdentifier(final Object jdoOid) {
 
-        //
-        // @javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.APPLICATION)
-        //
-        if(jdoOid instanceof javax.jdo.identity.ByteIdentity) {
-            return "b" + SEPARATOR + jdoOid;
-        }
-        if(jdoOid instanceof javax.jdo.identity.IntIdentity) {
-            return "i" + SEPARATOR + jdoOid;
-        }
-        if(jdoOid instanceof javax.jdo.identity.StringIdentity) {
-            return "s" + SEPARATOR + jdoOid;
-        }
-        if(jdoOid instanceof javax.jdo.identity.LongIdentity) {
-            return "l" + SEPARATOR + jdoOid;
-        }
-        if(jdoOid instanceof javax.jdo.identity.ObjectIdentity) {
-            final javax.jdo.identity.ObjectIdentity id = (ObjectIdentity) jdoOid;
-            final Object keyAsObject = id.getKeyAsObject();
-            // UUID support
-            if(keyAsObject instanceof UUID) {
-                final UUID uuid = (UUID) keyAsObject;
-                return "u" + SEPARATOR + uuid.toString();
-            }
-        }
-
-        if(jdoOid instanceof DatastoreId) {
-
-            //
-            // prettier handling of common datatypes if possible
-            //
-
-            final DatastoreId dnOid = (DatastoreId) jdoOid;
-            final Object keyValue = dnOid.getKeyAsObject();
-
-
-            if( keyValue instanceof String ||
-                    keyValue instanceof Long ||
-                    keyValue instanceof BigDecimal || // 1.8.0 did not support BigDecimal
-                    keyValue instanceof BigInteger ||
-                    keyValue instanceof Integer) {
-
-                // no separator
-                return "" + keyValue;
-            }
-
-        }
-
-        // the JDO spec (5.4.3) requires that OIDs are serializable toString and
-        // recreatable through the constructor
-        return jdoOid.getClass().getName() + SEPARATOR + jdoOid.toString();
+        return encodingChain.handle(jdoOid)
+        .orElseGet(()->
+            // the JDO spec (5.4.3) requires that OIDs are serializable toString and
+            // re-create-able through the constructor
+            jdoOid.getClass().getName() + SEPARATOR + jdoOid.toString());
     }
 
-    private static List<String> dnPrefixes = Arrays.asList("S", "I", "L", "M", "B");
-
+   
     public static Object toJdoObjectId(SpecificationLoader specificationLoader, RootOid oid) {
 
-        final String idStr = oid.getIdentifier();
-        final int separatorIdx = idStr.indexOf(SEPARATOR);
-
-        final ObjectSpecification spec = specificationLoader.lookupBySpecIdElseLoad(oid.getObjectSpecId());
-        final JdoPersistenceCapableFacet jdoPcFacet = spec.getFacet(JdoPersistenceCapableFacet.class);
-
-
-        if(separatorIdx != -1) {
-
-            // behaviour for OIDs as of 1.8.0 and previously
-
-            final String distinguisher = idStr.substring(0, separatorIdx);
-            final String keyStr = idStr.substring(separatorIdx + 1);
-
-            final boolean isApplicationIdentity = isApplicationIdentity(jdoPcFacet);
-
-            if("s".equals(distinguisher)) {
-                if (isApplicationIdentity) {
-                    return keyStr;
-                } else {
-                    return new StringIdentity(objectTypeClassFor(specificationLoader, oid), keyStr);
-                }
-            } else if("i".equals(distinguisher)) {
-                if(isApplicationIdentity) {
-                    return Integer.parseInt(keyStr);
-                } else {
-                    return new IntIdentity(objectTypeClassFor(specificationLoader, oid), keyStr);
-                }
-            } else if("l".equals(distinguisher)) {
-                if(isApplicationIdentity) {
-                    return Long.parseLong(keyStr);
-                } else {
-                    return new LongIdentity(objectTypeClassFor(specificationLoader, oid), keyStr);
-                }
-            } else if("b".equals(distinguisher)) {
-                if(isApplicationIdentity) {
-                    return Byte.parseByte(keyStr);
-                } else {
-                    return new ByteIdentity(objectTypeClassFor(specificationLoader, oid), keyStr);
-                }
-            } else if("u".equals(distinguisher)) {
-                if(isApplicationIdentity) {
-                    return UUID.fromString(keyStr);
-                } else {
-                    return new ObjectIdentity(
-                            objectTypeClassFor(specificationLoader, oid), UUID.fromString(keyStr));
-                }
-            }
+        val spec = specificationLoader.lookupBySpecIdElseLoad(oid.getObjectSpecId());
+        val request = JdoObjectIdDecodingRequest.parse(spec, oid.getIdentifier());
+        
+        return decodingChain.handle(request)
+        .orElseGet(()->{
 
-            if(dnPrefixes.contains(distinguisher)) {
-                return keyStr + "[OID]" + spec.getFullIdentifier();
-            }
+            val clsName = request.getDistinguisher();
+            val keyStr = request.getKeyStr();         
 
-            final String clsName = distinguisher;
             try {
                 final Class<?> cls = _Context.loadClass(clsName);
                 final Constructor<?> cons = cls.getConstructor(String.class);
                 final Object dnOid = cons.newInstance(keyStr);
                 return dnOid.toString();
             } catch (ClassNotFoundException | IllegalArgumentException | InstantiationException | IllegalAccessException | SecurityException | InvocationTargetException | NoSuchMethodException e) {
-                throw new JdoObjectIdSerializer.Exception(e);
+                throw _Exceptions.unrecoverableFormatted(
+                        "failed to instantiate %s with arg %s", clsName, keyStr, e);
             }
+            
+        });
 
-        } else {
-
-            // there was no separator, so this identifier must have been for
-            // @javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE)
-            // for one of the common types (prettier handling)
-
-            // in DN 4.1, we did this...
-            // return idStr + "[OID]" + spec.getFullIdentifier();
-
-            // in DN 5.1, we simply do this...
-            return idStr;
-        }
     }
 
-    protected static boolean isApplicationIdentity(final JdoPersistenceCapableFacet jdoPcFacet) {
-        return jdoPcFacet != null && jdoPcFacet.getIdentityType() == IdentityType.APPLICATION;
-    }
-
-    private static Class<?> objectTypeClassFor(SpecificationLoader specificationLoader, RootOid oid) {
-        
-        val objectSpecId = oid.getObjectSpecId();
-        val spec = specificationLoader.lookupBySpecIdElseLoad(objectSpecId);
-        val correspondingClass = spec.getCorrespondingClass();
-        return correspondingClass;
-    }
 
 }


[isis] 01/02: ISIS-2158 minor: refactoring base64 utils into _String

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

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git

commit b9d946b2b565b4edc0dc6ed378fb2e5ea31383ab
Author: Andi Huber <ah...@apache.org>
AuthorDate: Wed Feb 19 13:27:50 2020 +0100

    ISIS-2158 minor: refactoring base64 utils into _String
---
 .../core/commons/internal/assertions/_Assert.java  | 25 ++++++++++++++
 .../isis/core/commons/internal/base/_Strings.java  |  9 +++++
 .../appfeat/ApplicationFeatureFactory.java         |  4 +--
 .../services/appfeat/ApplicationFeatureId.java     | 15 ++-------
 .../ApplicationFeatureRepositoryDefault.java       | 31 +++++++++--------
 .../ApplicationFeatureRepositoryDefaultTest.java   | 39 +++++++++-------------
 6 files changed, 71 insertions(+), 52 deletions(-)

diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/internal/assertions/_Assert.java b/core/commons/src/main/java/org/apache/isis/core/commons/internal/assertions/_Assert.java
index fd6c8d4..ab0b016 100644
--- a/core/commons/src/main/java/org/apache/isis/core/commons/internal/assertions/_Assert.java
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/internal/assertions/_Assert.java
@@ -20,6 +20,7 @@
 package org.apache.isis.core.commons.internal.assertions;
 
 import java.util.Objects;
+import java.util.function.Supplier;
 
 import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.core.commons.internal.exceptions._Exceptions;
@@ -53,6 +54,12 @@ public final class _Assert {
             fail(message, true, false);
         }
     }
+    
+    public static void assertTrue(boolean condition, Supplier<String> lazyMessage) {
+        if (!condition) {
+            fail(lazyMessage.get(), true, false);
+        }
+    }
 
     // -- FALSE
 
@@ -66,6 +73,12 @@ public final class _Assert {
         }
     }
     
+    public static void assertFalse(boolean condition, Supplier<String> lazyMessage) {
+        if (condition) {
+            fail(lazyMessage.get(), true, false);
+        }
+    }
+    
     // -- NOT NULL
 
     public static void assertNotNull(Object object) {
@@ -78,6 +91,12 @@ public final class _Assert {
         }
     }
     
+    public static void assertNotNull(Object object, Supplier<String> lazyMessage) {
+        if (object==null) {
+            fail(lazyMessage.get(), "not null", "null");
+        }
+    }
+    
     /**
      * <em>Assert</em> that {@code expected} and {@code actual} are equal.
      * <p>If both are {@code null}, they are considered equal.
@@ -101,6 +120,12 @@ public final class _Assert {
         }
     }
     
+    public static void assertEquals(Object left, Object right, Supplier<String> lazyMessage) {
+        if (!Objects.equals(left, right)) {
+            fail(lazyMessage.get(), left, right);
+        }
+    }
+    
     // -- TYPE INSTANCE OF
 
     public static void assertTypeIsInstanceOf(Class<?> type, Class<?> requiredType) {
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/internal/base/_Strings.java b/core/commons/src/main/java/org/apache/isis/core/commons/internal/base/_Strings.java
index ac999bb..585863d 100644
--- a/core/commons/src/main/java/org/apache/isis/core/commons/internal/base/_Strings.java
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/internal/base/_Strings.java
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -640,6 +641,14 @@ public final class _Strings {
                 .map(String::trim)
                 .filter(not(String::isEmpty));
     }
+    
+    public static String base64UrlDecode(final String str) {
+        return _Strings.convert(str, _Bytes.ofUrlBase64, StandardCharsets.UTF_8);
+    }
+
+    public static String base64UrlEncode(final String str) {
+        return _Strings.convert(str, _Bytes.asUrlBase64, StandardCharsets.UTF_8);
+    }
 
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureFactory.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureFactory.java
index 3720a51..c33eb3b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureFactory.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureFactory.java
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.core.metamodel.services.appfeat;
 
-import javax.inject.Inject;
 import javax.inject.Named;
 
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -27,7 +26,6 @@ import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.OrderPrecedence;
-import org.apache.isis.applib.services.factory.FactoryService;
 
 @Service
 @Named("isisMetaModel.ApplicationFeatureFactory")
@@ -36,7 +34,7 @@ import org.apache.isis.applib.services.factory.FactoryService;
 @Qualifier("Default")
 public class ApplicationFeatureFactory {
 
-    @Inject FactoryService factoryService;
+    // @Inject FactoryService factoryService;
     
     public ApplicationFeature newApplicationFeature() {
         
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureId.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureId.java
index 70e2033..1ef6e8a 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureId.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureId.java
@@ -19,7 +19,6 @@
 package org.apache.isis.core.metamodel.services.appfeat;
 
 import java.io.Serializable;
-import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -39,7 +38,6 @@ import org.apache.isis.applib.util.Hashing;
 import org.apache.isis.applib.util.ObjectContracts;
 import org.apache.isis.applib.util.TitleBuffer;
 import org.apache.isis.applib.util.ToString;
-import org.apache.isis.core.commons.internal.base._Bytes;
 import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.core.commons.internal.collections._Lists;
 import org.apache.isis.core.metamodel.spec.ObjectSpecId;
@@ -128,7 +126,7 @@ public class ApplicationFeatureId implements Comparable<ApplicationFeatureId>, S
      * Round-trip with {@link #asEncodedString()}
      */
     public static ApplicationFeatureId parseEncoded(final String encodedString) {
-        return new ApplicationFeatureId(base64UrlDecode(encodedString));
+        return new ApplicationFeatureId(_Strings.base64UrlDecode(encodedString));
     }
 
 
@@ -319,16 +317,9 @@ public class ApplicationFeatureId implements Comparable<ApplicationFeatureId>, S
 
     @Programmatic
     public String asEncodedString() {
-        return base64UrlEncode(asString());
-    }
-
-    private static String base64UrlDecode(final String str) {
-        return _Strings.convert(str, _Bytes.ofUrlBase64, StandardCharsets.UTF_8);
-    }
-
-    private static String base64UrlEncode(final String str) {
-        return _Strings.convert(str, _Bytes.asUrlBase64, StandardCharsets.UTF_8);
+        return _Strings.base64UrlEncode(asString());
     }
+    
 
     // //////////////////////////////////////
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java
index 928a252..75f0026 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java
@@ -37,10 +37,8 @@ import org.apache.isis.applib.annotation.Where;
 import org.apache.isis.applib.services.appfeat.ApplicationFeatureRepository;
 import org.apache.isis.applib.services.appfeat.ApplicationMemberType;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.core.commons.internal.base._Lazy;
 import org.apache.isis.core.commons.internal.collections._Maps;
 import org.apache.isis.core.commons.internal.collections._Sets;
-import org.apache.isis.core.commons.internal.ioc.ManagedBeanAdapter;
 import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.core.config.metamodel.services.ApplicationFeaturesInitConfiguration;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
@@ -69,8 +67,13 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class ApplicationFeatureRepositoryDefault implements ApplicationFeatureRepository {
 
+    private final IsisConfiguration configuration;
+    private final ServiceRegistry serviceRegistry;
+    private final SpecificationLoader specificationLoader;
+    private final ApplicationFeatureFactory applicationFeatureFactory;
+    
     // -- caches
-    SortedMap<ApplicationFeatureId, ApplicationFeature> packageFeatures = _Maps.newTreeMap();
+    final SortedMap<ApplicationFeatureId, ApplicationFeature> packageFeatures = _Maps.newTreeMap();
     private final SortedMap<ApplicationFeatureId, ApplicationFeature> classFeatures = _Maps.newTreeMap();
     private final SortedMap<ApplicationFeatureId, ApplicationFeature> memberFeatures = _Maps.newTreeMap();
     private final SortedMap<ApplicationFeatureId, ApplicationFeature> propertyFeatures = _Maps.newTreeMap();
@@ -79,6 +82,15 @@ public class ApplicationFeatureRepositoryDefault implements ApplicationFeatureRe
 
     // -- init
 
+    @Inject
+    public ApplicationFeatureRepositoryDefault(IsisConfiguration configuration, ServiceRegistry serviceRegistry,
+            SpecificationLoader specificationLoader, ApplicationFeatureFactory applicationFeatureFactory) {
+        this.configuration = configuration;
+        this.serviceRegistry = serviceRegistry;
+        this.specificationLoader = specificationLoader;
+        this.applicationFeatureFactory = applicationFeatureFactory;
+    }
+
     @PostConstruct
     public void init() {
         if(isEagerInitialize()) {
@@ -86,8 +98,6 @@ public class ApplicationFeatureRepositoryDefault implements ApplicationFeatureRe
         }
     }
 
-    @Inject IsisConfiguration configuration;
-
     private boolean isEagerInitialize() {
         ApplicationFeaturesInitConfiguration setting = configuration.getCore().getRuntimeServices().getApplicationFeatures().getInit();
         return setting == ApplicationFeaturesInitConfiguration.EAGER || setting == ApplicationFeaturesInitConfiguration.EAGERLY;
@@ -548,16 +558,9 @@ public class ApplicationFeatureRepositoryDefault implements ApplicationFeatureRe
                 .collect(_Sets.toUnmodifiableSorted());
     }
 
-    @Inject ServiceRegistry serviceRegistry;
-    @Inject SpecificationLoader specificationLoader;
-    @Inject ApplicationFeatureFactory applicationFeatureFactory;
 
-    private _Lazy<List<ManagedBeanAdapter>> registeredServices = _Lazy.threadSafe(()->{
-        val registeredServices = 
-                serviceRegistry.streamRegisteredBeans()
-                .collect(Collectors.toList());
-        return registeredServices;
-    });
+
+
 
 
 }
\ No newline at end of file
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefaultTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefaultTest.java
index e2c7a90..07b2bc5 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefaultTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefaultTest.java
@@ -42,6 +42,7 @@ import org.apache.isis.applib.annotation.Where;
 import org.apache.isis.applib.services.factory.FactoryService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.core.commons.internal.collections._Lists;
+import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.core.metamodel.facets.all.hide.HiddenFacet;
 import org.apache.isis.core.metamodel.facets.members.hidden.HiddenFacetAbstract;
 import org.apache.isis.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
@@ -56,44 +57,36 @@ import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
-import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
 
 public class ApplicationFeatureRepositoryDefaultTest {
 
     @Rule
     public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(JUnitRuleMockery2.Mode.INTERFACES_AND_CLASSES);
 
-    @Mock
-    ObjectSpecification mockSpec;
-    @Mock
-    OneToOneAssociation mockProp;
-    @Mock
-    OneToManyAssociation mockColl;
-    @Mock
-    ObjectAction mockAct;
+    @Mock ObjectSpecification mockSpec;
+    @Mock OneToOneAssociation mockProp;
+    @Mock OneToManyAssociation mockColl;
+    @Mock ObjectAction mockAct;
+    
     ObjectAction mockActThatIsHidden;
 
-    @Mock
-    FactoryService mockFactoryService;
-
-    @Mock
-    ServiceRegistry mockServiceRegistry;
-
-    @Mock
-    SpecificationLoader mockSpecificationLoader;
+    @Mock FactoryService mockFactoryService;
+    @Mock ServiceRegistry mockServiceRegistry;
+    @Mock SpecificationLoader mockSpecificationLoader;
 
-    ApplicationFeatureRepositoryDefault applicationFeatureRepository;
+    protected ApplicationFeatureRepositoryDefault applicationFeatureRepository;
 
     @Before
     public void setUp() throws Exception {
-        applicationFeatureRepository = new ApplicationFeatureRepositoryDefault();
-        applicationFeatureRepository.serviceRegistry = mockServiceRegistry;
-        applicationFeatureRepository.specificationLoader = mockSpecificationLoader;
 
         final ApplicationFeatureFactory applicationFeatureFactory = new ApplicationFeatureFactory();
-        applicationFeatureRepository.applicationFeatureFactory = applicationFeatureFactory;
-        applicationFeatureFactory.factoryService = mockFactoryService;
+        //applicationFeatureFactory.factoryService = mockFactoryService;
 
+        applicationFeatureRepository = new ApplicationFeatureRepositoryDefault(
+                /*configuration*/ null, 
+                mockServiceRegistry, 
+                mockSpecificationLoader, 
+                applicationFeatureFactory);
 
         mockActThatIsHidden = context.mock(ObjectAction.class, "mockActThatIsHidden");
     }