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

AutoLink improvements #346

Merged
merged 3 commits into from
Nov 23, 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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
- Converter methods are now honored for Alternatives methods as well. (see [#270](https://github.com/klum-dsl/klum-ast/issues/270))
- `@Validate` now can be placed on classes. This effectively replaces `@Validate(option=Validation.Option.VALIDATE_UNMARKED)`, which is internally converted to the new format (see [#276](https://github.com/klum-dsl/klum-ast/issues/276)). The `@Validation` annotation is deprecated.
- Sanity check: Key Fields must not have `@Owner` or `@Field` annotations.
- Selector members for `@LinkTo` annotations allows to determine the link source from the provider based on the value of another field (see [#302](https://github.com/klum-dsl/klum-ast/issues/302))
- @LinkTo now correctly handles empty collections/maps as target

## Deprecations (see [Migration](https://github.com/klum-dsl/klum-ast/wiki/Migration)):
- The `@Validation` annotation is deprecated. Use `@Validate` on class level instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@

/**
* <p>Designates a default value for the given field. This automatically sets the field to
* that default value when the value of the annotated field is empty (as defined by Groovy Truth) during
* the default lifecycle phase..</p>
* that default value when the value of the annotated field is empty (null or empty collection/map) during
* the default lifecycle phase.</p>
*
* <p>The default target as decided by the members must be exactly one of:</p>
* <ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
*/
package com.blackbuild.klum.ast.util.layer3.annotations;

import com.blackbuild.groovy.configdsl.transform.NamedAnnotationMemberClosure;
import com.blackbuild.groovy.configdsl.transform.NoClosure;
import com.blackbuild.groovy.configdsl.transform.WriteAccess;
import com.blackbuild.groovy.configdsl.transform.cast.NeedsDSLClass;
import com.blackbuild.klum.cast.KlumCastValidated;
import com.blackbuild.klum.cast.checks.AlsoNeeds;
import com.blackbuild.klum.cast.checks.MutuallyExclusive;
import com.blackbuild.klum.cast.checks.NotOn;
import groovy.lang.Closure;

import java.lang.annotation.*;
Expand Down Expand Up @@ -83,14 +84,15 @@
*
* If the provider is a map, the field name is used as the key to access the provider. If the key does not exist, the link is not set.
*
* <h3>target field</h3>
* <h3>Target field</h3>
*
* Once the provider is determined, the field of the provider to be used as the provider of the link is resolved. This is done the following way:
* <ul>
* <li>If the field member is set, the field with the given name is used</li>
* <li>if the fieldId member is set, the field with the matching LinkSource annotation is taken. It is illegal
* to have field and fieldId set together</li>
* <li>if neither field nor fieldId is set, the field with the same name as the annotated field is used</li>
* <li>if the fieldId member is set, the field with the matching LinkSource annotation is taken</li>
* <li>if the selector member is set, the field of the provider is determined by the value of the selector field of the annotated field's instance</li>
* <li>It is illegal to have more than one of field, fieldId and selector set together</li>
* <li>if neither field, fieldId nor selector is set, the field with the same name as the annotated field is used</li>
* <li>if no field with the given name exists and exactly one field not annotated with LinkSource and of the correct type exists, that one is used</li>
* <li>if no matching field is found, an exception is thrown</li>
* </ul>
Expand All @@ -101,25 +103,32 @@
@KlumCastValidated
@NeedsDSLClass
@MutuallyExclusive({"provider", "providerType"})
@MutuallyExclusive({"field", "fieldId", "selector"})
@Inherited
@Documented
public @interface LinkTo {

/**
* The field of the target owner object to be used as the target for the link.
*/
String field() default "";
@NotOn(ElementType.TYPE) String field() default "";

/**
* If set use the field of the owner with a matching LinkSource annotation with the same id. Only one
* of field and targetId can be used at most.
*/
String fieldId() default "";
@NotOn(ElementType.TYPE) String fieldId() default "";

/**
* If set, the field of the provider is determined by the given selector. The selector is the name of a field
* of the receiver. If the selector field is empty or null, the link is not set.
*/
@NotOn(ElementType.TYPE) String selector() default "";

/**
* The owner of the link. By default, the owner of the annotated field's instance is used.
*/
Class<? extends Closure<Object>> provider() default None.class;
Class<? extends Closure<Object>> provider() default NoClosure.class;

/** If set, the owner is determined by walking the owner hierarchy up until the given type is found. */
Class<?> providerType() default Object.class;
Expand All @@ -128,29 +137,24 @@
* If set, determines the strategy to determine which field of the provider is to be used as the link source.
* FIELD_NAME: use the field with the same name as the annotated field, i.e. if the annotated field is called
* 'admin', the field 'admin' of the provider is used.
* INSTANCE_NAME: use the field with the same name as the instance name of the annotated field's owner, i.e. the
* OWNER_PATH: use the field with the same name as the instance name of the annotated field's owner, i.e. the
* name of the field of the annotated field's classes owner pointing to the instance of the annotated field's container.
* Can only be set together with one of provider or providerType.
*/
@AlsoNeeds({"provider", "providerType"})
Strategy strategy() default Strategy.AUTO;

/**
* If set, is added to automatically determined names (i.e. FIELD_NAME or INSTANCE_NAME).
* If set, is added to automatically determined names (i.e. FIELD_NAME or OWNER_PATH).
*/
String nameSuffix() default "";
/**
* Marker class for default value.
*/
class None extends NamedAnnotationMemberClosure<Object> {
public None(Object owner, Object thisObject) {
super(owner, thisObject);
}
}

enum Strategy {
/** Choose the single match between FIELD_NAME or OWNER_PATH depending on the context. */
AUTO,
/** Use the field of the provider with the same name as the annotated field. */
FIELD_NAME,
INSTANCE_NAME
/** Use the field of the provider with the same name as the field in the owner pointing to the annotated field's object. */
OWNER_PATH
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
package com.blackbuild.klum.ast.util.layer3.annotations;


import com.blackbuild.groovy.configdsl.transform.NoClosure;
import groovy.lang.Closure;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -61,11 +62,16 @@ public String fieldId() {
return fromField.fieldId();
}

@Override
public String selector() {
return fromField.selector();
}

@Override
public Class<? extends Closure<Object>> provider() {
if (!fromField.provider().equals(None.class)) return fromField.provider();
if (fromField.provider() != NoClosure.class) return fromField.provider();
if (fromClass != null) return fromClass.provider();
return None.class;
return NoClosure.class;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
*/
package com.blackbuild.klum.ast.process;

import java.util.Collection;
import java.util.Map;

/**
* Represents an action that is executed in a phase. The action is executed for each element in the model.
*/
Expand Down Expand Up @@ -53,4 +56,14 @@ public int getPhase() {
public String getPhaseName() {
return phaseName;
}

protected boolean isUnset(Map.Entry<String, Object> entry) {
Object value = entry.getValue();
if (value == null) return true;
if (value instanceof Collection)
return ((Collection<?>) value).isEmpty();
if (value instanceof Map)
return ((Map<?, ?>) value).isEmpty();
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import com.blackbuild.klum.ast.process.VisitingPhaseAction;
import com.blackbuild.klum.ast.util.layer3.ClusterModel;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

import java.lang.reflect.Field;

Expand All @@ -45,7 +44,7 @@ public void visit(String path, Object element, Object container) {
ClusterModel.getFieldsAnnotatedWith(element, Default.class)
.entrySet()
.stream()
.filter(entry -> !DefaultTypeTransformation.castToBoolean(entry.getValue()))
.filter(this::isUnset)
.forEach(entry -> applyDefaultValue(element, entry.getKey()));
LifecycleHelper.executeLifecycleMethods(KlumInstanceProxy.getProxyFor(element), Default.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void visit(String path, Object element, Object container) {
ClusterModel.getFieldsAnnotatedWith(element, LinkTo.class)
.entrySet()
.stream()
.filter(entry -> entry.getValue() == null)
.filter(this::isUnset)
.forEach(entry -> LinkHelper.autoLink(element, entry.getKey()));

LifecycleHelper.executeLifecycleMethods(KlumInstanceProxy.getProxyFor(element), AutoLink.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,30 @@
*/
package com.blackbuild.klum.ast.util.layer3;

import com.blackbuild.groovy.configdsl.transform.NoClosure;
import com.blackbuild.klum.ast.util.ClosureHelper;
import com.blackbuild.klum.ast.util.KlumInstanceProxy;
import com.blackbuild.klum.ast.util.layer3.annotations.LinkSource;
import com.blackbuild.klum.ast.util.layer3.annotations.LinkTo;
import com.blackbuild.klum.ast.util.layer3.annotations.LinkToWrapper;
import groovy.lang.MetaProperty;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static java.lang.String.format;

public class LinkHelper {

private LinkHelper() {}
private LinkHelper() {
}

static void autoLink(Object container, String fieldName) {
KlumInstanceProxy proxy = KlumInstanceProxy.getProxyFor(container);
Expand All @@ -51,46 +57,76 @@ static void autoLink(Object container, String fieldName) {

static void autoLink(KlumInstanceProxy proxy, Field field, LinkTo linkTo) {
Object value = determineLinkTarget(proxy, field, linkTo);
if (value != null)
if (value == null) return;

if (!field.getType().isAssignableFrom(value.getClass()))
throw new IllegalArgumentException(String.format("LinkTo annotation on %s#%s targets %s, which is not compatible with the field type %s",
field.getDeclaringClass().getName(), field.getName(), value.getClass().getName(), field.getType().getName()));

if (value instanceof Collection)
proxy.addElementsToCollection(field.getName(), (Collection<?>) value);
else if (value instanceof Map)
proxy.addElementsToMap(field.getName(), (Map<?, ?>) value);
else
proxy.setSingleField(field.getName(), value);
}

static Object determineLinkTarget(KlumInstanceProxy proxy, Field field, LinkTo linkTo) {
static Object determineLinkTarget(KlumInstanceProxy proxy, Field fieldToFill, LinkTo linkTo) {
Object providerObject = determineProviderObject(proxy, linkTo);
if (providerObject == null) return null;

if (!linkTo.field().isEmpty())
return InvokerHelper.getProperty(providerObject, linkTo.field());

if (!linkTo.fieldId().isEmpty())
return ClusterModel.getSingleValueOrFail(providerObject, field.getType(), it -> isLinkSourceWithId(it, linkTo.fieldId()));

MetaProperty metaPropertyForFieldName = getFieldNameProperty(field, providerObject, linkTo);
MetaProperty metaPropertyForInstanceName = getInstanceNameProperty(proxy, providerObject, linkTo);

if (pointToDifferentProperties(metaPropertyForInstanceName, metaPropertyForFieldName)) {
switch (linkTo.strategy()) {
case INSTANCE_NAME:
return metaPropertyForInstanceName.getProperty(providerObject);
case FIELD_NAME:
return metaPropertyForFieldName.getProperty(providerObject);
default:
throw new IllegalStateException(format("LinkTo annotation on %s#%s targeting %s would match both instance name (%s) and field name (%s). You need to explicitly set a strategy.",
field.getDeclaringClass().getName(), field.getName(), providerObject.getClass().getName(), metaPropertyForInstanceName.getName(), metaPropertyForFieldName.getName()));
}
return ClusterModel.getSingleValueOrFail(providerObject, fieldToFill.getType(), it -> isLinkSourceWithId(it, linkTo.fieldId()));

String selector = linkTo.selector();
if (!selector.isEmpty()) {
Object selectorValue = proxy.getInstanceProperty(selector);
if (selectorValue == null) return null;

if (selectorValue instanceof String)
return InvokerHelper.getProperty(providerObject, (String) selectorValue);
if (selectorValue instanceof Iterable)
return StreamSupport.stream(((Iterable<?>) selectorValue).spliterator(), false)
.map(it -> InvokerHelper.getProperty(providerObject, it.toString()))
.collect(Collectors.toList());
throw new IllegalArgumentException("Selector value must be a String or Iterable, but is " + selectorValue.getClass().getName());
}

if (metaPropertyForInstanceName != null)
return metaPropertyForInstanceName.getProperty(providerObject);
return inferLinkTarget(proxy, fieldToFill, linkTo, providerObject);
}

private static @Nullable Object inferLinkTarget(KlumInstanceProxy proxy, Field fieldToFill, LinkTo linkTo, Object providerObject) {
MetaProperty metaPropertyForFieldName = getFieldNameProperty(fieldToFill, providerObject, linkTo);
if (linkTo.strategy() == LinkTo.Strategy.FIELD_NAME)
return metaPropertyForFieldName != null ? metaPropertyForFieldName.getProperty(providerObject) : null;

MetaProperty metaPropertyForOwnerPath = getOwnerPathProperty(proxy, providerObject, linkTo);
if (linkTo.strategy() == LinkTo.Strategy.OWNER_PATH)
return metaPropertyForOwnerPath != null ? metaPropertyForOwnerPath.getProperty(providerObject) : null;

if (pointToDifferentProperties(metaPropertyForOwnerPath, metaPropertyForFieldName))
throw new IllegalStateException(format("LinkTo annotation on %s#%s targeting %s would match both instance name (%s) and field name (%s). You need to explicitly set a strategy.",
fieldToFill.getDeclaringClass().getName(), fieldToFill.getName(), providerObject.getClass().getName(), metaPropertyForOwnerPath.getName(), metaPropertyForFieldName.getName()));

if (metaPropertyForOwnerPath != null)
return metaPropertyForOwnerPath.getProperty(providerObject);
else if (metaPropertyForFieldName != null)
return metaPropertyForFieldName.getProperty(providerObject);

return ClusterModel.getSingleValueOrFail(providerObject, field.getType(), it -> !it.isAnnotationPresent(LinkSource.class));
return ClusterModel.getSingleValueOrFail(providerObject, fieldToFill.getType(), it -> !it.isAnnotationPresent(LinkSource.class));
}

private static boolean pointToDifferentProperties(MetaProperty metaPropertyForInstanceName, MetaProperty metaPropertyForFieldName) {
if (metaPropertyForInstanceName == null || metaPropertyForFieldName == null) return false;
return !metaPropertyForInstanceName.getName().equals(metaPropertyForFieldName.getName());
static Object determineProviderObject(KlumInstanceProxy proxy, LinkTo linkTo) {
if (linkTo.provider() != NoClosure.class)
return ClosureHelper.invokeClosureWithDelegateAsArgument(linkTo.provider(), proxy.getDSLInstance());
if (linkTo.providerType() != Object.class)
return StructureUtil.getAncestorOfType(proxy.getDSLInstance(), linkTo.providerType())
.orElse(null);

return proxy.getSingleOwner();
}

private static boolean isLinkSourceWithId(AnnotatedElement field, String id) {
Expand All @@ -101,7 +137,7 @@ static MetaProperty getFieldNameProperty(Field field, Object providerObject, Lin
return getMetaPropertyOrMapKey(providerObject, field.getName() + linkTo.nameSuffix());
}

static MetaProperty getInstanceNameProperty(KlumInstanceProxy proxy, Object providerObject, LinkTo linkTo) {
static MetaProperty getOwnerPathProperty(KlumInstanceProxy proxy, Object providerObject, LinkTo linkTo) {
Set<Object> owners = proxy.getOwners();
if (owners.size() != 1) return null;

Expand All @@ -115,10 +151,16 @@ static MetaProperty getInstanceNameProperty(KlumInstanceProxy proxy, Object prov
.orElse(null);
}

private static boolean pointToDifferentProperties(MetaProperty metaPropertyForInstanceName, MetaProperty metaPropertyForFieldName) {
if (metaPropertyForInstanceName == null || metaPropertyForFieldName == null) return false;
return !metaPropertyForInstanceName.getName().equals(metaPropertyForFieldName.getName());
}

static MetaProperty getMetaPropertyOrMapKey(Object providerObject, String name) {
MetaProperty result = InvokerHelper.getMetaClass(providerObject).getMetaProperty(name);
if (result != null) return result;
if (providerObject instanceof Map && ((Map<?, ?>) providerObject).containsKey(name)) return new MapKeyMetaProperty(name);
if (providerObject instanceof Map && ((Map<?, ?>) providerObject).containsKey(name))
return new MapKeyMetaProperty(name);
return null;
}

Expand All @@ -134,17 +176,7 @@ public Object getProperty(Object object) {

@Override
public void setProperty(Object object, Object newValue) {
((Map<String, Object>) object).put(name,newValue);
((Map<String, Object>) object).put(name, newValue);
}
}

static Object determineProviderObject(KlumInstanceProxy proxy, LinkTo linkTo) {
if (linkTo.provider() != LinkTo.None.class)
return ClosureHelper.invokeClosureWithDelegateAsArgument(linkTo.provider(), proxy.getDSLInstance());
if (linkTo.providerType() != Object.class)
return StructureUtil.getAncestorOfType(proxy.getDSLInstance(), linkTo.providerType())
.orElse(null);

return proxy.getSingleOwner();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class DefaultValuesSpec extends AbstractDSLSpec {
@DSL
class Foo {
@Default(field = 'another')
int value
Integer value
String another
}
'''
Expand Down
Loading