diff --git a/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/ReflectionService.java b/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/ReflectionService.java index 2ff65aaccc8b0..0573eb6687a15 100644 --- a/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/ReflectionService.java +++ b/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/ReflectionService.java @@ -890,7 +890,7 @@ public interface Messages { } @RequiredArgsConstructor - private static class PayloadValidator implements PayloadMapper.OnParameter { + protected static class PayloadValidator implements PayloadMapper.OnParameter { private static final VisibilityService VISIBILITY_SERVICE = new VisibilityService(JsonProvider.provider()); diff --git a/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityService.java b/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityService.java index 1e1cbbdb79658..0adbea7232837 100644 --- a/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityService.java +++ b/component-runtime-manager/src/main/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityService.java @@ -23,12 +23,16 @@ import static lombok.AccessLevel.PRIVATE; import java.lang.reflect.Array; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.json.JsonArrayBuilder; import javax.json.JsonNumber; import javax.json.JsonObject; import javax.json.JsonPointer; @@ -40,6 +44,7 @@ import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; +import lombok.ToString; @RequiredArgsConstructor public class VisibilityService { @@ -51,7 +56,8 @@ public class VisibilityService { public ConditionGroup build(final ParameterMeta param) { final boolean and = "AND".equalsIgnoreCase(param.getMetadata().getOrDefault("tcomp::condition::ifs::operator", "AND")); - return new ConditionGroup(param + Map conditions = new HashMap<>(); + ConditionGroup group = new ConditionGroup(param .getMetadata() .entrySet() .stream() @@ -63,12 +69,32 @@ public ConditionGroup build(final ParameterMeta param) { final String negateKey = "tcomp::condition::if::negate" + index; final String evaluationStrategyKey = "tcomp::condition::if::evaluationStrategy" + index; final String absoluteTargetPath = pathResolver.resolveProperty(param.getPath(), meta.getValue()); - return new Condition(toPointer(absoluteTargetPath), - Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")), - param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT), - param.getMetadata().getOrDefault(valueKey, "true").split(",")); + Condition condition = conditions.get(absoluteTargetPath); + if (condition == null) { + condition = new Condition('/' + absoluteTargetPath.replace('.', '/'), + toPointer(absoluteTargetPath), + Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")), + param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT), + param.getMetadata().getOrDefault(valueKey, "true").split(",")); + conditions.put(absoluteTargetPath, condition); + } else { + List collection = Arrays.stream(condition.values).collect(toList()); + collection.add(String.valueOf(param.getMetadata().getOrDefault(valueKey, "true"))); + condition = new Condition('/' + absoluteTargetPath.replace('.', '/'), + toPointer(absoluteTargetPath), + Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")), + param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT), + collection.toArray(new String[0])); + conditions.replace(absoluteTargetPath, condition); + } + return condition; }) .collect(toList()), and ? stream -> stream.allMatch(i -> i) : stream -> stream.anyMatch(i -> i)); + if (and) { + group.conditions.clear(); + group.conditions.addAll(conditions.values()); + } + return group; } private JsonPointer toPointer(final String absoluteTargetPath) { @@ -119,6 +145,7 @@ private String doResolveProperty(final String propPath, final String paramRef) { } @RequiredArgsConstructor(access = PRIVATE) + @ToString public static class Condition { private static final Function TO_STRING = v -> v == null ? null : String.valueOf(v); @@ -126,6 +153,8 @@ public static class Condition { private static final Function TO_LOWERCASE = v -> v == null ? null : String.valueOf(v).toLowerCase(ROOT); + private final String path; + private final JsonPointer pointer; private final boolean negation; @@ -141,97 +170,120 @@ boolean evaluateCondition(final JsonObject payload) { private boolean evaluate(final String expected, final JsonObject payload) { final Object actual = extractValue(payload); switch (evaluationStrategy) { - case "DEFAULT": - return expected.equals(TO_STRING.apply(actual)); - case "LENGTH": - if (actual == null) { - return "0".equals(expected); - } - final int expectedSize = Integer.parseInt(expected); - if (Collection.class.isInstance(actual)) { - return expectedSize == Collection.class.cast(actual).size(); - } - if (actual.getClass().isArray()) { - return expectedSize == Array.getLength(actual); - } - if (String.class.isInstance(actual)) { - return expectedSize == String.class.cast(actual).length(); - } - return false; - default: - Function preprocessor = TO_STRING; - if (evaluationStrategy.startsWith("CONTAINS")) { - final int start = evaluationStrategy.indexOf('('); - if (start >= 0) { - final int end = evaluationStrategy.indexOf(')', start); - if (end >= 0) { - final Map configuration = Stream - .of(evaluationStrategy.substring(start + 1, end).split(",")) - .map(String::trim) - .filter(it -> !it.isEmpty()) - .map(it -> { - final int sep = it.indexOf('='); - if (sep > 0) { - return new String[] { it.substring(0, sep), it.substring(sep + 1) }; - } - return new String[] { "value", it }; - }) - .collect(toMap(a -> a[0], a -> a[1])); - if (Boolean.parseBoolean(configuration.getOrDefault("lowercase", "false"))) { - preprocessor = TO_LOWERCASE; + case "DEFAULT": + if (Collection.class.isInstance(actual)) { + //if values >= actual, return true, it actual contains any element out of values, return false; + for (Object s : Collection.class.cast(actual)) { + if (!Arrays.stream(values).collect(toList()).contains(s)) { + return false; } } + return true; } + return expected.equals(TO_STRING.apply(actual)); + case "LENGTH": if (actual == null) { - return false; - } - if (CharSequence.class.isInstance(actual)) { - return ofNullable(preprocessor.apply(TO_STRING.apply(actual))) - .map(it -> it.contains(expected)) - .orElse(false); + return "0".equals(expected); } + final int expectedSize = Integer.parseInt(expected); if (Collection.class.isInstance(actual)) { - final Collection collection = Collection.class.cast(actual); - return collection.stream().map(preprocessor).anyMatch(it -> it.contains(expected)); + return expectedSize == Collection.class.cast(actual).size(); } if (actual.getClass().isArray()) { - return IntStream - .range(0, Array.getLength(actual)) - .mapToObj(i -> Array.get(actual, i)) - .map(preprocessor) - .anyMatch(it -> it.contains(expected)); + return expectedSize == Array.getLength(actual); + } + if (String.class.isInstance(actual)) { + return expectedSize == String.class.cast(actual).length(); } return false; - } - throw new IllegalArgumentException("Not supported operation '" + evaluationStrategy + "'"); + default: + Function preprocessor = TO_STRING; + if (evaluationStrategy.startsWith("CONTAINS")) { + final int start = evaluationStrategy.indexOf('('); + if (start >= 0) { + final int end = evaluationStrategy.indexOf(')', start); + if (end >= 0) { + final Map configuration = Stream + .of(evaluationStrategy.substring(start + 1, end).split(",")) + .map(String::trim) + .filter(it -> !it.isEmpty()) + .map(it -> { + final int sep = it.indexOf('='); + if (sep > 0) { + return new String[]{it.substring(0, sep), it.substring(sep + 1)}; + } + return new String[]{"value", it}; + }) + .collect(toMap(a -> a[0], a -> a[1])); + if (Boolean.parseBoolean(configuration.getOrDefault("lowercase", "false"))) { + preprocessor = TO_LOWERCASE; + } + } + } + if (actual == null) { + return false; + } + if (CharSequence.class.isInstance(actual)) { + return ofNullable(preprocessor.apply(TO_STRING.apply(actual))) + .map(it -> it.contains(expected)) + .orElse(false); + } + if (Collection.class.isInstance(actual)) { + final Collection collection = Collection.class.cast(actual); + return collection.stream().map(preprocessor).anyMatch(it -> it.contains(expected)); + } + if (actual.getClass().isArray()) { + return IntStream + .range(0, Array.getLength(actual)) + .mapToObj(i -> Array.get(actual, i)) + .map(preprocessor) + .anyMatch(it -> it.contains(expected)); + } + return false; + } + throw new IllegalArgumentException("Not supported operation '" + evaluationStrategy + "'"); } } private Object extractValue(final JsonObject payload) { if (!pointer.containsValue(payload)) { + if (path.contains("[${index}]")) { + final JsonPointer ptr = JsonProvider.provider() + .createPointer(path.substring(0, path.indexOf("[${index}]"))); + final JsonPointer subptr = JsonProvider.provider() + .createPointer(path.substring(path.indexOf("]") + 1)); + final JsonArrayBuilder builder = JsonProvider.provider().createArrayBuilder(); + ptr.getValue(payload) + .asJsonArray() + .stream() + .forEach(j -> { + JsonValue value = subptr.getValue(j.asJsonObject()); + builder.add(value); + }); + return ofNullable(builder.build()).map(this::mapValue).orElse(null); + } return null; } return ofNullable(pointer.getValue(payload)).map(this::mapValue).orElse(null); - } private Object mapValue(final JsonValue value) { switch (value.getValueType()) { - case ARRAY: - return value.asJsonArray().stream().map(this::mapValue).collect(toList()); - case STRING: - return JsonString.class.cast(value).getString(); - case TRUE: - return true; - case FALSE: - return false; - case NUMBER: - return JsonNumber.class.cast(value).doubleValue(); - case NULL: - return null; - case OBJECT: - default: - return value; + case ARRAY: + return value.asJsonArray().stream().map(this::mapValue).collect(toList()); + case STRING: + return JsonString.class.cast(value).getString(); + case TRUE: + return true; + case FALSE: + return false; + case NUMBER: + return JsonNumber.class.cast(value).doubleValue(); + case NULL: + return null; + case OBJECT: + default: + return value; } } } diff --git a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/ReflectionServiceTest.java b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/ReflectionServiceTest.java index 882d9d7d76d42..5695a8f3383c5 100644 --- a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/ReflectionServiceTest.java +++ b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/ReflectionServiceTest.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.Serializable; import java.lang.reflect.Constructor; @@ -799,40 +800,18 @@ void nestedRequiredActiveIfWithWrongPattern() throws NoSuchMethodException { @Test void nestedRequiredActiveIf_Rest() throws NoSuchMethodException { - final ParameterModelService service = new ParameterModelService(new PropertyEditorRegistry()); - final List metas = service - .buildParameterMetas(MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - "def", new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "test"))); - final Object[] params = reflectionService - .parameterFactory(MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - emptyMap(), metas) - .apply(new HashMap() { - - { - put("value.apiDesc.loadAPI", "false"); - } - }); - + final Map payload = new HashMap(); + payload.put("value.apiDesc.loadAPI", "false"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.RestDatastore.class); assertTrue(MethodsHolder.RestDatastore.class.isInstance(params[0])); } @Test void nestedRequiredActiveIfTrue_Rest() throws NoSuchMethodException { - final ParameterModelService service = new ParameterModelService(new PropertyEditorRegistry()); - final List metas = service - .buildParameterMetas(MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - "def", new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "test"))); - final Object[] params = reflectionService - .parameterFactory(MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - emptyMap(), metas) - .apply(new HashMap() { - - { - put("value.apiDesc.loadAPI", "true"); - put("value.complexConfiguration.url", "https://talend.com"); - } - }); - + final Map payload = new HashMap(); + payload.put("value.apiDesc.loadAPI", "true"); + payload.put("value.complexConfiguration.url", "https://talend.com"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.RestDatastore.class); assertTrue(MethodsHolder.RestDatastore.class.isInstance(params[0])); final MethodsHolder.RestDatastore value = MethodsHolder.RestDatastore.class.cast(params[0]); assertTrue(value.getApiDesc().isLoadAPI()); @@ -841,22 +820,149 @@ void nestedRequiredActiveIfTrue_Rest() throws NoSuchMethodException { @Test void nestedRequiredActiveIfWrong_Rest() throws NoSuchMethodException { - final ParameterModelService service = new ParameterModelService(new PropertyEditorRegistry()); - final List metas = service - .buildParameterMetas(MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - "def", new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "test"))); + final Map payload = new HashMap(); + payload.put("value.apiDesc.loadAPI", "true"); + payload.put("value.complexConfiguration.url", ""); assertThrows(IllegalArgumentException.class, - () -> reflectionService - .parameterFactory( - MethodsHolder.class.getMethod("visibility", MethodsHolder.RestDatastore.class), - emptyMap(), metas) - .apply(new HashMap() { + () -> buildObjectParams("visibility", payload, MethodsHolder.RestDatastore.class)); + } - { - put("value.apiDesc.loadAPI", "true"); - put("value.complexConfiguration.url", " "); - } - })); + @Test + void simpleRequiredActiveIfFiltersOk() throws NoSuchMethodException { + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "ALL"); + payload.put("configuration.logicalOpValue", "ALL"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + assertTrue(MethodsHolder.FilterConfiguration.class.isInstance(params[0])); + final MethodsHolder.FilterConfiguration value = MethodsHolder.FilterConfiguration.class.cast(params[0]); + assertEquals("ALL", value.getLogicalOpType()); + assertEquals("ALL", value.getLogicalOpValue()); + } + + @Test + void simpleRequiredActiveIfFiltersKo() throws NoSuchMethodException { + final String expected = "- Property 'configuration.logicalOpValue' is required."; + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "ALL"); + try { + buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + } catch (IllegalArgumentException e) { + assertEquals(expected, e.getMessage()); + } + } + + @Test + void simpleNestedRequiredActiveIfFiltersOk() throws NoSuchMethodException { + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "AL"); + payload.put("configuration.filters[0].columnName", "col1"); + payload.put("configuration.filters[0].operator", "IS_VALID"); + payload.put("configuration.filters[0].value", "VALID"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + assertTrue(MethodsHolder.FilterConfiguration.class.isInstance(params[0])); + final MethodsHolder.FilterConfiguration value = MethodsHolder.FilterConfiguration.class.cast(params[0]); + assertEquals("VALID", value.getFilters().get(0).getValue()); + } + + @Test + void simpleNestedRequiredActiveIfFiltersKo() throws NoSuchMethodException { + final String expected = "- Property 'configuration.filters[${index}].value' is required."; + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "AL"); + payload.put("configuration.filters[0].columnName", "col1"); + payload.put("configuration.filters[0].operator", "IS_VALID"); + try { + buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + fail("Cannot reach here!"); + } catch (IllegalArgumentException e) { + assertEquals(expected, e.getMessage()); + } + } + + @Test + void nestedRequiredActiveIfFiltersOk() throws NoSuchMethodException { + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "AL"); + // visibility (false) should not throw Property 'configuration.filters[${index}].value' is required. + payload.put("configuration.filters[0].columnName", "col0"); + payload.put("configuration.filters[0].operator", "IS_NULL"); + payload.put("configuration.filters[1].columnName", "col1"); + payload.put("configuration.filters[1].operator", "IS_EMPTY"); + payload.put("configuration.filters[2].columnName", "col2"); + payload.put("configuration.filters[2].operator", "IS_NOT_NULL"); + payload.put("configuration.filters[3].columnName", "col3"); + payload.put("configuration.filters[3].operator", "IS_NOT_EMPTY"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + assertTrue(MethodsHolder.FilterConfiguration.class.isInstance(params[0])); + final MethodsHolder.FilterConfiguration value = MethodsHolder.FilterConfiguration.class.cast(params[0]); + assertEquals("AL", value.getLogicalOpType()); + assertEquals("col0", value.getFilters().get(0).getColumnName()); + } + + @Test + void nestedRequiredActiveIfFiltersKo() throws NoSuchMethodException { + final String expected = "- Property 'configuration.filters[${index}].value' is required."; + final Map payload = new HashMap(); + payload.put("configuration.logicalOpType", "AL"); + payload.put("configuration.filters[0].columnName", "col0"); + payload.put("configuration.filters[0].operator", "IS_NULL"); + payload.put("configuration.filters[0].value", "V"); + payload.put("configuration.filters[1].columnName", "col1"); + payload.put("configuration.filters[1].operator", "IS_EMPTY"); + payload.put("configuration.filters[1].value", "V"); + // this array element SHOULD BE VISIBLE and its value required + payload.put("configuration.filters[2].columnName", "col2"); + payload.put("configuration.filters[2].operator", "IS_VALID"); + try { + buildObjectParams("visibility", payload, MethodsHolder.FilterConfiguration.class); + fail("configuration.filters[2].operator=IS_VALID should be visible!"); + } catch (IllegalArgumentException e) { + assertEquals(expected, e.getMessage()); + } + } + + @Test + void configWithActiveIfEnumOK() throws NoSuchMethodException { + final Map payload = new HashMap(); + payload.put("configuration.bool1", "true"); + payload.put("configuration.bool2", "false"); + payload.put("configuration.bool3", "true"); + payload.put("configuration.enumRequired", "ONE"); + payload.put("configuration.enumIf", "ONE"); + payload.put("configuration.enumIfs", "ONE"); + final Object[] params = buildObjectParams("visibility", payload, MethodsHolder.ConfigWithActiveIfEnum.class); + assertTrue(MethodsHolder.ConfigWithActiveIfEnum.class.isInstance(params[0])); + final MethodsHolder.ConfigWithActiveIfEnum value = MethodsHolder.ConfigWithActiveIfEnum.class.cast(params[0]); + assertTrue(value.isBool1()); + } + + @Test + void configWithActiveIfEnumKoAll() throws NoSuchMethodException { + final String expected = + "- Property 'configuration.enumIf' is required.\n- Property 'configuration.enumIfs' is required.\n- Property 'configuration.enumRequired' is required."; + final Map payload = new HashMap(); + payload.put("configuration.bool1", "true"); + payload.put("configuration.bool2", "false"); + payload.put("configuration.bool3", "true"); + try { + buildObjectParams("visibility", payload, MethodsHolder.ConfigWithActiveIfEnum.class); + fail("Cannot reach here!"); + } catch (IllegalArgumentException e) { + assertEquals(expected, e.getMessage()); + } + } + + private Object[] buildObjectParams(final String method, final Map payload, final Class... args) + throws NoSuchMethodException { + return buildObjectParams(MethodsHolder.class.getMethod(method, args), payload); + } + + private Object[] buildObjectParams(final Method factory, final Map payload) + throws NoSuchMethodException { + final ParameterModelService service = new ParameterModelService(new PropertyEditorRegistry()); + final List metas = service.buildParameterMetas(factory, "def", + new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "test"))); + return reflectionService.parameterFactory(factory, emptyMap(), metas).apply(payload); } private Function, Object[]> getComponentFactory(final Class param, diff --git a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/parameterenricher/ConditionParameterEnricherTest.java b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/parameterenricher/ConditionParameterEnricherTest.java index 529ac60820288..f986d7d1ef525 100644 --- a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/parameterenricher/ConditionParameterEnricherTest.java +++ b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/parameterenricher/ConditionParameterEnricherTest.java @@ -317,4 +317,175 @@ public Class annotationType() { } })); } + + @Test + void activeIfsOnSameTarget() { + assertEquals(new HashMap() { + + { + put("tcomp::condition::ifs::operator", "AND"); + put("tcomp::condition::if::target::0", "filter.operator"); + put("tcomp::condition::if::value::0", "IS_NULL"); + put("tcomp::condition::if::negate::0", "true"); + put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + put("tcomp::condition::if::target::1", "filter.operator"); + put("tcomp::condition::if::value::1", "IS_NOT_NULL"); + put("tcomp::condition::if::negate::1", "true"); + put("tcomp::condition::if::evaluationStrategy::1", "DEFAULT"); + put("tcomp::condition::if::target::2", "filter.operator"); + put("tcomp::condition::if::value::2", "IS_EMPTY"); + put("tcomp::condition::if::negate::2", "true"); + put("tcomp::condition::if::evaluationStrategy::2", "DEFAULT"); + put("tcomp::condition::if::target::3", "filter.operator"); + put("tcomp::condition::if::value::3", "IS_NOT_EMPTY"); + put("tcomp::condition::if::negate::3", "true"); + put("tcomp::condition::if::evaluationStrategy::3", "DEFAULT"); + } + }, new ConditionParameterEnricher().onParameterAnnotation("testParam", String.class, new ActiveIfs() { + + @Override + public Operator operator() { + return Operator.AND; + } + + @Override + public Class annotationType() { + return ActiveIfs.class; + } + + @Override + public ActiveIf[] value() { + return new ActiveIf[] { + new ActiveIf() { + + @Override + public EvaluationStrategyOption[] evaluationStrategyOptions() { + return new EvaluationStrategyOption[0]; + } + + @Override + public String target() { + return "filter.operator"; + } + + @Override + public boolean negate() { + return true; + } + + @Override + public EvaluationStrategy evaluationStrategy() { + return EvaluationStrategy.DEFAULT; + } + + @Override + public Class annotationType() { + return ActiveIf.class; + } + + @Override + public String[] value() { + return new String[] { "IS_NULL" }; + } + }, + new ActiveIf() { + + @Override + public EvaluationStrategyOption[] evaluationStrategyOptions() { + return new EvaluationStrategyOption[0]; + } + + @Override + public String target() { + return "filter.operator"; + } + + @Override + public boolean negate() { + return true; + } + + @Override + public EvaluationStrategy evaluationStrategy() { + return EvaluationStrategy.DEFAULT; + } + + @Override + public Class annotationType() { + return ActiveIf.class; + } + + @Override + public String[] value() { + return new String[] { "IS_NOT_NULL" }; + } + }, + new ActiveIf() { + + @Override + public EvaluationStrategyOption[] evaluationStrategyOptions() { + return new EvaluationStrategyOption[0]; + } + + @Override + public String target() { + return "filter.operator"; + } + + @Override + public boolean negate() { + return true; + } + + @Override + public EvaluationStrategy evaluationStrategy() { + return EvaluationStrategy.DEFAULT; + } + + @Override + public Class annotationType() { + return ActiveIf.class; + } + + @Override + public String[] value() { + return new String[] { "IS_EMPTY" }; + } + }, + new ActiveIf() { + + @Override + public EvaluationStrategyOption[] evaluationStrategyOptions() { + return new EvaluationStrategyOption[0]; + } + + @Override + public String target() { + return "filter.operator"; + } + + @Override + public boolean negate() { + return true; + } + + @Override + public EvaluationStrategy evaluationStrategy() { + return EvaluationStrategy.DEFAULT; + } + + @Override + public Class annotationType() { + return ActiveIf.class; + } + + @Override + public String[] value() { + return new String[] { "IS_NOT_EMPTY" }; + } + } + }; + } + })); + } } diff --git a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/PayloadMapperTest.java b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/PayloadMapperTest.java index 53feb4c4a5f1b..f349993ae283e 100644 --- a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/PayloadMapperTest.java +++ b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/PayloadMapperTest.java @@ -94,6 +94,24 @@ void complex() throws NoSuchMethodException { + "\"other\":{\"value\":\"done\"},\"simple\":\"rs\",\"strings\":[\"rs1\"]}}", value.toString()); } + @Test + void filters() throws NoSuchMethodException { + final List params = service + .buildParameterMetas( + MethodsHolder.class.getMethod("visibility", MethodsHolder.FilterConfiguration.class), "def", + new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "test"))); + final Map payload = new TreeMap<>(); + payload.put("configuration.logicalOpType", "ALL"); + payload.put("configuration.filters[0].columnName", "col0"); + payload.put("configuration.filters[0].operator", "IS_NULL"); + payload.put("configuration.filters[0].value", ""); + final JsonValue value = extractorFactory.visitAndMap(params, payload); + System.out.println(value.toString()); + assertEquals( + "{\"configuration\":{\"filters\":[{\"columnName\":\"col0\",\"operator\":\"IS_NULL\",\"value\":\"\"}],\"logicalOpType\":\"ALL\"}}", + value.toString()); + } + public static class OtherObject { @Option diff --git a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityServiceTest.java b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityServiceTest.java index d99937854f41c..7231b24d3c2d6 100644 --- a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityServiceTest.java +++ b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/reflect/visibility/VisibilityServiceTest.java @@ -42,9 +42,10 @@ void one() { final VisibilityService.ConditionGroup conditionGroup = service .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "foo", "foo", null, emptyList(), emptyList(), metadata, false)); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().build())); assertTrue(conditionGroup.isVisible(Json.createObjectBuilder().add("the_target", "a").build())); assertTrue(conditionGroup.isVisible(Json.createObjectBuilder().add("the_target", "c").build())); - assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().add("the_target", "d").build())); } @Test @@ -69,4 +70,160 @@ void two() { assertFalse(conditionGroup .isVisible(Json.createObjectBuilder().add("the_target1", "d").add("the_target2", "aa").build())); } + + @Test + void activeIfs() { + final Map metadata = new HashMap<>(); + metadata.put("tcomp::condition::ifs::operator", "AND"); + metadata.put("tcomp::condition::if::target::0", "operator"); + metadata.put("tcomp::condition::if::value::0", "IS_NULL"); + metadata.put("tcomp::condition::if::negate::0", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + metadata.put("tcomp::condition::if::target::1", "operator"); + metadata.put("tcomp::condition::if::value::1", "IS_NOT_NULL"); + metadata.put("tcomp::condition::if::negate::1", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::1", "DEFAULT"); + metadata.put("tcomp::condition::if::target::2", "operator"); + metadata.put("tcomp::condition::if::value::2", "IS_EMPTY"); + metadata.put("tcomp::condition::if::negate::2", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::2", "DEFAULT"); + metadata.put("tcomp::condition::if::target::3", "operator"); + metadata.put("tcomp::condition::if::value::3", "IS_NOT_EMPTY"); + metadata.put("tcomp::condition::if::negate::3", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::3", "DEFAULT"); + final VisibilityService.ConditionGroup conditionGroup = service + .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "filter", "filter", null, + emptyList(), emptyList(), metadata, false)); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "IS_NUMERIC").build())); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "VALID").build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "IS_NULL").build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "IS_NOT_NULL").build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "IS_EMPTY").build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder().add("operator", "IS_NOT_EMPTY").build())); + } + + @Test + void activeIfsWithArray() { + final Map metadata = new HashMap<>(); + metadata.put("tcomp::condition::ifs::operator", "AND"); + metadata.put("tcomp::condition::if::target::0", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::0", "IS_NULL"); + metadata.put("tcomp::condition::if::negate::0", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + metadata.put("tcomp::condition::if::target::1", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::1", "IS_NOT_NULL"); + metadata.put("tcomp::condition::if::negate::1", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::1", "DEFAULT"); + metadata.put("tcomp::condition::if::target::2", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::2", "IS_EMPTY"); + metadata.put("tcomp::condition::if::negate::2", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::2", "DEFAULT"); + metadata.put("tcomp::condition::if::target::3", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::3", "IS_NOT_EMPTY"); + metadata.put("tcomp::condition::if::negate::3", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::3", "DEFAULT"); + final VisibilityService.ConditionGroup conditionGroup = service + .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "filter", "filter", null, + emptyList(), emptyList(), metadata, false)); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", + Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_EMPTY").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_EMPTY").build())) + .build())); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_VALID").build())) + .build())); + } + + @Test + void activeIfsWithArray_false() { + final Map metadata = new HashMap<>(); + metadata.put("tcomp::condition::ifs::operator", "AND"); + metadata.put("tcomp::condition::if::target::0", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::0", "IS_NULL"); + metadata.put("tcomp::condition::if::negate::0", "false"); + metadata.put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + metadata.put("tcomp::condition::if::target::1", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::1", "IS_NOT_NULL"); + metadata.put("tcomp::condition::if::negate::1", "false"); + metadata.put("tcomp::condition::if::evaluationStrategy::1", "DEFAULT"); + metadata.put("tcomp::condition::if::target::2", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::2", "IS_EMPTY"); + metadata.put("tcomp::condition::if::negate::2", "false"); + metadata.put("tcomp::condition::if::evaluationStrategy::2", "DEFAULT"); + metadata.put("tcomp::condition::if::target::3", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::3", "IS_NOT_EMPTY"); + metadata.put("tcomp::condition::if::negate::3", "false"); + metadata.put("tcomp::condition::if::evaluationStrategy::3", "DEFAULT"); + final VisibilityService.ConditionGroup conditionGroup = service + .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "filter", "filter", null, + emptyList(), emptyList(), metadata, false)); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", + Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_EMPTY").build())) + .build())); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", + Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_NOT_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_EMPTY").build())) + .build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_VALID").build())) + .build())); + } + @Test + void activeIfWithArray() { + final Map metadata = new HashMap<>(); + metadata.put("tcomp::condition::if::target::0", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::0", "IS_NULL,IS_NOT_NULL,IS_EMPTY,IS_NOT_EMPTY"); + metadata.put("tcomp::condition::if::negate::0", "true"); + metadata.put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + final VisibilityService.ConditionGroup conditionGroup = service + .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "filter", "filter", null, + emptyList(), emptyList(), metadata, false)); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", + Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_EMPTY").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_EMPTY").build())) + .build())); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_VALID").build())) + .build())); + } + + @Test + void activeIfWithArray_false() { + final Map metadata = new HashMap<>(); + metadata.put("tcomp::condition::if::target::0", "arry[${index}].operator"); + metadata.put("tcomp::condition::if::value::0", "IS_NULL,IS_NOT_NULL,IS_EMPTY,IS_NOT_EMPTY"); + metadata.put("tcomp::condition::if::negate::0", "false"); + metadata.put("tcomp::condition::if::evaluationStrategy::0", "DEFAULT"); + final VisibilityService.ConditionGroup conditionGroup = service + .build(new ParameterMeta(null, String.class, ParameterMeta.Type.STRING, "filter", "filter", null, + emptyList(), emptyList(), metadata, false)); + assertTrue(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", + Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_NULL").build()) + .add(Json.createObjectBuilder().add("operator", "IS_EMPTY").build()) + .add(Json.createObjectBuilder().add("operator", "IS_NOT_EMPTY").build())) + .build())); + assertFalse(conditionGroup.isVisible(Json.createObjectBuilder() + .add("arry", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("operator", "IS_VALID").build())) + .build())); + } } diff --git a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/test/MethodsHolder.java b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/test/MethodsHolder.java index 6ed77653a08c8..203f59f0ea718 100644 --- a/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/test/MethodsHolder.java +++ b/component-runtime-manager/src/test/java/org/talend/sdk/component/runtime/manager/test/MethodsHolder.java @@ -15,20 +15,27 @@ */ package org.talend.sdk.component.runtime.manager.test; +import static org.talend.sdk.component.api.configuration.condition.ActiveIfs.Operator.AND; + import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import org.talend.sdk.component.api.configuration.Option; import org.talend.sdk.component.api.configuration.action.Proposable; import org.talend.sdk.component.api.configuration.condition.ActiveIf; +import org.talend.sdk.component.api.configuration.condition.ActiveIfs; import org.talend.sdk.component.api.configuration.constraint.Max; import org.talend.sdk.component.api.configuration.constraint.Min; import org.talend.sdk.component.api.configuration.constraint.Pattern; import org.talend.sdk.component.api.configuration.constraint.Required; import org.talend.sdk.component.api.configuration.type.DataSet; +import org.talend.sdk.component.api.configuration.ui.OptionsOrder; +import org.talend.sdk.component.api.meta.Documentation; +import lombok.Data; import lombok.Getter; public class MethodsHolder { @@ -81,6 +88,14 @@ public void visibility(final RestDatastore value) { // no-op } + public void visibility(@Option("configuration") final FilterConfiguration filters) { + // no-op + } + + public void visibility(@Option("configuration") final ConfigWithActiveIfEnum config) { + // no-op + } + @Getter public static class Array { @@ -169,4 +184,88 @@ public static class ComplexConfiguration { private String url = ""; } } + + @Data + public static class FilterConfiguration { + + @Option + @Required + @Documentation("How to combine filters") + private String logicalOpType; + + @Option + @Required + @ActiveIf(target = "logicalOpType", value = "ALL") + @Documentation("How to combine filters") + private String logicalOpValue; + + @Option + @Documentation("The list of filters to apply") + private List filters = new ArrayList<>(Arrays.asList(new Criteria())); + + @Data + @OptionsOrder({ "columnName", "function", "operator", "value" }) + @Documentation("An unitary filter.") + public static class Criteria { + + @Option + @Required + @Documentation("The input field path to use for this criteria") + private String columnName; + + @Option + @Documentation("The operator") + private String operator; + + @Option + @Required + @ActiveIfs(operator = AND, value = { + @ActiveIf(negate = true, target = "operator", value = "IS_NULL"), + @ActiveIf(negate = true, target = "operator", value = "IS_NOT_NULL"), + @ActiveIf(negate = true, target = "operator", value = "IS_EMPTY"), + @ActiveIf(negate = true, target = "operator", value = "IS_NOT_EMPTY") }) + @Documentation("The value to compare to") + private String value; + } + } + + @Data + public static class ConfigWithActiveIfEnum { + + @Option + @Required + private boolean bool1; + + @Option + @Required + private boolean bool2; + + @Option + @Required + private boolean bool3; + + @Option + @Required + private ActiveIfEnum enumRequired; + + @Option + @Required + @ActiveIf(target = "bool1", value = "true") + private ActiveIfEnum enumIf; + + @Option + @Required + @ActiveIfs({ + @ActiveIf(target = "bool1", value = "true"), + @ActiveIf(target = "bool2", value = "false"), + @ActiveIf(target = "bool3", value = "true") + }) + private ActiveIfEnum enumIfs; + + enum ActiveIfEnum { + ONE, + TWO, + THREE + } + } }