diff --git a/pom.xml b/pom.xml
index b0c475c..cd0667f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,10 @@
com.google.code.findbugs
jsr305
+
+ com.google.guava
+ guava
+
net.shibboleth.metadata
aggregator-pipeline
diff --git a/src/main/java/uk/org/iay/incommon/mda/validate/BaseAsValidator.java b/src/main/java/uk/org/iay/incommon/mda/validate/BaseAsValidator.java
new file mode 100644
index 0000000..ec43c4d
--- /dev/null
+++ b/src/main/java/uk/org/iay/incommon/mda/validate/BaseAsValidator.java
@@ -0,0 +1,150 @@
+/*
+ * 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 uk.org.iay.incommon.mda.validate;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.validate.Validator;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * An abstract base class for {@link Validator} implementations which validate a value of
+ * one type "as" another type.
+ *
+ * The implementation calls a template method in the implementation subclass to perform the
+ * conversion. If the conversion succeeds, a sequence of {@link Validator}s are applied to
+ * that new value.
+ *
+ * If the value cannot be converted to the new type, the template method is expected to
+ * throw {@link IllegalArgumentException}. In this case, behaviour depends on the
+ * {@link #conversionRequired} property.
+ *
+ * If {@link #conversionRequired} is true (the default) then an error
+ * status will be applied to the {@link Item}, and the validator will return
+ * {@link net.shibboleth.metadata.validate.Validator.Action#DONE}.
+ *
+ * If {@link #conversionRequired} is false then the validator
+ * will simply return {@link net.shibboleth.metadata.validate.Validator.Action#CONTINUE}
+ * so that subsequent validators may still be applied. This allows several "as" validators
+ * to be applied in sequence, each taking a different approach.
+ *
+ * @param type of the original value
+ * @param type of the new value to which validators should be applied
+ */
+public abstract class BaseAsValidator extends BaseLocalValidator implements Validator {
+
+ /** The validator sequence to apply. */
+ @Nonnull
+ private ValidatorSequence validators = new ValidatorSequence<>();
+
+ /** Whether conversion to the new type must succeed. Default: true */
+ private boolean conversionRequired = true;
+
+ /**
+ * Set the list of validators to apply to each item.
+ *
+ * @param newValidators the list of validators to set
+ */
+ public void setValidators(@Nonnull final List> newValidators) {
+ validators.setValidators(newValidators);
+ }
+
+ /**
+ * Gets the list of validators being applied to each item.
+ *
+ * @return list of validators
+ */
+ @Nonnull
+ public List> getValidators() {
+ return validators.getValidators();
+ }
+
+ /**
+ * Set whether conversion to the new type is required to succeed.
+ *
+ * @param required true if the conversion is required to succeed
+ */
+ public void setConversionRequired(final boolean required) {
+ conversionRequired = required;
+ }
+
+ /**
+ * Returns whether conversion to the new type is required to succeed.
+ *
+ * @return true if the conversion is required to succeed
+ */
+ public boolean isConversionRequired() {
+ return conversionRequired;
+ }
+
+ /**
+ * Apply each of the configured validators in turn to the provided object.
+ *
+ * @param value object to be validated
+ * @param item the {@link Item} context for the validation
+ *
+ * @return the result of applying the validators to the value
+ *
+ * @throws StageProcessingException if errors occur during processing
+ */
+ protected Action applyValidators(@Nonnull final A value, @Nonnull final Item> item)
+ throws StageProcessingException {
+ return validators.validate(value, item, getId());
+ }
+
+ /**
+ * Convert from the old value type to the new.
+ *
+ * @param from a value of the old type
+ * @return a value of the new type
+ * @throws IllegalArgumentException if a conversion can not be performed
+ */
+ protected abstract A convert(@Nonnull final V from) throws IllegalArgumentException;
+
+ @Override
+ public Action validate(@Nonnull final V t, @Nonnull final Item> item, @Nonnull final String stageId)
+ throws StageProcessingException {
+ try {
+ final A v = convert(t);
+ return applyValidators(v, item);
+ } catch (final IllegalArgumentException e) {
+ if (isConversionRequired()) {
+ addErrorMessage(t, item, stageId);
+ return Action.DONE;
+ } else {
+ return Action.CONTINUE;
+ }
+ }
+ }
+
+ @Override
+ protected void doDestroy() {
+ validators.destroy();
+ validators = null;
+ super.doDestroy();
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ validators.setId(getId());
+ validators.initialize();
+ }
+
+}
diff --git a/src/main/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidator.java b/src/main/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidator.java
new file mode 100644
index 0000000..2049ce9
--- /dev/null
+++ b/src/main/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidator.java
@@ -0,0 +1,42 @@
+/*
+ * 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 uk.org.iay.incommon.mda.validate.string;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.validate.Validator;
+import uk.org.iay.incommon.mda.validate.BaseAsValidator;
+
+/**
+ * A Validator that checks {@link String} values as domain names by converting the
+ * value to an {@link InternetDomainName} and applying a sequence of validators to that value.
+ *
+ * This validator fails (and returns {@link net.shibboleth.metadata.validate.Validator.Action#DONE}) if the
+ * value can not be converted to an {@link InternetDomainName}.
+ *
+ * Otherwise, the validator applies the sequence of validators to the {@link InternetDomainName} and returns
+ * the value of that sequence.
+ */
+public class AsDomainNameStringValidator extends BaseAsValidator
+ implements Validator {
+
+ @Override
+ protected InternetDomainName convert(@Nonnull final String domain) throws IllegalArgumentException {
+ return InternetDomainName.from(domain);
+ }
+
+}
diff --git a/src/main/resources/uk/org/iay/incommon/mda/beans.xml b/src/main/resources/uk/org/iay/incommon/mda/beans.xml
index cc9b55d..101f273 100644
--- a/src/main/resources/uk/org/iay/incommon/mda/beans.xml
+++ b/src/main/resources/uk/org/iay/incommon/mda/beans.xml
@@ -44,6 +44,9 @@
+
+
diff --git a/src/test/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidatorTest.java b/src/test/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidatorTest.java
new file mode 100644
index 0000000..c96a0bf
--- /dev/null
+++ b/src/test/java/uk/org/iay/incommon/mda/validate/string/AsDomainNameStringValidatorTest.java
@@ -0,0 +1,108 @@
+
+package uk.org.iay.incommon.mda.validate.string;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.validate.Validator;
+import net.shibboleth.metadata.validate.Validator.Action;
+import uk.org.iay.incommon.mda.validate.BaseLocalValidator;
+import uk.org.ukfederation.mda.MockItem;
+
+public class AsDomainNameStringValidatorTest {
+
+ private static class CountingValidator extends BaseLocalValidator implements Validator {
+ public int count;
+ private final Action action;
+
+ @Override
+ public Action validate(InternetDomainName e, Item> item, String stageId) throws StageProcessingException {
+ count++;
+ return action;
+ }
+
+ /** Constructor. */
+ public CountingValidator(final Action a) {
+ action = a;
+ }
+ }
+
+ @Test
+ public void testOK() throws Exception {
+ final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+ counter.setId("counter");
+ counter.initialize();
+
+ final List> nestedSequence = new ArrayList<>();
+ nestedSequence.add(counter);
+
+ final Item item = new MockItem("content");
+
+ final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+ val.setId("id");
+ val.setValidators(nestedSequence);
+ val.initialize();
+
+ final Action res = val.validate("example.org", item, "stage");
+ Assert.assertEquals(res, Action.CONTINUE);
+ Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+ Assert.assertEquals(counter.count, 1);
+ }
+
+ @Test
+ public void testNoConvertDefault() throws Exception {
+ final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+ counter.setId("counter");
+ counter.initialize();
+
+ final List> nestedSequence = new ArrayList<>();
+ nestedSequence.add(counter);
+
+ final Item item = new MockItem("content");
+
+ final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+ val.setId("id");
+ val.setValidators(nestedSequence);
+ val.setMessage("quick brown %s");
+ val.initialize();
+
+ final Action res = val.validate("example**.org", item, "stage");
+ Assert.assertEquals(res, Action.DONE);
+ Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+ final ErrorStatus err = item.getItemMetadata().get(ErrorStatus.class).get(0);
+ Assert.assertTrue(err.getStatusMessage().contains("quick brown example**.org"));
+ Assert.assertEquals(counter.count, 0);
+ }
+
+ @Test
+ public void testNoConvertFalse() throws Exception {
+ final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+ counter.setId("counter");
+ counter.initialize();
+
+ final List> nestedSequence = new ArrayList<>();
+ nestedSequence.add(counter);
+
+ final Item item = new MockItem("content");
+
+ final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+ val.setId("id");
+ val.setConversionRequired(false);
+ val.setValidators(nestedSequence);
+ val.initialize();
+
+ final Action res = val.validate("example**.org", item, "stage");
+ Assert.assertEquals(res, Action.CONTINUE);
+ Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+ Assert.assertEquals(counter.count, 0);
+ }
+
+}