From d9ac9e44a7bd6fff086d5d24b7a39bed32f1ab2e Mon Sep 17 00:00:00 2001 From: Otto Fowler Date: Fri, 6 Mar 2020 08:06:08 -0500 Subject: [PATCH] add validator for lists that ensure the element validator is called for empty entries Signed-off-by: Matthew Burgess This closes #4116 --- .../processor/util/StandardValidators.java | 11 +- .../validator/TestStandardValidators.java | 151 ++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java index f2ca0a1237..11962bc526 100644 --- a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java @@ -566,7 +566,14 @@ public class StandardValidators { }; } - public static Validator createListValidator(boolean trimEntries, boolean excludeEmptyEntries, Validator validator) { + public static Validator createListValidator(boolean trimEntries, boolean excludeEmptyEntries, + Validator elementValidator){ + return createListValidator(trimEntries,excludeEmptyEntries, elementValidator, false); + } + + public static Validator createListValidator(boolean trimEntries, boolean excludeEmptyEntries, + Validator validator, + boolean ensureElementValidation) { return (subject, input, context) -> { if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) { return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build(); @@ -576,7 +583,7 @@ public class StandardValidators { return new ValidationResult.Builder().subject(subject).input(null).explanation("List must have at least one non-empty element").valid(false).build(); } - final String[] list = input.split(","); + final String[] list = ensureElementValidation ? input.split(",",-1) : input.split(","); if (list.length == 0) { return new ValidationResult.Builder().subject(subject).input(null).explanation("List must have at least one non-empty element").valid(false).build(); } diff --git a/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/validator/TestStandardValidators.java b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/validator/TestStandardValidators.java index 6c6bfe986d..c82c64e341 100644 --- a/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/validator/TestStandardValidators.java +++ b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/util/validator/TestStandardValidators.java @@ -309,6 +309,157 @@ public class TestStandardValidators { assertEquals(2, mockValidator.getValidateCallCount()); } + + @Test + public void testListValidatorEnsuringElementValidation() { + // use the TestMockValidator to be sure the item validator get's called when we think it should + // note that it will reset the count after every get call. + // note that we fail fast, so if there are 3 items, and the second fails validation, the call + // count will be 2 + InstrumentedStandardValidator mockValidator = new InstrumentedStandardValidator(StandardValidators.NON_EMPTY_VALIDATOR, true); + Validator val = StandardValidators.createListValidator(true, false, mockValidator, true); + ValidationResult vr; + + final ValidationContext validationContext = Mockito.mock(ValidationContext.class); + + vr = val.validate("List", null, validationContext); + assertFalse(vr.isValid()); + assertEquals(0, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + // Whitespace will be trimmed + vr = val.validate("List", " ", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + // An empty list is the same as null, "" or " " + vr = val.validate("List", ",", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", " , ", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + // will evaluate to no entry + vr = val.validate("List", ",,,,", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + // will evaluate to an empty element + vr = val.validate("List", ",foo", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "1", validationContext); + assertTrue(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "1,2,3", validationContext); + assertTrue(vr.isValid()); + assertEquals(3, mockValidator.getValidateCallCount()); + + // The parser will bother with whitespace after the last comma, as we ensure the inner validator is called + vr = val.validate("List", "a,", validationContext); + assertFalse(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + + // However it will bother if there is an empty element in the list (two commas in a row, e.g.) + vr = val.validate("List", "a,,c", validationContext); + assertFalse(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "a, ,c, ", validationContext); + assertFalse(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + + // Try without trim and use a non-blank validator instead of a non-empty one + // + // use the TestMockValidator to be sure the item validator get's called when we think it should + // note that it will reset the count after every get call. + // note that we fail fast, so if there are 3 items, and the second fails validation, the call + // count will be 2 + mockValidator = new InstrumentedStandardValidator(StandardValidators.NON_BLANK_VALIDATOR, true); + val = StandardValidators.createListValidator(false, true, mockValidator); + + vr = val.validate("List", null, validationContext); + assertFalse(vr.isValid()); + assertEquals(0, mockValidator.getValidateCallCount()); + + // List Validator will ignore empty entries + vr = val.validate("List", "", validationContext); + assertTrue(vr.isValid()); + assertEquals(0, mockValidator.getValidateCallCount()); + + // Whitespace will not be trimmed, but it is still invalid because a non-blank validator is used + vr = val.validate("List", " ", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "a,,c", validationContext); + assertTrue(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "a, ,c, ", validationContext); + assertFalse(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + + // Try without trim and use a non-empty validator + // use the TestMockValidator to be sure the item validator get's called when we think it should + // note that it will reset the count after every get call. + // note that we fail fast, so if there are 3 items, and the second fails validation, the call + // count will be 2 + mockValidator = new InstrumentedStandardValidator(StandardValidators.NON_EMPTY_VALIDATOR, true); + val = StandardValidators.createListValidator(false, false, mockValidator); + + vr = val.validate("List", null, validationContext); + assertFalse(vr.isValid()); + assertEquals(0, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + // Whitespace will not be trimmed + vr = val.validate("List", " ", validationContext); + assertTrue(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "a, ,c, ", validationContext); + assertTrue(vr.isValid()); + assertEquals(4, mockValidator.getValidateCallCount()); + + // Try with trim and use a boolean validator // Try without trim and use a non-empty validator + // use the TestMockValidator to be sure the item validator get's called when we think it should + // note that it will reset the count after every get call. + // note that we fail fast, so if there are 3 items, and the second fails validation, the call + // count will be 2 + mockValidator = new InstrumentedStandardValidator(StandardValidators.BOOLEAN_VALIDATOR, true); + val = StandardValidators.createListValidator(true, true, mockValidator); + vr = val.validate("List", "notbool", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", " notbool \n ", validationContext); + assertFalse(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", "true", validationContext); + assertTrue(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", " true \n ", validationContext); + assertTrue(vr.isValid()); + assertEquals(1, mockValidator.getValidateCallCount()); + + vr = val.validate("List", " , false, true,\n", validationContext); + assertTrue(vr.isValid()); + assertEquals(2, mockValidator.getValidateCallCount()); + } + @Test public void testCreateURLorFileValidator() { Validator val = StandardValidators.createURLorFileValidator();