[java-metadata-aggregator] 03/03: MDA-233 - Better value context indication from validators

Ian Young ian at iay.org.uk
Tue Mar 26 11:20:18 UTC 2024


This is an automated email from the git hooks/post-receive script.

iay pushed a commit to branch main
in repository java-metadata-aggregator.

View the commit online:
http://git.shibboleth.net/view/?p=java-metadata-aggregator.git;a=commit;h=b8991e4d215f42b03289c5ba0d112f7cfb708dae

commit b8991e4d215f42b03289c5ba0d112f7cfb708dae
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Tue Mar 26 11:19:45 2024 +0000

    MDA-233 - Better value context indication from validators
    
    - Add a second validate method with a valueContext parameter
    - Both validate methods now have default method bodies;
      implementing classes can provide either or both.
    - Re-worked various classes to pass this down through a stack
      of validations.
    - BaseAsValidator now interpolates a valueContext before calling
      sub-validators.
    - Implemented extended message handling (in a non-templated way,
      for now) for the URL validator classes, which need this most.
    
    https://shibboleth.atlassian.net/browse/MDA-233
---
 .../metadata/validate/BaseAsValidator.java         | 13 +++-
 .../metadata/validate/BaseValidator.java           | 80 ++++++++++++++++++++--
 .../shibboleth/metadata/validate/Validator.java    | 65 +++++++++++++++++-
 .../metadata/validate/ValidatorSequence.java       |  7 +-
 .../validate/url/EmptyPortURLValidator.java        | 20 +++++-
 .../validate/url/HTTPSProtocolURLValidator.java    | 13 +++-
 .../validate/url/MissingHostURLValidator.java      | 18 ++++-
 .../dom/StringAttributeValidationStageTest.java    | 18 ++---
 .../net/shibboleth/metadata/testing/BaseTest.java  | 17 +++++
 .../metadata/validate/BaseAsValidatorTest.java     | 46 ++++++-------
 .../metadata/validate/BaseValidatorTest.java       |  3 +
 .../metadata/validate/ValidatorSequenceTest.java   | 18 +++++
 .../validate/string/AsURLStringValidatorTest.java  | 67 ++++++++++++++++--
 .../metadata/validate/testing/BoomAsValidator.java | 35 ++++++++++
 ...ctingValidator.java => CapturingValidator.java} | 37 ++++++----
 .../validate/testing/PassThroughAsValidator.java   | 33 +++++++++
 .../validate/url/EmptyPortURLValidatorTest.java    | 27 ++++++--
 .../url/HTTPSProtocolURLValidatorTest.java         | 24 ++++++-
 .../validate/url/MissingHostURLValidatorTest.java  | 41 +++++++++++
 19 files changed, 505 insertions(+), 77 deletions(-)

diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java
index 7349366..6a430bf 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java
@@ -17,6 +17,7 @@ package net.shibboleth.metadata.validate;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
@@ -108,11 +109,17 @@ public abstract class BaseAsValidator<V, A> extends BaseValidator implements Val
     protected abstract @Nonnull A convert(@Nonnull final V from) throws IllegalArgumentException;
 
     @Override
-    public @Nonnull Action validate(@Nonnull final V t, @Nonnull final Item<?> item, @Nonnull final String callerId)
-            throws StageProcessingException {
+    public @Nonnull Action validate(final @Nonnull V t, final @Nonnull Item<?> item, final @Nonnull String callerId,
+            final @Nullable String valueContext) throws StageProcessingException {
         try {
             final A v = convert(t);
-            return validators.validate(v, item, makeComponentId(callerId));
+
+            // If called without an explicit value context, interpose the
+            // string form of the value we have just converted. In many cases,
+            // that will have been a String anyway.
+            final String context = valueContext == null ? t.toString() : valueContext;
+
+            return validators.validate(v, item, makeComponentId(callerId), context);
         } catch (final IllegalArgumentException e) {
             if (isConversionRequired()) {
                 addErrorMessage(t, item, callerId, e);
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
index 4166816..7021028 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
@@ -28,8 +28,10 @@ import net.shibboleth.shared.logic.Constraint;
 /**
  * Base class for validator implementations.
  * 
+ * <p>
  * Encapsulates the notion of an identifier for each validator class, and helper
  * methods for constructing status metadata.
+ * </p>
  *
  * @since 0.9.0
  */
@@ -39,16 +41,41 @@ public abstract class BaseValidator extends AbstractIdentifiableInitializableCom
     /**
      * Message format string.
      *
-     * The generated message is formatted using this with the object being validated passed
+     * <p>
+     * The generated message is formatted using this with the value being validated passed
      * as an argument.
-     *
-     * Defaults to <code>"value rejected: '%s'"</code>.
+     * <p>
+     * 
+     * <p>
+     * Defaults to <code>"value rejected: '%s'"</code>. May be overwritten by a subclass via
+     * its constructor, or by setting the {@code message} property explicitly as configuration.
+     * </p>
      *
      * @since 0.10.0
      */
     @Nonnull @GuardedBy("this")
-    private String message = "value rejected: '%s'";
+    private String message;
 
+    /*
+     * Constructor.
+     *
+     * @since 0.10.0
+     */
+    protected BaseValidator() {
+        this.message = "value rejected: '%s'";
+    }
+    
+    /**
+     * Constructor.
+     *
+     * @param message default value for the {@code message} property
+     *
+     * @since 0.10.0
+     */
+    protected BaseValidator(final @Nonnull String message) {
+        this.message = message;
+    }
+    
     /**
      * Returns the message format string.
      *
@@ -126,6 +153,26 @@ public abstract class BaseValidator extends AbstractIdentifiableInitializableCom
         }
     }
 
+    /**
+     * Formats a value context for presentation as a message prefix.
+     * 
+     * <p>
+     * If there is no explicit value context, returns an empty string.
+     * </p>
+     *
+     * @param valueContext value context string, or {@code null}
+     * @return formatted value context prefix
+     *
+     * @since 0.10.0
+     */
+    private @Nonnull String formatValueContext(final @Nullable String valueContext) {
+        if (valueContext == null) {
+            return "";
+        } else {
+            return "'" + valueContext + "': ";
+        }
+    }
+    
     /**
      * Formats the message with the given subject.
      *
@@ -166,6 +213,31 @@ public abstract class BaseValidator extends AbstractIdentifiableInitializableCom
         }
     }
     
+    /**
+     * Add an {@link ErrorStatus} to the given {@link Item}.
+     *
+     * <p>
+     * The status message included in the {@link ErrorStatus} is generated
+     * by formatting the provided value with the {@link #message} field.
+     * </p>
+     *
+     * <p>
+     * If a {@code valueContext} has been provided, it will be included
+     * in the resulting message as a prefix.
+     * </p>
+     *
+     * @param extra extra value to include in the status metadata
+     * @param item {@link Item} to add the status metadata to
+     * @param callerId a {@link String} identifying the caller
+     * @param valueContext the context of the validation, or {@code null}
+     *
+     * @since 0.10.0
+     */
+    protected void addErrorMessage(@Nonnull final Object extra, @Nonnull final Item<?> item,
+            @Nonnull final String callerId, @Nullable final String valueContext) {
+        addError(formatValueContext(valueContext) + formatMessage(extra), item, callerId);
+    }
+
     /**
      * Add an {@link ErrorStatus} to the given {@link Item}.
      *
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/Validator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/Validator.java
index f4dddf4..cc7a332 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/Validator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/Validator.java
@@ -15,6 +15,7 @@
 package net.shibboleth.metadata.validate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -24,7 +25,21 @@ import net.shibboleth.shared.component.IdentifiableComponent;
 import net.shibboleth.shared.component.InitializableComponent;
 
 /**
- * Interface for a validator to be applied to an value in the context of a given {@link Item}.
+ * Interface for a validator to be applied to an value in the context of a given {@link Item},
+ * to which status metadata will be added when appropriate.
+ *
+ * <p>
+ * A {@code validate} call may be provided with a "value context" {@link String} indicating
+ * the larger validation of which this call is a component. For example, a validation of
+ * the host component of a URL value might be provided with a value context of the
+ * entire URL. If the value context is not required, it may be omitted either by passing {@code null}
+ * or by using the {@code validate} method lacking that parameter.
+ * </p>
+ * 
+ * <p>
+ * The interface provides default method bodies for both forms of {@code validate} so
+ * that implementing classes only need to provide one or the other in most cases.
+ * </p>
  *
  * <p>
  * {@code Validator}s <strong>must</strong> be thread-safe.
@@ -72,13 +87,57 @@ public interface Validator<V> extends DestructableComponent, IdentifiableCompone
      * normally use a {@code componentId} of <code>stage/val</code>.
      * </p>
      *
+     * <p>
+     * The <code>valueContext</code> makes the original context of the <code>value</code>
+     * available to help in constructing meaningful error messages. For example, the
+     * validation of a complex structured value such as a URI might be broken down into
+     * multiple sub-validations of its components: scheme, for example. When relevant, the
+     * <code>valueContext</code> provides a representation of the original complex value
+     * while </code>value</code> is the component currently being validated.
+     * </p>
+     *
      * @param value the value to be validated
      * @param item the {@link Item} context for the validation
      * @param callerId a {@link String} identifying the caller
+     * @param valueContext an additional {@link String} indicating the value context, or {@code null}
      * @return an indication of whether to process additional validators
      * @throws StageProcessingException if an error occurs during validation
+     *
+     * @since 0.10.0
      */
+    default
+    @Nonnull Action validate(@Nonnull V value, @Nonnull Item<?> item, @Nonnull String callerId, @Nullable String valueContext)
+            throws StageProcessingException {
+        return validate(value, item, callerId);
+    }
+
+    /**
+     * Apply the validator to a value in the context of the given {@link Item}.
+     *
+     * <p>
+     * The validator influences future processing by adding item metadata to the {@link Item}.
+     * </p>
+     *
+     * <p>
+     * A common case is that the validator will add a {@link net.shibboleth.metadata.StatusMetadata}
+     * to the {@link Item}, for example a {@link net.shibboleth.metadata.ErrorStatus}.
+     * In this case, the convention is that the {@code componentId} of the
+     * {@link net.shibboleth.metadata.ErrorStatus} would be created by combining the
+     * {@code callerId} with a <code>/</code> and the validator's own identifier.
+     * For example, a validator {@code val} called by a stage {@code stage} would
+     * normally use a {@code componentId} of <code>stage/val</code>.
+     * </p>
+     *
+     * @param value the value to be validated
+     * @param item the {@link Item} context for the validation
+     * @param callerId a {@link String} identifying the caller
+     * @return an indication of whether to process additional validators
+     * @throws StageProcessingException if an error occurs during validation
+     */
+    default
     @Nonnull Action validate(@Nonnull V value, @Nonnull Item<?> item, @Nonnull String callerId)
-        throws StageProcessingException;
-    
+            throws StageProcessingException {
+        return validate(value, item, callerId, null);
+    }
+
 }
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
index a5f681e..846e82c 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
@@ -17,6 +17,7 @@ package net.shibboleth.metadata.validate;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
@@ -68,10 +69,10 @@ public class ValidatorSequence<V> extends BaseValidator implements Validator<V>
     }
 
     @Override
-    public @Nonnull Action validate(@Nonnull final V value, @Nonnull final Item<?> item, @Nonnull final String callerId)
-            throws StageProcessingException {
+    public @Nonnull Action validate(@Nonnull final V value, @Nonnull final Item<?> item, @Nonnull final String callerId,
+            @Nullable final String valueContext) throws StageProcessingException {
         for (final Validator<V> validator: getValidators()) {
-            final Action action = validator.validate(value, item, callerId);
+            final Action action = validator.validate(value, item, callerId, valueContext);
             if (action == Action.DONE) {
                 return action;
             }
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidator.java
index 88a1f0c..58c54ee 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidator.java
@@ -17,6 +17,7 @@ package net.shibboleth.metadata.validate.url;
 import java.net.URL;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -25,16 +26,31 @@ import net.shibboleth.metadata.validate.Validator;
 
 /**
  * Validates that a {@link URL} does not contain an empty port component.
+ *
+ * @since 0.10.0
  */
 public class EmptyPortURLValidator extends BaseValidator implements Validator<URL> {
 
+    /**
+     * Constructor.
+     *
+     * <p>
+     * Note: as this validator checks for an empty value, there is no benefit to
+     * including that value in the formatted message, so the placeholder is not included in
+     * the formatting string.
+     * </p>
+     */
+    public EmptyPortURLValidator() {
+        super("libxml2: port present but empty");
+    }
+
     @Override
     public @Nonnull Action validate(final @Nonnull URL url, final @Nonnull Item<?> item,
-            final @Nonnull String callerId) throws StageProcessingException {
+            final @Nonnull String callerId, final @Nullable String valueContext) throws StageProcessingException {
         final String authority = url.getAuthority();
         if (authority != null && !authority.isEmpty()) {
             if (authority.charAt(authority.length() - 1) == ':') {
-                addError("libxml2: port present but empty", item, callerId);
+                addErrorMessage(authority, item, callerId, valueContext);
                 return Action.DONE;
             }
         }
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidator.java
index 6e83204..859904d 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidator.java
@@ -17,6 +17,7 @@ package net.shibboleth.metadata.validate.url;
 import java.net.URL;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -29,12 +30,20 @@ import net.shibboleth.metadata.validate.Validator;
  */
 public class HTTPSProtocolURLValidator extends BaseValidator implements Validator<URL> {
 
+    /**
+     * Constructor.
+     */
+    public HTTPSProtocolURLValidator() {
+        super("protocol '%s' must be https");
+    }
+
     @Override
     public @Nonnull Action validate(final @Nonnull URL url, final @Nonnull Item<?> item,
-            final @Nonnull String callerId) throws StageProcessingException {
+            final @Nonnull String callerId, final @Nullable String valueContext) throws StageProcessingException {
         final var protocol = url.getProtocol();
+        assert protocol != null;
         if (!"https".equals(protocol)) {
-            addError("protocol '" + protocol + "' must be https", item, callerId);
+            addErrorMessage(protocol, item, callerId, valueContext);
             return Action.DONE;
         }
         return Action.CONTINUE;
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/MissingHostURLValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/MissingHostURLValidator.java
index d082d6c..882199b 100644
--- a/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/MissingHostURLValidator.java
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/url/MissingHostURLValidator.java
@@ -17,6 +17,7 @@ package net.shibboleth.metadata.validate.url;
 import java.net.URL;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -28,11 +29,24 @@ import net.shibboleth.metadata.validate.Validator;
  */
 public class MissingHostURLValidator extends BaseValidator implements Validator<URL> {
 
+    /**
+     * Constructor.
+     *
+     * <p>
+     * Note: as this validator checks for an empty value, there is no benefit to
+     * including that value in the formatted message, so the placeholder is not included in
+     * the formatting string.
+     * </p>
+     */
+    public MissingHostURLValidator() {
+        super("host name not present");
+    }
+
     @Override
     public @Nonnull Action validate(final @Nonnull URL url, final @Nonnull Item<?> item,
-            final @Nonnull String callerId) throws StageProcessingException {
+            final @Nonnull String callerId, final @Nullable String valueContext) throws StageProcessingException {
         if ("".equals(url.getHost())) {
-            addError("host name not present", item, callerId);
+            addErrorMessage("", item, callerId, valueContext);
             return Action.DONE;
         }
         return Action.CONTINUE;
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/dom/StringAttributeValidationStageTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/dom/StringAttributeValidationStageTest.java
index 60c5089..3b669a1 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/dom/StringAttributeValidationStageTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/dom/StringAttributeValidationStageTest.java
@@ -16,7 +16,7 @@ import net.shibboleth.metadata.dom.saml.SAMLMetadataSupport;
 import net.shibboleth.metadata.dom.testing.BaseDOMTest;
 import net.shibboleth.metadata.validate.RejectAllValidator;
 import net.shibboleth.metadata.validate.Validator;
-import net.shibboleth.metadata.validate.testing.CollectingValidator;
+import net.shibboleth.metadata.validate.testing.CapturingValidator;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.xml.XMLConstants;
@@ -80,7 +80,7 @@ public class StringAttributeValidationStageTest extends BaseDOMTest {
         var reject = new RejectAllValidator<String>();
         reject.setId("reject");
         reject.initialize();
-        var collect = CollectingValidator.<String>getInstance("collect");
+        var collect = CapturingValidator.<String>getInstance("collect");
         var validators = new ArrayList<Validator<String>>();
         validators.add(collect);
         validators.add(reject);
@@ -96,10 +96,10 @@ public class StringAttributeValidationStageTest extends BaseDOMTest {
         stage.destroy();
         reject.destroy();
         
-        var values = collect.getValues();
+        var values = collect.getCaptures();
         collect.destroy();
         Assert.assertEquals(values.size(), 1);
-        Assert.assertEquals(values.get(0), "a");
+        Assert.assertEquals(values.get(0).value(), "a");
 
         var errors = item.getItemMetadata().get(ErrorStatus.class);
         Assert.assertEquals(errors.size(), 1);
@@ -140,7 +140,7 @@ public class StringAttributeValidationStageTest extends BaseDOMTest {
         attributes.add(new QName("index"));
         attributes.add(XMLConstants.XML_LANG_ATTRIB_NAME);
 
-        var collect = CollectingValidator.<String>getInstance("collect");
+        var collect = CapturingValidator.<String>getInstance("collect");
         var validators = new ArrayList<Validator<String>>();
         validators.add(collect);
 
@@ -154,16 +154,16 @@ public class StringAttributeValidationStageTest extends BaseDOMTest {
 
         stage.destroy();
 
-        var values = collect.getValues();
+        var values = collect.getCaptures();
         collect.destroy();
 
         System.out.println(values);
         Assert.assertEquals(values.size(), 18);
         // Count how many "en"s there are
-        Assert.assertEquals(values.stream().filter(x -> x.equals("en")).count(), 3);
+        Assert.assertEquals(values.stream().filter(x -> x.value().equals("en")).count(), 3);
         // Count how many "shibboleth.net"s there are
-        Assert.assertEquals(values.stream().filter(x -> x.contains("shibboleth.net")).count(), 3);
+        Assert.assertEquals(values.stream().filter(x -> x.value().contains("shibboleth.net")).count(), 3);
         // Count how many indexes (matching single digits) there are
-        Assert.assertEquals(values.stream().filter(x -> x.matches("\\d")).count(), 12);
+        Assert.assertEquals(values.stream().filter(x -> x.value().matches("\\d")).count(), 12);
     }
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/testing/BaseTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/testing/BaseTest.java
index c208594..0a818d3 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/testing/BaseTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/testing/BaseTest.java
@@ -20,7 +20,10 @@ import javax.annotation.Nonnull;
 
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.Resource;
+import org.testng.Assert;
 
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
 import net.shibboleth.shared.logic.Constraint;
 
 public abstract class BaseTest {
@@ -131,4 +134,18 @@ public abstract class BaseTest {
         }
     }
 
+    /**
+     * Extract a given {@link ErrorStatus} from an {@link Item}
+     *
+     * @param item {@link Item} from which the {@link ErrorStatus} should be extracted
+     * @param index index of the {@link ErrorStatus} to be extracted
+     *
+     * @return the extracted {@link ErrorStatus}
+     */
+    protected ErrorStatus extractError(final @Nonnull Item<?> item, int index) {
+        final var errors = item.getItemMetadata().get(ErrorStatus.class);
+        Assert.assertTrue(errors.size() > index, "indexed error status not present");
+        return errors.get(index);
+    }
+
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseAsValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseAsValidatorTest.java
index 98e0be0..7cce5d3 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseAsValidatorTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseAsValidatorTest.java
@@ -11,36 +11,15 @@ import org.testng.annotations.Test;
 import net.shibboleth.metadata.ErrorStatus;
 import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
+import net.shibboleth.metadata.validate.testing.BoomAsValidator;
 import net.shibboleth.metadata.validate.testing.BoomValidator;
+import net.shibboleth.metadata.validate.testing.CapturingValidator;
+import net.shibboleth.metadata.validate.testing.PassThroughAsValidator;
+import net.shibboleth.shared.collection.CollectionSupport;
 
 public class BaseAsValidatorTest {
 
 
-    /**
-     * Test an "as" validator which always fails to convert.
-     */
-    private class BoomAsValidator extends BaseAsValidator<String, String> {
-
-        @Override
-        protected @Nonnull String convert(@Nonnull String from) throws IllegalArgumentException {
-            if (from.equals("nothing")) {
-                throw new IllegalArgumentException();
-            } else {
-                throw new IllegalArgumentException("something");
-            }
-        }
-
-    }
-    
-    private class PassThroughAsValidator<T> extends BaseAsValidator<T, T> {
-
-        @Override
-        protected @Nonnull T convert(@Nonnull T from) throws IllegalArgumentException {
-            return from;
-        }
-        
-    }
-
     @Test
     public void convertTestNoDetail() throws Exception {
 
@@ -98,4 +77,21 @@ public class BaseAsValidatorTest {
         Assert.assertEquals(errors.get(0).getComponentId(), "outer/passer/boomer");
         Assert.assertEquals(errors.get(0).getStatusMessage(), "value rejected: 'value'");
     }
+    
+    @Test
+    public void testValueContext() throws Exception {
+        final var collector = CapturingValidator.<String>getInstance("capture");
+        final var item = new MockItem("content");
+        final var passer = new PassThroughAsValidator<String>();
+        passer.setId("passer");
+        passer.setValidators(CollectionSupport.singletonList(collector));
+        passer.initialize();
+        passer.validate("value1", item, "caller1");
+        passer.validate("value2", item, "caller2", "value-context");
+        final var c = collector.getCaptures();
+        Assert.assertEquals(c.size(), 2);
+        Assert.assertEquals(c.get(0).valueContext(), "value1"); // implicit
+        Assert.assertEquals(c.get(1).valueContext(), "value-context"); // explicit
+        passer.destroy();
+    }
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseValidatorTest.java
index a6242b3..bcc92a7 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseValidatorTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/BaseValidatorTest.java
@@ -12,6 +12,9 @@ import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
 import net.shibboleth.metadata.validate.testing.BoomValidator;
 
+/**
+ * Tests for the {@link BaseValidator} class.
+ */
 public class BaseValidatorTest {
 
     @Test
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/ValidatorSequenceTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/ValidatorSequenceTest.java
index 4d4d9e6..27af8d6 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/ValidatorSequenceTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/ValidatorSequenceTest.java
@@ -11,6 +11,8 @@ import net.shibboleth.metadata.ErrorStatus;
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
+import net.shibboleth.metadata.validate.testing.CapturingValidator;
+import net.shibboleth.shared.collection.CollectionSupport;
 
 public class ValidatorSequenceTest {
 
@@ -85,4 +87,20 @@ public class ValidatorSequenceTest {
         Assert.assertEquals(err.getComponentId(), "stage/reject");
     }
 
+    @Test
+    public void testValueContext() throws Exception {
+        final var collector = CapturingValidator.<String>getInstance("capture");
+        final var v = new ValidatorSequence<String>();
+        v.setId("seq");
+        v.setValidators(CollectionSupport.singletonList(collector));
+        v.initialize();
+        final var item = new MockItem("content");
+        v.validate("value", item, "caller1");
+        v.validate("value2", item, "caller2", "value-context");
+        final var c = collector.getCaptures();
+        Assert.assertEquals(c.size(), 2);
+        Assert.assertNull(c.get(0).valueContext());
+        Assert.assertEquals(c.get(1).valueContext(), "value-context");
+        v.destroy();
+    }
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsURLStringValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsURLStringValidatorTest.java
index 2fc812f..9aebb1c 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsURLStringValidatorTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsURLStringValidatorTest.java
@@ -8,12 +8,18 @@ import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.testing.BaseTest;
 import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
-import net.shibboleth.metadata.validate.testing.CollectingValidator;
+import net.shibboleth.metadata.validate.testing.CapturingValidator;
+import net.shibboleth.metadata.validate.url.MissingHostURLValidator;
 import net.shibboleth.shared.collection.CollectionSupport;
 
-public class AsURLStringValidatorTest {
+public class AsURLStringValidatorTest extends BaseTest {
+
+    public AsURLStringValidatorTest() {
+        super(AsURLStringValidator.class);
+    }
 
     /**
      * Generic test for a good URL.
@@ -23,7 +29,7 @@ public class AsURLStringValidatorTest {
      * @throws Exception if something goes wrong
      */
     private @Nonnull URL good(@Nonnull final String value) throws Exception {
-        final var cv = CollectingValidator.<URL>getInstance("collect");
+        final var cv = CapturingValidator.<URL>getInstance("collect");
         final var item = new MockItem("item");
         final var v = new AsURLStringValidator();
         v.setId("test");
@@ -32,9 +38,9 @@ public class AsURLStringValidatorTest {
         var result = v.validate(value, item, "stage");
         Assert.assertEquals(result, Action.CONTINUE);
         Assert.assertTrue(item.getItemMetadata().isEmpty());
-        var collected = cv.getValues();
-        Assert.assertEquals(collected.size(), 1);
-        var url = collected.get(0);
+        var captures = cv.getCaptures();
+        Assert.assertEquals(captures.size(), 1);
+        var url = captures.get(0).value();
         cv.destroy();
         v.destroy();
         assert url != null;
@@ -91,4 +97,53 @@ public class AsURLStringValidatorTest {
         badURL("http://*** FILL IN ***/");
     }
 
+    @Test
+    public void testImplicitValueContext() throws Exception {
+        final var item = new MockItem("item");
+
+        final var vv = new MissingHostURLValidator();
+        vv.setId("host");
+        vv.initialize();
+
+        final var v = new AsURLStringValidator();
+        v.setId("test");
+        v.setValidators(CollectionSupport.listOf(vv));
+        v.initialize();
+        
+        final var result = v.validate("http:///example", item, "caller");
+        Assert.assertEquals(result, Action.DONE);
+        
+        final var message = extractError(item, 0).getStatusMessage();
+        Assert.assertEquals(message, "'http:///example': host name not present");
+
+        final var result2 = v.validate("http:///example", item, "caller", null);
+        Assert.assertEquals(result2, Action.DONE);
+        
+        final var message2 = extractError(item, 1).getStatusMessage();
+        Assert.assertEquals(message2, "'http:///example': host name not present");
+
+        v.destroy();
+    }
+    
+    @Test
+    public void textExplicitValueContext() throws Exception {
+        final var item = new MockItem("item");
+
+        final var vv = new MissingHostURLValidator();
+        vv.setId("host");
+        vv.initialize();
+
+        final var v = new AsURLStringValidator();
+        v.setId("test");
+        v.setValidators(CollectionSupport.listOf(vv));
+        v.initialize();
+        
+        final var result = v.validate("http:///example", item, "caller", "explicit-context");
+        Assert.assertEquals(result, Action.DONE);
+        
+        final var message = extractError(item, 0).getStatusMessage();
+        Assert.assertEquals(message, "'explicit-context': host name not present");
+
+        v.destroy();
+    }
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/BoomAsValidator.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/BoomAsValidator.java
new file mode 100644
index 0000000..4d93d07
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/BoomAsValidator.java
@@ -0,0 +1,35 @@
+/*
+ * 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 net.shibboleth.metadata.validate.testing;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.metadata.validate.BaseAsValidator;
+
+/**
+ * An "as" validator which always fails to convert.
+ */
+public class BoomAsValidator extends BaseAsValidator<String, String> {
+
+    @Override
+    protected @Nonnull String convert(@Nonnull String from) throws IllegalArgumentException {
+        if (from.equals("nothing")) {
+            throw new IllegalArgumentException();
+        } else {
+            throw new IllegalArgumentException("something");
+        }
+    }
+
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CollectingValidator.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CapturingValidator.java
similarity index 59%
rename from mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CollectingValidator.java
rename to mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CapturingValidator.java
index 7de5cfe..f5da24a 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CollectingValidator.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/CapturingValidator.java
@@ -18,6 +18,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -26,7 +27,7 @@ import net.shibboleth.metadata.validate.Validator;
 import net.shibboleth.shared.component.ComponentInitializationException;
 
 /**
- * A {@link Validator} implementation which collects the values passed to it for
+ * A {@link Validator} implementation which captures the values passed to it for
  * validation.
  * 
  * <p>This can be used in tests to record the nodes which are visited, and the
@@ -34,18 +35,28 @@ import net.shibboleth.shared.component.ComponentInitializationException;
  *
  * @param <T> type of the values to be validated
  */
-public class CollectingValidator<T> extends BaseValidator implements Validator<T> {
+public class CapturingValidator<T> extends BaseValidator implements Validator<T> {
 
-    /** Values this validator has seen, in order. */
-    private final @Nonnull List<T> values = new ArrayList<>();
+    /**
+     * Represents a captured validation.
+     *
+     * @param value the value to be validated
+     * @param valueContext the value context, or {@code null}
+     * @param <T> type of the values to be validated
+     */
+    public record Capture<T>(@Nonnull T value, @Nullable String valueContext) {
+    }
+
+    /** Validations this validator has seen, in order. */
+    private final @Nonnull List<Capture<T>> captures = new ArrayList<>();
     
     /**
-     * Return the values recorded by this validator.
+     * Return the values captured by this validator.
      * 
-     * @return the values recorded by this validator
+     * @return the values captured by this validator
      */
-    public @Nonnull List<T> getValues() {
-        return values;
+    public @Nonnull List<Capture<T>> getCaptures() {
+        return captures;
     }
 
     /**
@@ -56,18 +67,18 @@ public class CollectingValidator<T> extends BaseValidator implements Validator<T
      * @return new validator instance
      * @throws ComponentInitializationException if something goes wrong in initialisation
      */
-    public static @Nonnull <TT> CollectingValidator<TT> getInstance(final @Nonnull String id)
+    public static @Nonnull <TT> CapturingValidator<TT> getInstance(final @Nonnull String id)
             throws ComponentInitializationException {
-        final var instance = new CollectingValidator<TT>();
+        final var instance = new CapturingValidator<TT>();
         instance.setId(id);
         instance.initialize();
         return instance;
     }
 
     @Override
-    public @Nonnull Action validate(@Nonnull T e, @Nonnull Item<?> item, @Nonnull String callerId)
-            throws StageProcessingException {
-        values.add(e);
+    public @Nonnull Action validate(final @Nonnull T e, final @Nonnull Item<?> item, final @Nonnull String callerId,
+            final @Nullable String valueContext) throws StageProcessingException {
+        captures.add(new Capture<T>(e, valueContext));
         return Action.CONTINUE;
     }
     
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/PassThroughAsValidator.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/PassThroughAsValidator.java
new file mode 100644
index 0000000..615d6dd
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/testing/PassThroughAsValidator.java
@@ -0,0 +1,33 @@
+/*
+ * 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 net.shibboleth.metadata.validate.testing;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.metadata.validate.BaseAsValidator;
+
+/**
+ * An "as" validator which simply passes through instead of converting.
+ *
+ * @param <T> the type to pass through
+ */
+public class PassThroughAsValidator<T> extends BaseAsValidator<T, T> {
+
+    @Override
+    protected @Nonnull T convert(@Nonnull T from) throws IllegalArgumentException {
+        return from;
+    }
+    
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidatorTest.java
index 9256d4f..3afdfd4 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidatorTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/EmptyPortURLValidatorTest.java
@@ -5,18 +5,37 @@ import java.net.URL;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import net.shibboleth.metadata.testing.BaseTest;
 import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
 
-public class EmptyPortURLValidatorTest {
+public class EmptyPortURLValidatorTest extends BaseTest {
+
+    public EmptyPortURLValidatorTest() {
+        super(EmptyPortURLValidator.class);
+    }
+
     @Test
     public void testEmptyAuthority() throws Exception {
+        final var item = new MockItem("item");
+
         final var val = new EmptyPortURLValidator();
         val.setId("test");
         val.initialize();
-        final var item = new MockItem("item");
-        final var res = val.validate(new URL("https:///whatever"), item, "stage");
-        Assert.assertEquals(res, Action.CONTINUE);
+
+        final var res = val.validate(new URL("https://domain:/whatever"), item, "stage");
+        Assert.assertEquals(res, Action.DONE);
+
+        final var message = extractError(item, 0).getStatusMessage();
+        Assert.assertEquals(message, "libxml2: port present but empty");
+
+        final var res2 = val.validate(new URL("https://domain:/whatever"), item, "stage", "context");
+        Assert.assertEquals(res2, Action.DONE);
+
+        final var message2 = extractError(item, 1).getStatusMessage();
+        Assert.assertEquals(message2, "'context': libxml2: port present but empty");
+
         val.destroy();
     }
+
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidatorTest.java
index d96be44..d657ae2 100644
--- a/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidatorTest.java
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/HTTPSProtocolURLValidatorTest.java
@@ -6,10 +6,15 @@ import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.testing.BaseTest;
 import net.shibboleth.metadata.testing.MockItem;
 import net.shibboleth.metadata.validate.Validator.Action;
 
-public class HTTPSProtocolURLValidatorTest {
+public class HTTPSProtocolURLValidatorTest extends BaseTest {
+
+    public HTTPSProtocolURLValidatorTest() {
+        super(HTTPSProtocolURLValidator.class);
+    }
 
     @Test
     public void testHttps() throws Exception {
@@ -51,4 +56,21 @@ public class HTTPSProtocolURLValidatorTest {
         Assert.assertEquals(msg, "protocol 'http' must be https");
     }
 
+    @Test
+    public void testValueContext() throws Exception {
+        final var item = new MockItem("data");
+
+        final var val = new HTTPSProtocolURLValidator();
+        val.setId("test");
+        val.initialize();
+
+        final var res = val.validate(new URL("http://example.com/"), item, "stage", "context");
+        Assert.assertEquals(res, Action.DONE);
+
+        final var errors = item.getItemMetadata().get(ErrorStatus.class);
+        Assert.assertEquals(errors.size(), 1);
+
+        final var message = extractError(item, 0).getStatusMessage();
+        Assert.assertEquals(message, "'context': protocol 'http' must be https");
+    }
 }
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/MissingHostURLValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/MissingHostURLValidatorTest.java
new file mode 100644
index 0000000..fe4b5f9
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/url/MissingHostURLValidatorTest.java
@@ -0,0 +1,41 @@
+package net.shibboleth.metadata.validate.url;
+
+import java.net.URL;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.metadata.testing.BaseTest;
+import net.shibboleth.metadata.testing.MockItem;
+import net.shibboleth.metadata.validate.Validator.Action;
+
+public class MissingHostURLValidatorTest extends BaseTest {
+
+    public MissingHostURLValidatorTest() {
+        super(MissingHostURLValidator.class);
+    }
+
+    @Test
+    public void testMissingHost() throws Exception {
+        final var item = new MockItem("item");
+
+        final var val = new MissingHostURLValidator();
+        val.setId("test");
+        val.initialize();
+
+        final var res = val.validate(new URL("https:///whatever"), item, "stage");
+        Assert.assertEquals(res, Action.DONE);
+
+        final var message = extractError(item, 0).getStatusMessage();
+        Assert.assertEquals(message, "host name not present");
+
+        final var res2 = val.validate(new URL("https:///whatever"), item, "stage", "context");
+        Assert.assertEquals(res2, Action.DONE);
+
+        final var message2 = extractError(item, 1).getStatusMessage();
+        Assert.assertEquals(message2, "'context': host name not present");
+
+        val.destroy();
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list