You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2021/09/07 13:31:56 UTC

[tapestry-5] 01/01: TAP5-2692: @StaticActivationContextValue

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

thiagohp pushed a commit to branch rest
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit b58a865881495f4494909029614b6d180182eec1
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Tue Sep 7 10:24:36 2021 -0300

    TAP5-2692: @StaticActivationContextValue
---
 .../annotations/StaticActivationContextValue.java  | 47 +++++++++++++
 .../internal/transform/OnEventWorker.java          | 60 ++++++++++++++--
 .../apache/tapestry5/runtime/ComponentEvent.java   | 35 ++++++++++
 .../test/app1/StaticActivationContextValueDemo.tml | 27 ++++++++
 .../tapestry5/integration/app1/MiscTests.groovy    | 16 +++++
 .../tapestry5/integration/app1/pages/Index.java    |  4 +-
 .../pages/StaticActivationContextValueDemo.java    | 81 ++++++++++++++++++++++
 7 files changed, 264 insertions(+), 6 deletions(-)

diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/StaticActivationContextValue.java b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/StaticActivationContextValue.java
new file mode 100644
index 0000000..c87fd44
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/StaticActivationContextValue.java
@@ -0,0 +1,47 @@
+// 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.
+
+package org.apache.tapestry5.annotations;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.apache.tapestry5.ioc.annotations.AnnotationUseContext.PAGE;
+import static org.apache.tapestry5.ioc.annotations.AnnotationUseContext.COMPONENT;
+import static org.apache.tapestry5.ioc.annotations.AnnotationUseContext.MIXIN;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.tapestry5.ioc.annotations.UseWith;
+
+/**
+ * Annotation that may be placed on parameters of event handler methods to define them
+ * as having a static value. If, in a given request, the activation context value for that
+ * parameter doesn't match the static value, the event handler method won't be called.
+ * This is particularly useful for REST endpoint event handler methods in pages. 
+ *  
+ * @since 5.8.0
+ */
+@Target(
+{ PARAMETER })
+@Retention(RUNTIME)
+@Documented
+@UseWith(
+{ PAGE, COMPONENT, MIXIN })
+public @interface StaticActivationContextValue
+{
+    /** 
+     * The activation context value, case sensitive. Empty values or ones with spaces are forbidden. 
+     */
+    String value();
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/OnEventWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/OnEventWorker.java
index 8436b26..9b570e2 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/OnEventWorker.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/OnEventWorker.java
@@ -16,6 +16,7 @@ import java.lang.reflect.Array;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 import org.apache.tapestry5.ComponentResources;
 import org.apache.tapestry5.EventContext;
@@ -23,6 +24,7 @@ import org.apache.tapestry5.ValueEncoder;
 import org.apache.tapestry5.annotations.OnEvent;
 import org.apache.tapestry5.annotations.PublishEvent;
 import org.apache.tapestry5.annotations.RequestParameter;
+import org.apache.tapestry5.annotations.StaticActivationContextValue;
 import org.apache.tapestry5.annotations.DisableStrictChecks;
 import org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.commons.util.CollectionFactory;
@@ -48,6 +50,7 @@ import org.apache.tapestry5.plastic.LocalVariableCallback;
 import org.apache.tapestry5.plastic.MethodAdvice;
 import org.apache.tapestry5.plastic.MethodDescription;
 import org.apache.tapestry5.plastic.MethodInvocation;
+import org.apache.tapestry5.plastic.MethodParameter;
 import org.apache.tapestry5.plastic.PlasticClass;
 import org.apache.tapestry5.plastic.PlasticField;
 import org.apache.tapestry5.plastic.PlasticMethod;
@@ -74,8 +77,6 @@ public class OnEventWorker implements ComponentClassTransformWorker2
 
     private final OperationTracker operationTracker;
 
-    private final boolean componentIdCheck = true;
-
     private final InstructionBuilderCallback RETURN_TRUE = new InstructionBuilderCallback()
     {
         public void doBuild(InstructionBuilder builder)
@@ -167,6 +168,8 @@ public class OnEventWorker implements ComponentClassTransformWorker2
 
         boolean handleActivationEventContext = false;
         
+        final String[] staticActivationContextValues;
+        
         final PublishEvent publishEvent;
 
         EventHandlerMethod(PlasticMethod method)
@@ -184,6 +187,40 @@ public class OnEventWorker implements ComponentClassTransformWorker2
             componentId = extractComponentId(methodName, onEvent);
             
             publishEvent = method.getAnnotation(PublishEvent.class);
+            staticActivationContextValues = extractStaticActivationContextValues(method);
+        }
+        
+        final private Pattern WHITESPACE = Pattern.compile(".*\\s.*");
+
+        private String[] extractStaticActivationContextValues(PlasticMethod method)
+        {
+            String[] values = null;
+            for (int i = 0; i < method.getParameters().size(); i++) 
+            {
+                MethodParameter parameter = method.getParameters().get(i);
+                final StaticActivationContextValue staticValue = parameter.getAnnotation(StaticActivationContextValue.class);
+                if (staticValue != null) 
+                {
+                    if (values == null) 
+                    {
+                        values = new String[method.getParameters().size()];
+                    }
+                    String value = staticValue.value();
+                    if (value != null && !value.isEmpty() && !WHITESPACE.matcher(value).matches())
+                    {
+                        values[i] = value;
+                    }
+                    else 
+                    {
+                        throw new RuntimeException(String.format("%s has at least one parameter "
+                                + "with a @%s annotation with an invalid value (empty string or "
+                                + "value containing whitespace)",
+                                method.getMethodIdentifier(),
+                                StaticActivationContextValue.class.getSimpleName()));
+                    }
+                }
+            }
+            return values;
         }
 
         void buildMatchAndInvocation(InstructionBuilder builder, final LocalVariable resultVariable)
@@ -192,8 +229,21 @@ public class OnEventWorker implements ComponentClassTransformWorker2
                     parameterSource == null ? null
                             : method.getPlasticClass().introduceField(EventHandlerMethodParameterSource.class, description.methodName + "$parameterSource").inject(parameterSource);
 
+            final PlasticField staticActivationContextValueField =
+                    staticActivationContextValues == null ? null
+                            : method.getPlasticClass().introduceField(String[].class, description.methodName + "$staticActivationContextValues").inject(staticActivationContextValues);
+            
             builder.loadArgument(0).loadConstant(eventType).loadConstant(componentId).loadConstant(minContextValues);
-            builder.invoke(ComponentEvent.class, boolean.class, "matches", String.class, String.class, int.class);
+            if (staticActivationContextValueField != null)
+            {
+                builder.loadThis().getField(staticActivationContextValueField);
+            }
+            else
+            {
+                builder.loadNull();
+            }
+            
+        builder.invoke(ComponentEvent.class, boolean.class, "matches", String.class, String.class, int.class, String[].class);
 
             builder.when(Condition.NON_ZERO, new InstructionBuilderCallback()
             {
@@ -374,13 +424,13 @@ public class OnEventWorker implements ComponentClassTransformWorker2
         {
             if (eventHandlerMethod.publishEvent != null)
             {
-                publishEvents.put(eventHandlerMethod.eventType.toLowerCase());
+                publishEvents.add(eventHandlerMethod.eventType.toLowerCase());
             }
         }
         
         // If we do have events to publish, we apply the mixin and pass
         // event information to it.
-        if (publishEvents.length() > 0) {
+        if (publishEvents.size() > 0) {
             model.addMixinClassName(PublishServerSideEvents.class.getName(), "after:*");
             model.setMeta(InternalConstants.PUBLISH_COMPONENT_EVENTS_META, publishEvents.toString());
         }
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/runtime/ComponentEvent.java b/tapestry-core/src/main/java/org/apache/tapestry5/runtime/ComponentEvent.java
index 050f1cd..e7e21fe 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/runtime/ComponentEvent.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/runtime/ComponentEvent.java
@@ -36,10 +36,45 @@ public interface ComponentEvent extends Event
      * @param parameterCount
      *            minimum number of context values
      * @return true if the event matches (and has not yet been aborted)
+     * @since 5.8.0
      */
     boolean matches(String eventType, String componentId, int parameterCount);
 
     /**
+     * Returns true if the event matches the provided criteria and the event has not yet been aborted.
+     * 
+     * @param eventType
+     *            the type of event (case insensitive match)
+     * @param componentId
+     *            component is to match against (case insensitive), or the empty string
+     * @param parameterCount
+     *            minimum number of context values
+     * @param staticActivationContextValues
+     *            a String array. If null, there are no static activation context values. If
+     *            any value in the array is null, it's considered dynamic and ignored.
+     *            I any value in the arra isn't null, it's compared to the corresponding
+     *            activation context value. If it doesn't match, this method will return null.
+     * @return true if the event matches (and has not yet been aborted)
+     */
+    default boolean matches(String eventType, String componentId, int parameterCount, String[] staticActivationContextValues)
+    {
+        boolean matches = matches(eventType, componentId, parameterCount);
+        if (matches && staticActivationContextValues != null)
+        {
+            for (int i = 0; i < staticActivationContextValues.length; i++)
+            {
+                if (staticActivationContextValues[i] != null && 
+                        !staticActivationContextValues[i].equals(getEventContext().get(String.class, i)))
+                {
+                    matches = false;
+                    break;
+                }
+            }
+        }
+        return matches;
+    }
+
+    /**
      * Coerces a context value to a particular type. The context is an array of objects; typically it is an array of
      * strings of extra path information encoded into the action URL.
      * 
diff --git a/tapestry-core/src/test/app1/StaticActivationContextValueDemo.tml b/tapestry-core/src/test/app1/StaticActivationContextValueDemo.tml
new file mode 100644
index 0000000..609b85c
--- /dev/null
+++ b/tapestry-core/src/test/app1/StaticActivationContextValueDemo.tml
@@ -0,0 +1,27 @@
+<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
+<h1>@StaticActivationContextValue Demo</h1>
+
+    <p>
+        This is way more of a test for <code>@StaticActivationContextValue</code>,
+        which is targeted at implementing multiple REST endpoints in a single component page
+        than an example of usage. Changing page or component
+        state is better done with <code>EventLink</code>. 
+    </p>
+
+    <p>
+        State: <span id="state">${state}</span>
+    </p>
+    
+    <p>
+        <a t:type="PageLink" t:page="staticActivationContextValueDemo" t:context="completed" id="completed">Mark as completed</a>
+    </p>
+
+    <p>
+        <a t:type="PageLink" t:page="staticActivationContextValueDemo" t:context="closed" id="closed">Mark as closed</a>
+    </p>
+
+    <p>
+        <a t:type="PageLink" t:page="staticActivationContextValueDemo" t:context="reset" id="reset">Reset state</a>
+    </p>
+
+</html>
diff --git a/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/MiscTests.groovy b/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/MiscTests.groovy
index 77f521c..8856ab1 100644
--- a/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/MiscTests.groovy
+++ b/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/MiscTests.groovy
@@ -95,5 +95,21 @@ class MiscTests extends App1TestCase {
         // it matches the name of a formal parameter of AltTitle.
         assertTextPresent "Parameter(s) 'AltTitle.title' are required for org.apache.tapestry5.corelib.components.ActionLink, but have not been bound."
     }
+    
+    @Test
+    void static_activation_context_value() {
+        openLinks "@StaticActivationContextValue Demo"
+        assertText "state", "none"
+        
+        click "completed"
+        assertText "state", "completed"
+
+        click "closed"
+        assertText "state", "closed"
+
+        click "reset"
+        assertText "state", "none"
+        
+    }
 
 }
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
index 4af960f..d2fbd68 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -59,7 +59,9 @@ public class Index
             .newList(
                     
                     new Item("PublishEventDemo", "@PublishEvent Demo", "Publishing server-side events to client-side code (JavaScript)"),
-                    
+
+                    new Item("StaticActivationContextValueDemo", "@StaticActivationContextValue Demo", "Demonstrates the usage of @StaticActivationContextValue"),
+
                     new Item("Html5DateFieldDemo", "Html5DateField Demo", "Choosing dates using the native HTML5 date picker"),
 
 //                    new Item("ZoneFormDemo", "Zone Form Decoration", "Fields inside an Ajax-updatd Form are still decorated properly."),
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/StaticActivationContextValueDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/StaticActivationContextValueDemo.java
new file mode 100644
index 0000000..ed57644
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/StaticActivationContextValueDemo.java
@@ -0,0 +1,81 @@
+// Copyright 2011 The Apache Software Foundation
+//
+// 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.
+
+// 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.
+package org.apache.tapestry5.integration.app1.pages;
+
+import org.apache.tapestry5.EventConstants;
+import org.apache.tapestry5.annotations.OnEvent;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.annotations.StaticActivationContextValue;
+
+public class StaticActivationContextValueDemo
+{
+    @Property
+    private String state;
+    
+    @Property
+    final private String COMPLETED = "completed"; 
+
+    @Property
+    final private String CLOSED = "closed"; 
+
+    @Property
+    final private String RESET = "reset"; 
+
+    @Property
+    final private String NONE = "none";
+    
+    @OnEvent(EventConstants.ACTIVATE)
+    void completed(@StaticActivationContextValue(COMPLETED) String ignored)
+    {
+        state = COMPLETED;
+    }
+
+    @OnEvent(EventConstants.ACTIVATE)
+    void closed(@StaticActivationContextValue(CLOSED) String ignored)
+    {
+        state = CLOSED;
+    }
+
+    @OnEvent(EventConstants.ACTIVATE)
+    void reset(@StaticActivationContextValue(RESET) String ignored)
+    {
+        state = NONE;
+    }
+    
+    void onActivate()
+    {
+        if (state == null)
+        {
+            state = NONE;
+        }
+    }
+    
+    public Object onPassivate()
+    {
+        return state != null && !NONE.equals(state) ? new String[] {state} : null;
+    }
+    
+}