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 2021/09/02 16:00:07 UTC

[isis] branch master updated (9bd81d7 -> 9766d0f)

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 9bd81d7  ISIS-2774: adds support for enforcing member annotations
     new 7cfcd3b  ISIS-2774: Can<T>: support unique(..) by custom bi-predicate
     new 9766d0f  ISIS-2774: more rigorous TitleFacet validation/ conflict detection

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:
 .../org/apache/isis/commons/collections/Can.java   |  11 +-
 .../apache/isis/commons/collections/Can_Empty.java |   7 +-
 .../isis/commons/collections/Can_Multiple.java     |  14 +++
 .../isis/commons/collections/Can_Singleton.java    |   6 +
 .../facets/object/title/TitleFacetAbstract.java    |  32 +++++
 .../annotation/TitleAnnotationFacetFactory.java    | 117 +++++++++++-------
 .../annotation/TitleFacetViaTitleAnnotation.java   | 136 ++++++++++++---------
 .../TitleAnnotationFacetFactoryTest.java           |   6 +-
 .../TitleFacetViaTitleAnnotationTest.java          |  24 ++--
 .../promptStyle/ActionLayoutPromptStyleVm.java     |   2 +-
 10 files changed, 236 insertions(+), 119 deletions(-)

[isis] 01/02: ISIS-2774: Can: support unique(..) by custom bi-predicate

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 7cfcd3b593d204a856fb7b3d1900d159a01ba741
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Sep 2 17:49:48 2021 +0200

    ISIS-2774: Can<T>: support unique(..) by custom bi-predicate
---
 .../main/java/org/apache/isis/commons/collections/Can.java | 11 ++++++++++-
 .../org/apache/isis/commons/collections/Can_Empty.java     |  7 ++++++-
 .../org/apache/isis/commons/collections/Can_Multiple.java  | 14 ++++++++++++++
 .../org/apache/isis/commons/collections/Can_Singleton.java |  6 ++++++
 4 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/commons/src/main/java/org/apache/isis/commons/collections/Can.java b/commons/src/main/java/org/apache/isis/commons/collections/Can.java
index 3ad6513..fbf6eeb 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/Can.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/Can.java
@@ -30,6 +30,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -38,9 +39,10 @@ import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.springframework.lang.Nullable;
 import javax.enterprise.inject.Instance;
 
+import org.springframework.lang.Nullable;
+
 import org.apache.isis.commons.internal.base._NullSafe;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
 
@@ -397,6 +399,13 @@ extends Iterable<T>, Comparable<Can<T>>, Serializable {
 
     /**
      * Returns a {@code Can} with all the elements from this {@code Can}, but
+     * duplicated elements removed, based on given {@code equality} relation.
+     * @return non-null
+     */
+    public Can<T> unique(@NonNull BiPredicate<T, T> equality);
+
+    /**
+     * Returns a {@code Can} with all the elements from this {@code Can}, but
      * contained in reversed order.
      * @return non-null
      */
diff --git a/commons/src/main/java/org/apache/isis/commons/collections/Can_Empty.java b/commons/src/main/java/org/apache/isis/commons/collections/Can_Empty.java
index 7b1fc6a..6fa3786 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/Can_Empty.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/Can_Empty.java
@@ -27,6 +27,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -106,6 +107,11 @@ final class Can_Empty<T> implements Can<T> {
     }
 
     @Override
+    public Can<T> unique(final @NonNull BiPredicate<T, T> equality) {
+        return this;
+    }
+
+    @Override
     public Can<T> reverse() {
         return this;
     }
@@ -245,5 +251,4 @@ final class Can_Empty<T> implements Can<T> {
     }
 
 
-
 }
diff --git a/commons/src/main/java/org/apache/isis/commons/collections/Can_Multiple.java b/commons/src/main/java/org/apache/isis/commons/collections/Can_Multiple.java
index 0be52ab..1c7c7d5 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/Can_Multiple.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/Can_Multiple.java
@@ -29,6 +29,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -39,6 +40,7 @@ import org.springframework.lang.Nullable;
 
 import org.apache.isis.commons.internal.base._Casts;
 import org.apache.isis.commons.internal.base._Objects;
+import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Sets;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
 
@@ -120,6 +122,18 @@ final class Can_Multiple<T> implements Can<T> {
     }
 
     @Override
+    public Can<T> unique(final @NonNull BiPredicate<T, T> equality) {
+        val list = _Lists.<T>newArrayList();
+        elements
+        .forEach(element->{
+            if(!list.stream().anyMatch(x->equality.test(x, element))) {
+                list.add(element);
+            }
+        });
+        return Can.ofCollection(list);
+    }
+
+    @Override
     public Iterator<T> iterator() {
         return Collections.unmodifiableList(elements).iterator();
     }
diff --git a/commons/src/main/java/org/apache/isis/commons/collections/Can_Singleton.java b/commons/src/main/java/org/apache/isis/commons/collections/Can_Singleton.java
index cc6e2b1..667ffb5 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/Can_Singleton.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/Can_Singleton.java
@@ -29,6 +29,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -107,6 +108,11 @@ final class Can_Singleton<T> implements Can<T> {
     }
 
     @Override
+    public Can<T> unique(final @NonNull BiPredicate<T, T> equality) {
+        return this;
+    }
+
+    @Override
     public Can<T> reverse() {
         return this;
     }

[isis] 02/02: ISIS-2774: more rigorous TitleFacet validation/ conflict detection

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 9766d0f4f89234172d4f9215ae80030cc34cfcbd
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Sep 2 17:59:56 2021 +0200

    ISIS-2774: more rigorous TitleFacet validation/ conflict detection
---
 .../facets/object/title/TitleFacetAbstract.java    |  32 +++++
 .../annotation/TitleAnnotationFacetFactory.java    | 117 +++++++++++-------
 .../annotation/TitleFacetViaTitleAnnotation.java   | 136 ++++++++++++---------
 .../TitleAnnotationFacetFactoryTest.java           |   6 +-
 .../TitleFacetViaTitleAnnotationTest.java          |  24 ++--
 .../promptStyle/ActionLayoutPromptStyleVm.java     |   2 +-
 6 files changed, 200 insertions(+), 117 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/TitleFacetAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/TitleFacetAbstract.java
index a43df42..9e766d7 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/TitleFacetAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/TitleFacetAbstract.java
@@ -19,9 +19,15 @@
 
 package org.apache.isis.core.metamodel.facets.object.title;
 
+import java.util.Objects;
+
 import org.apache.isis.core.metamodel.facetapi.Facet;
 import org.apache.isis.core.metamodel.facetapi.FacetAbstract;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
+import org.apache.isis.core.metamodel.facets.ImperativeFacet;
+
+import lombok.NonNull;
+import lombok.val;
 
 public abstract class TitleFacetAbstract
 extends FacetAbstract
@@ -39,4 +45,30 @@ implements TitleFacet {
         super(type(), holder, precedence);
     }
 
+    @Override
+    public boolean semanticEquals(final @NonNull Facet other) {
+
+        // equality by facet-type and java-methods
+
+        if(!this.facetType().equals(other.facetType())) {
+            return false;
+        }
+
+        val otherFacet = (TitleFacet)other;
+
+        if(Objects.equals(this, otherFacet)) {
+            return true;
+        }
+
+        if(this instanceof ImperativeFacet
+                && otherFacet instanceof ImperativeFacet) {
+
+            return ((ImperativeFacet)this)
+                    .getMethods()
+                    .equals(((ImperativeFacet)otherFacet).getMethods());
+        }
+
+        return false;
+    }
+
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleAnnotationFacetFactory.java
index dc74a6f..6e614e8 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleAnnotationFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleAnnotationFacetFactory.java
@@ -26,33 +26,25 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
-import org.apache.isis.applib.annotation.Introspection.IntrospectionPolicy;
 import org.apache.isis.applib.annotation.Title;
+import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Strings;
-import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
-import org.apache.isis.core.metamodel.facetapi.FacetUtil;
 import org.apache.isis.core.metamodel.facetapi.FeatureType;
 import org.apache.isis.core.metamodel.facetapi.MetaModelRefiner;
 import org.apache.isis.core.metamodel.facets.Annotations;
 import org.apache.isis.core.metamodel.facets.FacetFactoryAbstract;
 import org.apache.isis.core.metamodel.facets.fallback.FallbackFacetFactory;
+import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
 import org.apache.isis.core.metamodel.facets.object.title.methods.TitleFacetViaMethodsFactory;
-import org.apache.isis.core.metamodel.methods.MethodFinderOptions;
-import org.apache.isis.core.metamodel.methods.MethodFinderUtils;
-import org.apache.isis.core.metamodel.methods.MethodLiteralConstants;
 import org.apache.isis.core.metamodel.progmodel.ProgrammingModel;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
-import org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
 import org.apache.isis.core.metamodel.specloader.validator.ValidationFailure;
 
 public class TitleAnnotationFacetFactory
 extends FacetFactoryAbstract
 implements MetaModelRefiner {
 
-    private static final String TITLE_METHOD_NAME = "title";
-
     @Inject
     public TitleAnnotationFacetFactory(final MetaModelContext mmc) {
         super(mmc, FeatureType.OBJECTS_ONLY);
@@ -67,15 +59,17 @@ implements MetaModelRefiner {
         final Class<?> cls = processClassContext.getCls();
         final FacetHolder facetHolder = processClassContext.getFacetHolder();
 
-        final List<Annotations.Evaluator<Title>> evaluators = Annotations.getEvaluators(cls, Title.class);
+        final var evaluators = Annotations.getEvaluators(cls, Title.class);
         if (evaluators.isEmpty()) {
             return;
         }
 
         sort(evaluators);
-        final List<TitleFacetViaTitleAnnotation.TitleComponent> titleComponents =
-                _Lists.map(evaluators, TitleFacetViaTitleAnnotation.TitleComponent.FROM_EVALUATORS);
-        FacetUtil.addFacet(new TitleFacetViaTitleAnnotation(titleComponents, facetHolder));
+        final var titleComponents =
+                Can.ofCollection(evaluators)
+                .map(TitleFacetViaTitleAnnotation.TitleComponent::of);
+
+        addFacet(new TitleFacetViaTitleAnnotation(titleComponents, facetHolder));
     }
 
     public static void sort(final List<Annotations.Evaluator<Title>> evaluators) {
@@ -159,49 +153,84 @@ implements MetaModelRefiner {
     @Override
     public void refineProgrammingModel(final ProgrammingModel programmingModel) {
 
-        programmingModel.addVisitingValidatorSkipManagedBeans(_objectSpec -> {
+        programmingModel.addVisitingValidatorSkipManagedBeans(objectSpec -> {
 
-            final var objectSpec = (ObjectSpecificationAbstract)_objectSpec;
-            final var cls = objectSpec.getCorrespondingClass();
-            final var introspectionPolicy = objectSpec.getIntrospectionPolicy();
+//            final var objectSpec = (ObjectSpecificationAbstract)_objectSpec;
+//            final var cls = objectSpec.getCorrespondingClass();
+//            final var introspectionPolicy = objectSpec.getIntrospectionPolicy();
 
-            final var titleMethod = MethodFinderUtils.findMethod(
-                    MethodFinderOptions.objectSupport(introspectionPolicy),
-                    cls, TITLE_METHOD_NAME, String.class, null);
-            if (titleMethod == null) {
-                return;
-            }
+            final var titleFacetTopRank =
+                objectSpec
+                .getFacetRanking(TitleFacet.class)
+                .map(facetRanking->facetRanking.getTopRank(TitleFacet.class))
+                .orElse(Can.empty())
+                .unique(TitleFacet::semanticEquals);
 
-            // determine if cls contains an @Title annotated method, not inherited from superclass
-            final ObjectSpecification superSpec = objectSpec.superclass();
-            if (superSpec == null) {
-                return;
-            }
+            // top-rank if present must not be ambiguous
+            if(titleFacetTopRank.isCardinalityMultiple()) {
 
-            final var superIntrospectionPolicy = ((ObjectSpecificationAbstract)superSpec).getIntrospectionPolicy();
-            final var superCls = superSpec.getCorrespondingClass();
+                final var conflictingFeatures =
+                        titleFacetTopRank
+                        .map(TitleFacet::getClass)
+                        .map(Class::getSimpleName)
+                        .toList();
 
-            if (countMethodsWithTitleAnnotation(introspectionPolicy, cls)
-                    > countMethodsWithTitleAnnotation(superIntrospectionPolicy, superCls)) {
                 ValidationFailure.raiseFormatted(
                         objectSpec,
                         "%s: conflict for determining a strategy for retrieval of title for class, "
-                        + "contains a method '%s' and an annotation '@%s'",
+                        + "conflicting title facets %s",
                         objectSpec.getFeatureIdentifier().getClassName(),
-                        TITLE_METHOD_NAME,
-                        Title.class.getName());
+                        conflictingFeatures.toString());
+
             }
 
+
+
+//
+//            final var titleMethod = MethodFinderUtils.findMethod(
+//                    MethodFinderOptions.objectSupport(introspectionPolicy),
+//                    cls, TITLE_METHOD_NAME, String.class, null);
+//            if (titleMethod == null) {
+//                return;
+//            }
+//
+//            // determine if cls contains an @Title annotated method, not inherited from superclass
+//            final ObjectSpecification superSpec = objectSpec.superclass();
+//            if (superSpec == null) {
+//                return;
+//            }
+//
+//            final var superIntrospectionPolicy = ((ObjectSpecificationAbstract)superSpec).getIntrospectionPolicy();
+//            final var superCls = superSpec.getCorrespondingClass();
+//
+//            //FIXME[ISIS-2774] also count declared fields that are @Title annotated
+//            if (countMethodsWithTitleAnnotation(introspectionPolicy, cls)
+//                    > 1L
+//                    //countMethodsWithTitleAnnotation(superIntrospectionPolicy, superCls)
+//                    ) {
+//                ValidationFailure.raiseFormatted(
+//                        objectSpec,
+//                        "%s: conflict for determining a strategy for retrieval of title for class, "
+//                        + "contains a method '%s' and an annotation '@%s' on a different members",
+//                        objectSpec.getFeatureIdentifier().getClassName(),
+//                        TITLE_METHOD_NAME,
+//                        Title.class.getName());
+//            }
+
         });
     }
 
-    private static long countMethodsWithTitleAnnotation(
-            final IntrospectionPolicy introspectionPolicy,
-            final Class<?> cls) {
-        return MethodFinderUtils.streamMethodsWithAnnotation(
-                MethodFinderOptions.objectSupport(introspectionPolicy), cls, Title.class)
-                .filter(method->!method.getName().equals(MethodLiteralConstants.TITLE))
-                .count();
-    }
+//    private static long countMethodsWithTitleAnnotation(
+//            final IntrospectionPolicy introspectionPolicy,
+//            final Class<?> cls) {
+//        return MethodFinderUtils.streamMethodsWithAnnotation(
+//                MethodFinderOptions.objectSupport(introspectionPolicy), cls, Title.class)
+//                // don't count methods that identify as title() methods
+//                .filter(method->
+//                        !
+//                        (method.getName().equals(MethodLiteralConstants.TITLE)
+//                                && method.getParameterCount() == 0))
+//                .count();
+//    }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleFacetViaTitleAnnotation.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleFacetViaTitleAnnotation.java
index 151df37..d6fcdbb 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleFacetViaTitleAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/title/annotation/TitleFacetViaTitleAnnotation.java
@@ -19,85 +19,60 @@
 
 package org.apache.isis.core.metamodel.facets.object.title.annotation;
 
+import java.lang.reflect.Method;
 import java.util.List;
 import java.util.function.BiConsumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
 
 import org.apache.isis.applib.annotation.Title;
+import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Strings;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.functions._Predicates;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
 import org.apache.isis.core.metamodel.facets.Annotations;
+import org.apache.isis.core.metamodel.facets.Annotations.MethodEvaluator;
+import org.apache.isis.core.metamodel.facets.ImperativeFacet;
 import org.apache.isis.core.metamodel.facets.object.title.TitleFacetAbstract;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 
+import lombok.Getter;
+import lombok.NonNull;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 @Log4j2
 public class TitleFacetViaTitleAnnotation
-extends TitleFacetAbstract {
+extends TitleFacetAbstract
+implements ImperativeFacet {
 
-    private final List<TitleComponent> components;
+    @Getter private final Can<TitleComponent> components;
 
-    public static class TitleComponent {
-        public static final Function<Annotations.Evaluator<Title>, TitleComponent> FROM_EVALUATORS =
-                titleEvaluator -> TitleComponent.of(titleEvaluator);
-
-                private final String prepend;
-                private final String append;
-                private final Annotations.Evaluator<Title> titleEvaluator;
-                private final int abbreviateTo;
-
-                private TitleComponent(final String prepend, final String append, final Annotations.Evaluator<Title> titleEvaluator, final int abbreviateTo) {
-                    super();
-                    this.prepend = prepend;
-                    this.append = append;
-                    this.titleEvaluator = titleEvaluator;
-                    this.abbreviateTo = abbreviateTo;
-                }
-
-                public String getPrepend() {
-                    return prepend;
-                }
-
-                public String getAppend() {
-                    return append;
-                }
-
-                public Annotations.Evaluator<Title> getTitleEvaluator() {
-                    return titleEvaluator;
-                }
+    @Getter(onMethod_ = {@Override}) private final @NonNull Can<Method> methods;
 
-                private static TitleComponent of(final Annotations.Evaluator<Title> titleEvaluator) {
-                    final Title annotation = titleEvaluator.getAnnotation();
-                    final String prepend = annotation != null ? annotation.prepend() : " ";
-                    final String append = annotation != null ? annotation.append() : "";
-                    final int abbreviateTo = annotation != null ? annotation.abbreviatedTo() : Integer.MAX_VALUE;
-                    return new TitleComponent(prepend, append, titleEvaluator, abbreviateTo);
-                }
+    public TitleFacetViaTitleAnnotation(final Can<TitleComponent> components, final FacetHolder holder) {
+        super(holder);
+        this.components = components;
 
-                @Override
-                public String toString() {
-                    final List<String> parts = _Lists.newArrayList();
-                    if(prepend != null && !_Strings.isNullOrEmpty(prepend.trim())) {
-                        parts.add("prepend=" + prepend);
-                    }
-                    if(append != null && !_Strings.isNullOrEmpty(append.trim())) {
-                        parts.add("append=" + append);
-                    }
-                    if(abbreviateTo != Integer.MAX_VALUE) {
-                        parts.add("abbreviateTo=" + abbreviateTo);
-                    }
-                    return String.join(";", parts);
-                }
+        // if there is just a single component and it happens to be a method (not a field)
+        // we can use imperative facet semantics which allows for TitleFacets to be compared by
+        // TitleFacetAbstract#semanticEquals(..) in support of more rigorous MM validation
+        this.methods = components.isCardinalityOne()
+                ? components
+                    .stream()
+                    .map(TitleComponent::getTitleEvaluator)
+                    .filter(MethodEvaluator.class::isInstance)
+                    .map(MethodEvaluator.class::cast)
+                    .map(MethodEvaluator::getMethod)
+                    .findFirst()
+                    .map(ImperativeFacet::singleMethod)
+                    .orElse(Can.empty())
+                : Can.empty();
     }
 
-    public TitleFacetViaTitleAnnotation(final List<TitleComponent> components, final FacetHolder holder) {
-        super(holder);
-        this.components = components;
+    @Override
+    public Intent getIntent(final Method method) {
+        return Intent.UI_HINT;
     }
 
     @Override
@@ -112,10 +87,6 @@ extends TitleFacetAbstract {
         return adapter.titleString();
     }
 
-    public List<TitleComponent> getComponents() {
-        return components;
-    }
-
     private static String abbreviated(final String str, final int maxLength) {
         return str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "...";
     }
@@ -171,8 +142,53 @@ extends TitleFacetAbstract {
     @Override
     public void visitAttributes(final BiConsumer<String, Object> visitor) {
         super.visitAttributes(visitor);
-        if(components != null && !_Strings.isNullOrEmpty(components.toString())) {
+        if(components != null && _Strings.isNotEmpty(components.toString())) {
             visitor.accept("components", components);
         }
     }
+
+    // -- HELPER
+
+    public static class TitleComponent {
+
+        public static TitleComponent of(final Annotations.Evaluator<Title> titleEvaluator) {
+            final Title annotation = titleEvaluator.getAnnotation();
+            final String prepend = annotation != null ? annotation.prepend() : " ";
+            final String append = annotation != null ? annotation.append() : "";
+            final int abbreviateTo = annotation != null ? annotation.abbreviatedTo() : Integer.MAX_VALUE;
+            return new TitleComponent(prepend, append, titleEvaluator, abbreviateTo);
+        }
+
+        @Getter private final String prepend;
+        @Getter private final String append;
+        @Getter private final Annotations.Evaluator<Title> titleEvaluator;
+        private final int abbreviateTo;
+
+        private TitleComponent(
+                final String prepend,
+                final String append,
+                final Annotations.Evaluator<Title> titleEvaluator,
+                final int abbreviateTo) {
+            super();
+            this.prepend = prepend;
+            this.append = append;
+            this.titleEvaluator = titleEvaluator;
+            this.abbreviateTo = abbreviateTo;
+        }
+
+        @Override
+        public String toString() {
+            final List<String> parts = _Lists.newArrayList();
+            if(prepend != null && !_Strings.isNullOrEmpty(prepend.trim())) {
+                parts.add("prepend=" + prepend);
+            }
+            if(append != null && !_Strings.isNullOrEmpty(append.trim())) {
+                parts.add("append=" + append);
+            }
+            if(abbreviateTo != Integer.MAX_VALUE) {
+                parts.add("abbreviateTo=" + abbreviateTo);
+            }
+            return String.join(";", parts);
+        }
+    }
 }
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleAnnotationFacetFactoryTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleAnnotationFacetFactoryTest.java
index 23e0beb..f614d09 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleAnnotationFacetFactoryTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleAnnotationFacetFactoryTest.java
@@ -86,7 +86,8 @@ extends AbstractFacetFactoryJUnit4TestCase {
 
         final List<Method> titleMethods = Arrays.asList(Customer.class.getMethod("someTitle"));
         for (int i = 0; i < titleMethods.size(); i++) {
-            final Annotations.MethodEvaluator<Title> titleEvaluator = (Annotations.MethodEvaluator<Title>) titleFacetViaTitleAnnotation.getComponents().get(i)
+            final Annotations.MethodEvaluator<Title> titleEvaluator =
+                    (Annotations.MethodEvaluator<Title>) titleFacetViaTitleAnnotation.getComponents().getElseFail(i)
                     .getTitleEvaluator();
 
             Assert.assertEquals(titleMethods.get(i),
@@ -129,7 +130,8 @@ extends AbstractFacetFactoryJUnit4TestCase {
 
         //final List<TitleComponent> components = titleFacetViaTitleAnnotation.getComponents();
         for (int i = 0; i < titleMethods.size(); i++) {
-            final Annotations.MethodEvaluator<Title> titleEvaluator = (Annotations.MethodEvaluator<Title>) titleFacetViaTitleAnnotation.getComponents().get(i)
+            final Annotations.MethodEvaluator<Title> titleEvaluator =
+                    (Annotations.MethodEvaluator<Title>) titleFacetViaTitleAnnotation.getComponents().getElseFail(i)
                     .getTitleEvaluator();
 
             Assert.assertEquals(titleMethods.get(i),
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleFacetViaTitleAnnotationTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleFacetViaTitleAnnotationTest.java
index e18dc5b..0bdde4d 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleFacetViaTitleAnnotationTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/ident/title/annotation/TitleFacetViaTitleAnnotationTest.java
@@ -28,8 +28,11 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
 import org.apache.isis.applib.annotation.Title;
-import org.apache.isis.commons.internal.collections._Lists;
+import org.apache.isis.commons.collections.Can;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2.Mode;
 import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
@@ -41,8 +44,7 @@ import org.apache.isis.core.metamodel.facets.object.title.annotation.TitleFacetV
 import org.apache.isis.core.metamodel.objectmanager.ObjectManager;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
+import lombok.val;
 
 public class TitleFacetViaTitleAnnotationTest {
 
@@ -54,7 +56,7 @@ public class TitleFacetViaTitleAnnotationTest {
     @Mock private ObjectManager mockObjectManager;
 
     protected MetaModelContext metaModelContext;
-    
+
     protected static class DomainObjectWithProblemInItsAnnotatedTitleMethod {
 
         @Title
@@ -98,16 +100,17 @@ public class TitleFacetViaTitleAnnotationTest {
 
         TitleAnnotationFacetFactory.sort(evaluatorList);
 
-        final List<TitleFacetViaTitleAnnotation.TitleComponent> components = _Lists.map(evaluatorList, TitleFacetViaTitleAnnotation.TitleComponent.FROM_EVALUATORS);
+        val components = Can.ofCollection(evaluatorList)
+                .map(TitleFacetViaTitleAnnotation.TitleComponent::of);
         final TitleFacetViaTitleAnnotation facet = new TitleFacetViaTitleAnnotation(components, mockFacetHolder);
         final NormalDomainObject normalPojo = new NormalDomainObject();
         final Sequence sequence = context.sequence("in-title-element-order");
         context.checking(new Expectations() {
             {
-                
+
                 allowing(mockFacetHolder).getMetaModelContext();
                 will(returnValue(metaModelContext));
-                
+
                 allowing(mockManagedObject).getPojo();
                 will(returnValue(normalPojo));
 
@@ -132,15 +135,16 @@ public class TitleFacetViaTitleAnnotationTest {
         final List<Annotations.Evaluator<Title>> evaluators = Annotations
                 .getEvaluators(DomainObjectWithProblemInItsAnnotatedTitleMethod.class, Title.class);
 
-        final List<TitleFacetViaTitleAnnotation.TitleComponent> components = _Lists.map(evaluators, TitleFacetViaTitleAnnotation.TitleComponent.FROM_EVALUATORS);
+        val components = Can.ofCollection(evaluators)
+                .map(TitleFacetViaTitleAnnotation.TitleComponent::of);
         final TitleFacetViaTitleAnnotation facet = new TitleFacetViaTitleAnnotation(components, mockFacetHolder);
         final DomainObjectWithProblemInItsAnnotatedTitleMethod screwedPojo = new DomainObjectWithProblemInItsAnnotatedTitleMethod();
         context.checking(new Expectations() {
             {
-                
+
                 allowing(mockFacetHolder).getMetaModelContext();
                 will(returnValue(metaModelContext));
-                
+
                 allowing(mockManagedObject).getPojo();
                 will(returnValue(screwedPojo));
             }
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/domain/actions/ActionLayout/promptStyle/ActionLayoutPromptStyleVm.java b/examples/demo/domain/src/main/java/demoapp/dom/domain/actions/ActionLayout/promptStyle/ActionLayoutPromptStyleVm.java
index 88ceff5..fbb4f87 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/domain/actions/ActionLayout/promptStyle/ActionLayoutPromptStyleVm.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/domain/actions/ActionLayout/promptStyle/ActionLayoutPromptStyleVm.java
@@ -52,7 +52,7 @@ public class ActionLayoutPromptStyleVm implements HasAsciiDocDescription {
         return "ActionLayout#promptStyle";
     }
 
-    @Title
+    //@Title //TODO is in conflict with the above, which one shall it be?
     @Property(editing = Editing.DISABLED)
     @PropertyLayout(fieldSetId = "general", sequence = "1")
     @XmlElement(required = true)