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/06/30 20:08:29 UTC

[isis] branch master updated: ISIS-1720: allow synthetic methods on mixin types to be considered as action candidates

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


The following commit(s) were added to refs/heads/master by this push:
     new a6bfb6a  ISIS-1720: allow synthetic methods on mixin types to be considered as action candidates
a6bfb6a is described below

commit a6bfb6ac3fe0b2fe7413bcdb3aaa1c6ce4773d1d
Author: andi-huber <ah...@apache.org>
AuthorDate: Wed Jun 30 22:07:50 2021 +0200

    ISIS-1720: allow synthetic methods on mixin types to be considered as
    action candidates
---
 .../ignore/javalang/RemoveMethodsFacetFactory.java |  14 +-
 .../specloader/specimpl/FacetedMethodsBuilder.java |  33 ++++-
 .../specimpl/ObjectSpecificationAbstract.java      |  29 ++--
 .../interact/WrapperInteractionTest3.java          | 157 +++++++++++++++++++++
 4 files changed, 215 insertions(+), 18 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java
index f32fd18..7edfe77 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java
@@ -33,6 +33,7 @@ import org.apache.isis.core.metamodel.context.MetaModelContext;
 import org.apache.isis.core.metamodel.facetapi.Facet;
 import org.apache.isis.core.metamodel.facetapi.FeatureType;
 import org.apache.isis.core.metamodel.facets.FacetFactoryAbstract;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 
 import lombok.val;
 
@@ -77,15 +78,20 @@ public class RemoveMethodsFacetFactory extends FacetFactoryAbstract {
     public void process(final ProcessClassContext processClassContext) {
         super.process(processClassContext);
 
-        Class<?> cls = processClassContext.getCls();
-        Method[] methods = cls.getMethods();
+        val cls = processClassContext.getCls();
+        val facetHolder = processClassContext.getFacetHolder();
+        val isConcreteMixin = facetHolder instanceof ObjectSpecification
+                ? ((ObjectSpecification)facetHolder).getBeanSort().isMixin()
+                : false;
+        final Method[] methods = cls.getMethods();
 
         val config = processClassContext.getFacetHolder().getMetaModelContext().getConfiguration();
         val isExplicitAction = config.getApplib().getAnnotation().getAction().isExplicit();
 
         for (Method method : methods) {
-            // remove synthetic methods
-            if (method.isSynthetic()) {
+            // remove synthetic methods (except when is a mixin)
+            if (!isConcreteMixin
+                    && method.isSynthetic()) {
                 processClassContext.removeMethod(method);
             }
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
index a7e3a4e..6a788dd 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
@@ -66,15 +66,27 @@ import lombok.extern.log4j.Log4j2;
 public class FacetedMethodsBuilder
 implements HasMetaModelContext {
 
+    private void debug(String format, Object...args) {
+        if(introspectedClass.getName().contains("WrapperInteractionTest3$Task$")) {
+            System.out.printf(format+"%n", args);
+        }
+    }
+
     private static final String GET_PREFIX = "get";
     private static final String IS_PREFIX = "is";
 
     /* thread-safety ... make sure every methodsRemaining access is synchronized! */
-    private static final class FacetedMethodsMethodRemover implements MethodRemover {
+    private static final class ConcurrentMethodRemover implements MethodRemover {
+
+        private void debug(String format, Method method) {
+            if(method.getDeclaringClass().getName().contains("WrapperInteractionTest3$Task$")) {
+                System.out.printf(format+"%n", method);
+            }
+        }
 
         private final Set<Method> methodsRemaining;
 
-        private FacetedMethodsMethodRemover(final Class<?> introspectedClass, final Method[] methods) {
+        private ConcurrentMethodRemover(final Class<?> introspectedClass, final Method[] methods) {
             this.methodsRemaining = Stream.of(methods)
                     .filter(_NullSafe::isPresent)
                     .collect(Collectors.toCollection(_Sets::newConcurrentHashSet));
@@ -86,7 +98,9 @@ implements HasMetaModelContext {
                 val doRemove = removeIf.test(method);
                 if(doRemove) {
                     onRemoval.accept(method);
+                    //debug("accept method-2 %s", method);
                 }
+                //debug("skipping method-2 %s", method);
                 return doRemove;
             });
         }
@@ -96,6 +110,10 @@ implements HasMetaModelContext {
             if(method==null) {
                 return;
             }
+            if(method.toString().contains("Succeeded.act(")) {
+                System.out.println("!!!gotcha " + method);
+            }
+            debug("remove method %s", method);
             methodsRemaining.remove(method);
         }
 
@@ -117,7 +135,7 @@ implements HasMetaModelContext {
     private List<FacetedMethod> associationFacetMethods;
     private List<FacetedMethod> actionFacetedMethods;
 
-    private final FacetedMethodsMethodRemover methodRemover;
+    private final ConcurrentMethodRemover methodRemover;
 
     @Getter private final FacetProcessor facetProcessor;
 
@@ -144,8 +162,12 @@ implements HasMetaModelContext {
         this.explicitAnnotationsForActions =
                 getConfiguration().getApplib().getAnnotation().getAction().isExplicit();
 
+        if(introspectedClass.getName().endsWith("WrapperInteractionTest3$Task$Succeeded")) {
+            System.out.printf("introspectedClass[%s]: %s%n", introspectedClass, Can.ofArray(introspectedClass.getMethods()));
+        }
+
         val methodsRemaining = introspectedClass.getMethods();
-        this.methodRemover = new FacetedMethodsMethodRemover(introspectedClass, methodsRemaining);
+        this.methodRemover = new ConcurrentMethodRemover(introspectedClass, methodsRemaining);
 
     }
 
@@ -367,6 +389,7 @@ implements HasMetaModelContext {
                 onActionFacetedMethod.accept(actionPeer);
                 return true;
             }
+            debug("skipping method %s", method);
             return false;
         });
 
@@ -375,6 +398,8 @@ implements HasMetaModelContext {
     private FacetedMethod findActionFacetedMethod(
             final Method actionMethod) {
 
+        debug("represents action %s -> %b", actionMethod, representsAction(actionMethod));
+
         if (!representsAction(actionMethod)) {
             return null;
         }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index efbfbe7..08585e6 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -272,6 +272,10 @@ implements ObjectSpecification {
             log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo);
         }
 
+        if(this.getCorrespondingClass().getName().endsWith("WrapperInteractionTest3$Task$MixinAbstract")) {
+            //System.out.printf("introspectUpTo[%s]: %s%n", this.getCorrespondingClass(), upTo);
+        }
+
         boolean revalidate = false;
 
         switch (introspectionState) {
@@ -769,11 +773,10 @@ implements ObjectSpecification {
 
     // -- mixin actions
     /**
-     * All contributee actions (each wrapping a service's contributed action) for this spec.
-     *
+     * All mixed in actions for this spec.
      * <p>
-     * If this specification {@link #isManagedBean() is actually for} a service,
-     * then returns an empty list.
+     * If this specification represents a service ({@link #isManagedBean()} or a value or
+     * a mixin, then is a no-op.
      */
     private void createMixedInActions(final Consumer<ObjectAction> onNewMixedInAction) {
         if (isManagedBean() || isValue() || isMixin()) {
@@ -784,6 +787,12 @@ implements ObjectSpecification {
             return;
         }
         for (val mixinType : mixinTypes) {
+
+            if(mixinType.getName().contains("WrapperInteractionTest")
+                    && this.getCorrespondingClass().getName().endsWith("WrapperInteractionTest3$Task")) {
+                System.out.printf("mixinType[%s]: %s%n", this.getCorrespondingClass(), mixinType);
+            }
+
             forEachMixedInAction(mixinType, onNewMixedInAction);
         }
     }
@@ -876,8 +885,8 @@ implements ObjectSpecification {
 
     // -- GUARDS
 
-    private boolean contributeeAndMixedInAssociationsAdded;
-    private boolean contributeeAndMixedInActionsAdded;
+    private boolean mixedInAssociationsAdded;
+    private boolean mixedInActionsAdded;
 
     private void createMixedInActions() {
         // update our list of actions if requesting for contributed actions
@@ -885,13 +894,13 @@ implements ObjectSpecification {
         // the "contributed.isIncluded()" guard is required because we cannot do this too early;
         // there must be a session available
         synchronized (unmodifiableActions) {
-            if(!contributeeAndMixedInActionsAdded) {
+            if(!mixedInActionsAdded) {
                 val actions = _Lists.newArrayList(this.objectActions);
                 if (isEntityOrViewModelOrAbstract()) {
                     createMixedInActions(actions::add);
                 }
                 sortCacheAndUpdateActions(actions);
-                contributeeAndMixedInActionsAdded = true;
+                mixedInActionsAdded = true;
             }
         }
     }
@@ -900,13 +909,13 @@ implements ObjectSpecification {
         // the "contributed.isIncluded()" guard is required because we cannot do this too early;
         // there must be a session available
         synchronized (unmodifiableAssociations) {
-            if(!contributeeAndMixedInAssociationsAdded) {
+            if(!mixedInAssociationsAdded) {
                 val associations = _Lists.newArrayList(this.associations);
                 if(isEntityOrViewModelOrAbstract()) {
                     createMixedInAssociations(associations::add);
                 }
                 sortAndUpdateAssociations(associations);
-                contributeeAndMixedInAssociationsAdded = true;
+                mixedInAssociationsAdded = true;
             }
         }
     }
diff --git a/regressiontests/stable-interact/src/test/java/org/apache/isis/testdomain/interact/WrapperInteractionTest3.java b/regressiontests/stable-interact/src/test/java/org/apache/isis/testdomain/interact/WrapperInteractionTest3.java
new file mode 100644
index 0000000..b45571e
--- /dev/null
+++ b/regressiontests/stable-interact/src/test/java/org/apache/isis/testdomain/interact/WrapperInteractionTest3.java
@@ -0,0 +1,157 @@
+/*
+ *  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.testdomain.interact;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestPropertySource;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Nature;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.core.config.presets.IsisPresets;
+import org.apache.isis.core.metamodel.facets.all.named.MemberNamedFacet;
+import org.apache.isis.core.metamodel.spec.feature.MixedIn;
+import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
+import org.apache.isis.testdomain.conf.Configuration_headless;
+import org.apache.isis.testdomain.model.interaction.Configuration_usingInteractionDomain;
+import org.apache.isis.testdomain.util.interaction.InteractionTestAbstract;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@SpringBootTest(
+        classes = {
+                Configuration_headless.class,
+                Configuration_usingInteractionDomain.class,
+
+                WrapperInteractionTest3.Task.class,
+                WrapperInteractionTest3.Task.Succeeded.class,
+                WrapperInteractionTest3.Task.Failed.class,
+        }
+)
+@TestPropertySource({
+    IsisPresets.SilenceMetaModel,
+    IsisPresets.SilenceProgrammingModel
+})
+class WrapperInteractionTest3
+extends InteractionTestAbstract {
+
+    @Data @DomainObject(nature = Nature.VIEW_MODEL)
+    public static class Task {
+
+        @RequiredArgsConstructor
+        enum Outcome {
+            SUPER(true),
+            GREAT(true),
+            OK(true),
+            BAD(false),
+            TERRIBLE(false),
+            JUST_GIVE_UP(false),;
+
+            final boolean representsSuccess;
+
+            public static List<Outcome> successes() {
+                return Arrays.stream(Outcome.values()).filter(x -> x.representsSuccess).collect(Collectors.toList());
+            }
+            public static List<Outcome> failures() {
+                return Arrays.stream(Outcome.values()).filter(x -> ! x.representsSuccess).collect(Collectors.toList());
+            }
+        }
+
+        @Property(optionality = Optionality.OPTIONAL)
+        Outcome outcome;
+
+        @Action
+        public class Succeeded
+        extends MixinAbstract {
+            public List<Task.Outcome> choices0Act() { return Task.Outcome.successes(); }
+        }
+
+        @Action
+        public class Failed
+        extends MixinAbstract {
+            public List<Task.Outcome> choices0Act() { return Task.Outcome.failures(); }
+        }
+
+        // an abstract mixin class
+        abstract class MixinAbstract {
+            public Task act(Task.Outcome outcome) {
+                Task.this.outcome = outcome;
+                return Task.this;
+            }
+        }
+    }
+
+    @Inject SpecificationLoader specificationLoader;
+
+    @Test
+    void mixinMemberNamedFacet_whenSharingSameAbstractMixin() {
+
+        val objectSpec1 = specificationLoader.specForType(Task.Succeeded.class).get();
+        val objectSpec2 = specificationLoader.specForType(Task.Failed.class).get();
+
+        assertTrue(objectSpec1.isMixin());
+        assertTrue(objectSpec2.isMixin());
+
+        assertEquals("Succeeded", objectSpec1.getSingularName());
+        assertEquals("Failed", objectSpec2.getSingularName());
+
+        val objectSpec = specificationLoader.specForType(Task.class).get();
+
+        assertEquals(
+                2L,
+                objectSpec.streamRuntimeActions(MixedIn.INCLUDED)
+                //.filter(ObjectAction::isMixedIn)
+                .peek(act->{
+                    System.out.println("act: " + act);
+                    val memberNamedFacet = act.getFacet(MemberNamedFacet.class);
+                    assertNotNull(memberNamedFacet);
+                    assertTrue(memberNamedFacet.getSpecialization().isLeft());
+                })
+                .count());
+    }
+
+    @Test
+    void mixinActionValidation() {
+
+        final Task task = new Task();
+
+        wrapMixin(Task.Succeeded.class, task).act(Task.Outcome.SUPER);
+        Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.SUPER);
+
+        wrapMixin(Task.Failed.class, task).act(Task.Outcome.JUST_GIVE_UP);
+        Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.JUST_GIVE_UP);
+    }
+
+}