Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EAK-518] Extend property scripting to respect settings declated in a chain of upstream members #529

Merged
merged 3 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@

import java.lang.reflect.Member;

/**
* Represents a {@link MemberSource} that belongs to a {@code Class} that is used as an inclusion into other classes
* (such as a {@code FieldSet} referenced by a class that defines a Touch UI dialog)
*/
public interface EmbeddedMemberSource extends MemberSource {

/**
* Retrieves an optional {@code Member} reference pointing to a member of a foreign Java class that triggered
* Retrieves an optional {@link MemberSource} reference pointing to a member of a foreign Java class that triggered
* rendering of the class that contains the current member. This is useful for rendering containers such as
* {@code FieldSet}s.
* <p><i>Ex.: Class named "{@code Foo}" contains the field {@code private FooFieldset fooFieldset;}. Class named
Expand All @@ -26,7 +31,19 @@ public interface EmbeddedMemberSource extends MemberSource {
* it will use the values {@code reportingClass = Foo.class} and {@code upstreamMember = Foo#fooFieldset}. These
* values can be then used to form up a rendering context for the current field {@code bar}: in particular, to get
* embeddable settings</i></p>
* @return A nullable {@code MemberSource} reference
*/
MemberSource getUpstreamSource();

/**
* Retrieves an optional {@link Member} reference pointing to a member of a foreign Java class that triggered
* rendering of the class that contains the current member. This is useful for rendering containers such as
* {@code FieldSet}s.
* @return A nullable {@code Member} reference
* @see #getUpstreamSource()
*/
Member getUpstreamMember();
default Member getUpstreamMember() {
MemberSource upstreamSource = getUpstreamSource();
return upstreamSource != null ? getUpstreamSource().adaptTo(Member.class) : null;
}
}
111 changes: 110 additions & 1 deletion docs/content/dev-tools/component-management/reusing-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Suppose that you have marked a `private String sampleText;` in your *HelloWorld.
To achieve this, add the `@Extends` annotation (the one that contains a pointer to *sampleText*) to *anotherText* field. Whatever widget annotation you defined for the *sampleText* field will now be "inherited" by *anotherText*.
<br>You can still add another `@TextField` to *anotherText* with properties that were not specified in *sampleText* or have different values there. Thereby "inheritance with overriding" is achieved. See the following snippet:
```java
public class CustomPropetiesDialog {
public class CustomPropertiesDialog {
@DialogField(label = "My text field")
@Extends(value = HelloWorld.class, field = "sampleText")
@TextField(emptyText = "Enter your text here")
Expand All @@ -98,3 +98,112 @@ Note: it is possible that the "parent" field in its own turn `@Extends`-es some
You should still make sure that all the fields involved have the same component annotation. A field marked with, say, `@DatePicker` will not extend some `@Checkbox` field, and so on.

Also pay attention so that when you extend a field and add another field-specific annotation to override some properties (like in the sample above), property values are either replaced or appended (like adding values from an array-typed property of "child" to the array-typed property of "parent"), but not erased. You cannot replace a non-empty value of a "parent" with a blank, or empty, value of a "child." So take care to design your "inheritance tree" starting from fields with more abstract, less populated component annotations, and then shifting to more specific ones.

## Scripting widget properties' values

Imagine that you have a fieldset `ButtonFieldSet` that comprises settings for a button such as "label", "hyperlink", "does it open in a new window?" and so on. Naturally, this fieldset contains a number of annotated fields, such as `@TextField`, `@Checkbox`, etc., and is reused across several components.

Now suppose that the requirement is such that in some of your components your "hyperlink" field must have the default value _https://google.com_, and in some other components it should be _https://bing.com_. Also, in some cases the "open in a new window" checkbox should be checked by default, and in some other cases it should be unchecked.

In this situation you would like to use some _variable_ (or, in other words, _scripted value_) with your reusable fieldset. It is so that in `MyComponentA` you use the the fieldset with the "default hyperlink" set to Google, and in `MyComponentB` you use the same fieldset with the "default hyperlink" set to Bing.

Here is a sample of how you can achieve this with the ToolKit:

```java
public class MyComponentA {
@FieldSet
@Setting(name = "defaultHyperlink", value = "https://google.com"),
@Setting(name = "openInNewWindow", value = "{Boolean}true")
private ButtonFieldSet buttonFieldSet;
}
```

```java
public class MyComponentB {
@FieldSet
@Setting(name = "defaultHyperlink", value = "https://bing.com"),
@Setting(name = "openInNewWindow", value = "{Boolean}false")
private ButtonFieldSet buttonFieldSet;
}
```

```java
public class ButtonFieldSet {
@DialogField
@TextField
private String label;

@DialogField
@TextField(value = "${ @defaultHyperlink }")
private String hyperlink;

@DialogField
@Checkbox(value = "${ @openInNewWindow }")
private boolean openInNewWindow;
}
```

The overall idea is that you declare a "named variable" with the `@Setting` annotation and then you "insert" it elsewhere into a String-typed property of another annotation using the string template like `${ @variableName }`. The format `@{ @variableName }` is also supported.

The string template does not have to be the only content of a property value. You can combine it with other text, like `value = "https://www.google.com/${@defaultPath}"`.

Every kind of property can be scripted except for properties of the `@Setting` annotation itself.

Surely, you cannot pass a string template to a boolean-typed or a numeric property. There is however a workaround. If you need to turn a non-string value into a variable, pass it via an additional `@Property` annotation like in the following example:

```java
public class MyComponent {
@FieldSet
@Setting(name = "minValue", value = "{Double}0.0")
@Setting(name = "maxValue", value = "{Double}100.0")
private MyFieldset myFieldset;
}

// ...
public class MyFieldset {
@DialogField
@NumberField(min = 0 /* an optional un-scripted default */, max = 1 /* another optional default */)
@Property(name = "min", value = "${@minValue}")
@Property(name = "max", value = "${@maxValue}")
private double value;
}

```

##### What are the places you can declare your settings in?

Basically, the ToolKit supports four sorts of settngs:
1) Settings that are attached to the same class member they are used (usually does not make much sense but is possible).
2) Settings that are attached to the <u>class</u> where the said member is declared or to any of its <u>superclasses</u>.
3) Settings that are attached to the <u>member of another class</u> that has the type of the class in which the settings are used. In the code, this is referred to as the "upstream member".

> To put it simple, imagine that there is a field named _title_ in a class named _MyFieldset_. Then, there is a field named _titleFieldset_ of type _MyFieldset_ in a class named _MyComponent_.
> <br>In this case `MyComponent.titleFieldset` is the _upstream member_ for `MyFieldset.title`. If you annotate `MyComponent.titleFieldset` with `@Setting`, this annotation is respected as the upstream setting when rendering any annotation declared at `MyFieldset.title` .

4) Settings that are attached to the <u>class where the upstream member is declared</u> or to any of its <u>superclasses</u>.

As you see it, there is some "stack" of variables that can affect rendering of the current field, and these vars can belong to different "scopes". If there are variables with the same name, the values are overridden from the most "remote" scope to the most "close" one. That is, settings declared at level #4 in the list above are overridden by settings declared at level #3, then by level #2, and finally by level #1.

##### Scripting expressions syntax

Expressions within the `${}` or `@{}` brackets are not limited to just settings' names. They basically follow the (simplified) JavaScript syntax. Therefore, you can, for example, refer to a setting with a fallback value like `${@mySetting || 'default value'}` or use a ternary like `${@mySetting ? 'value if true' : 'value if false'}`. You can also use arithmetic operations, string concatenation, and so on.

Inside the expression, you can use the special `@this` object (alias `source`). `@this` refers, naturally, to the class member that is being currently rendered.

With `@this.class` you can get the declaring class of the current member. Then you can retrieve some "properties" of the class, like `@this.class.name` or `@this.class.parent`, or else a collection of `@this.class.ancestors`.

With `@this.context` (alias `@this.upstream`) you can reach the "upstream" class member (see definition above) if the expression is being used inside a fieldset.

There is a way to retrieve a property of a declared annotation with, e.g., `@this.annotation('DialogField').label`. You can also get all the declared annotations with `@this.annotations()`. Same way you can get a particular annotation or all the annotations of the declaring class with `@this.class.annotation('Dialog')` or `@this.class.annotations()[1]`. See more of it in the [test classes](https://github.com/exadel-inc/etoolbox-authoring-kit/tree/master/plugin/src/test/com/exadel/aem/toolkit/plugin/handlers/common/cases/components).

##### Property scripting or DependsOn?

From the first look on them, the interpolatable string templates discussed above appear similar to the [DependsOn](../depends-on/introduction.md) queries. You must however understand the difference between them.

Both techniques alter the view and/or behavior of a Touch UI dialog conditionally.

_DependsOn_ does this dynamically (in runtime) in the browser. In a common scenario, _DependsOn_ is used to modify the state of a dialog widget after the user interacted with another widget of the same dialog (like showing or hiding a text field upon checking or unchecking a box).

_Property scripting_ with `@Setting`s this statically at the time of project building. It is used, e.g., to make a text field inside a fieldset display its default value as "Foo" when used within "MyComponentA" and "Bar" when used within "MyComponentB" -- all without the need to create two different fieldsets.

You cannot however make a scripted template react to a user action like you would do with a _DependsOn_ query. Also, you cannot refer to `@some_variable` declared with `@Setting` from a _DependsOn_ query, and vice versa. All in all, property scripting and _DependsOn_ modify the Touch UI experience from different angles and "live" in different worlds.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
*/
package com.exadel.aem.toolkit.plugin.handlers.placement.containers;

import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -125,7 +124,7 @@ List<Source> getAvailableForContainer(Source host, Target target) {
} else {
renderedMembers = ClassUtil.getSources(
hostClass,
host.adaptTo(Member.class),
host.adaptTo(MemberSource.class),
nonIgnoredMembersFilter,
false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.lang.reflect.Member;
import java.lang.reflect.Method;

import org.apache.commons.lang3.StringUtils;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
Expand All @@ -32,6 +33,7 @@ class MemberAdapter extends AbstractAdapter implements Annotated, Callable {

private static final String PN_CLASS = "class";
private static final String PN_CONTEXT = "context";
private static final String PN_UPSTREAM = "upstream";

private final Member reflectedMember;
private final Member upstreamMember;
Expand Down Expand Up @@ -80,7 +82,7 @@ public Object get(String name, Scriptable start) {
if (PN_CLASS.equals(name) && reflectedMember.getDeclaringClass() != null) {
return new ClassAdapter(reflectedMember.getDeclaringClass());
}
if (PN_CONTEXT.equals(name) && upstreamMember != null) {
if (StringUtils.equalsAny(name, PN_CONTEXT, PN_UPSTREAM) && upstreamMember != null) {
return new MemberAdapter(upstreamMember);
}
if (METHOD_ANNOTATION.equals(name)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;

import org.apache.commons.lang3.StringUtils;

import com.exadel.aem.toolkit.api.annotations.main.Setting;
import com.exadel.aem.toolkit.api.annotations.meta.ResourceType;
import com.exadel.aem.toolkit.api.annotations.widgets.FieldSet;
import com.exadel.aem.toolkit.api.annotations.widgets.MultiField;
import com.exadel.aem.toolkit.api.handlers.EmbeddedMemberSource;
import com.exadel.aem.toolkit.api.handlers.MemberSource;
import com.exadel.aem.toolkit.api.handlers.Source;
import com.exadel.aem.toolkit.api.markers._Default;
Expand All @@ -42,14 +43,12 @@
*/
class MemberSourceImpl extends SourceImpl implements ModifiableMemberSource {

private static final List<String> FALSY_VALUES = Arrays.asList("false", "0", "0.0", "null", "undefined");

private final Class<?> componentType;

private final Member member;
private Class<?> declaringClass;
private Class<?> reportingClass;
private Member upstreamMember;
private MemberSource upstreamSource;

private String name;

Expand Down Expand Up @@ -118,16 +117,16 @@ public void setReportingClass(Class<?> value) {
* {@inheritDoc}
*/
@Override
public Member getUpstreamMember() {
return this.upstreamMember;
public MemberSource getUpstreamSource() {
return this.upstreamSource;
}

/**
* {@inheritDoc}
*/
@Override
public void setUpstreamMember(Member value) {
this.upstreamMember = value;
public void setUpstreamSource(MemberSource value) {
this.upstreamSource = value;
}

/**
Expand Down Expand Up @@ -203,22 +202,40 @@ <T extends Annotation> T getAnnotation(Class<T> type) {
* <br>- to the class where the current member is defined and all its superclasses/interfaces;
* <br>- to the "upstream" member of the related class that triggered rendering of the current class;
* <br>- and then to all the ancestors of that "upstream" class
* @see Sources#fromMember(Member, Class, Member)
* @see Sources#fromMember(Member, Class, MemberSource)
*/
@Override
DataStack getDataStack() {
DataStack result = new DataStack();
if (upstreamMember != null) {
for (Class<?> ancestor : ClassUtil.getInheritanceTree(upstreamMember.getDeclaringClass())) {

// Collect the vector of upstreams for the current sources
LinkedList<MemberSource> upstreamSources = new LinkedList<>();
MemberSource currentUpstreamSource = upstreamSource;
while (currentUpstreamSource instanceof EmbeddedMemberSource) {
upstreamSources.add(currentUpstreamSource);
currentUpstreamSource = ((EmbeddedMemberSource) currentUpstreamSource).getUpstreamSource();
}

// Add to the stack the settings stored at the class level in the declaring classes of upstream members
// (less significant)
upstreamSources.descendingIterator().forEachRemaining(upstream -> {
for (Class<?> ancestor : ClassUtil.getInheritanceTree(upstream.getDeclaringClass())) {
result.append(ancestor.getAnnotationsByType(Setting.class));
}
}
});

// Add to the stack the settings stored at the class level in the declaring class of the current member and all
// the parent classes of the declaring class
for (Class<?> ancestor : ClassUtil.getInheritanceTree(getDeclaringClass())) {
result.append(ancestor.getAnnotationsByType(Setting.class));
}
if (upstreamMember != null) {
result.append(((AnnotatedElement) upstreamMember).getAnnotationsByType(Setting.class));
}

// Add to the stack the settings stored at the member level in all the upstream members
upstreamSources.descendingIterator().forEachRemaining(upstream -> {
result.append(upstream.adaptTo(Setting[].class));
});

// Add to the stack the settings stored at the member level locally (most significant)
result.append(adaptTo(Setting[].class));
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
*/
package com.exadel.aem.toolkit.plugin.sources;

import java.lang.reflect.Member;

import com.exadel.aem.toolkit.api.handlers.EmbeddedMemberSource;
import com.exadel.aem.toolkit.api.handlers.MemberSource;
import com.exadel.aem.toolkit.api.handlers.Source;
Expand Down Expand Up @@ -48,10 +46,10 @@ public interface ModifiableMemberSource extends EmbeddedMemberSource {
void setReportingClass(Class<?> value);

/**
* Assigns the {@link Member} value that the underlying Java field or method will be considered "reported by". This
* facility is designed for members of entities such as fieldsets. This method gives the ability to query for
* Assigns the {@link MemberSource} value that the underlying Java field or method will be considered "reported by".
* This facility is designed for members of entities such as fieldsets. This method gives the ability to query for
* metadata attached to the field/method of a class that uses the fieldset
* @param value {@code Member} reference
* @param value {@code MemberSource} reference
*/
void setUpstreamMember(Member value);
void setUpstreamSource(MemberSource value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ public static Source fromMember(Member value, Class<?> reportingClass) {
* @param value {@code Method} or a {@code Field} for which a source facade is created
* @param reportingClass Optional {@code Class<?>} pointer determining the class that "reports" to the ToolKit Maven
* plugin about the current member. See {@link MemberSource#getReportingClass()}
* @param upstreamMember Optional {@code Member} reference that triggered rendering of the class that contains the
* current member. See {@link ModifiableMemberSource#getUpstreamMember()} ()}
* @param upstreamSource Optional {@code MemberSource} reference that triggered rendering of the class that contains
* the current member. See {@link ModifiableMemberSource#getUpstreamSource()}
* @return {@code Source} instance
*/
public static Source fromMember(Member value, Class<?> reportingClass, Member upstreamMember) {
public static Source fromMember(Member value, Class<?> reportingClass, MemberSource upstreamSource) {
ModifiableMemberSource result = new MemberSourceImpl(value);
result.setReportingClass(reportingClass);
result.setUpstreamMember(upstreamMember);
result.setUpstreamSource(upstreamSource);
return result;
}

Expand Down
Loading
Loading