Custom Validator to check if a String contains XML

This blog post will show you how to create a custom validator to check if a String contains valid XML using the Java Validation API.

Suppose you have a highly technical application that requires the user to enter some XML in a web form. You want to validate this on the client side, but also on the server side, since you should never trust your client.

For the purpose of the example, suppose you have this entity:

@Entity
public class MyEntity {
  private String name;
  @Column(columnDefinition = "TEXT")
  private String xml;

  // getters and setters omitted...
}

For the validation of the name, I can use @NotNull and @Size validations. I would like to have something similar for the XML string. On top of that I also want to optionally add a link to an XSD file that validates the XML string.

This will be the resulting entity:

@Entity
public class MyEntity {
  @NotNull
  @Size(min=1,max=50)
  private String name;
  @XmlString(xsdLocation="/xsd/my-xsd.xsd")
  @Column(columnDefinition = "TEXT")
  private String xml;

  // getters and setters omitted...
}

To make this work, we first need to declare our new @XmlString annotation:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = XmlStringValidator.class)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface XmlString {
    String message() default "Invalid XML String";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String xsdLocation() default "";
}

Next, we need to create the validator itself:

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.IOException;
import java.io.StringReader;

public class XmlStringValidator implements ConstraintValidator<XmlString, String> {
    private static final Logger LOGGER = LoggerFactory.getLogger(XmlStringValidator.class);
// ------------------------------ FIELDS ------------------------------

    private Schema schema;

// ------------------------ INTERFACE METHODS ------------------------


// --------------------- Interface ConstraintValidator ---------------------

    @Override
    public void initialize(XmlString xmlString) {
        if (StringUtils.isNotEmpty(xmlString.xsdLocation())) {
            try {
                SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                schema = sf.newSchema(new StreamSource(getClass().getResourceAsStream(xmlString.xsdLocation())));
            } catch (SAXException e) {
                LOGGER.error("Unable to read the XSD file " + xmlString.xsdLocation() + ". The XmlString will not validated against the XSD!", e );
            }
        }
    }

    @Override
    public boolean isValid(String xmlString, ConstraintValidatorContext context) {
        boolean result = true;

        if (StringUtils.isNotEmpty(xmlString)) {
            try {
                if (schema != null) {
                    validateXmlAgainstXsd(xmlString);
                } else {
                    validateIfXml(xmlString);
                }
            } catch (ParserConfigurationException | SAXException | IOException e) {
                LOGGER.trace("Invalid XML", e);
                result = false;
            }
        } else {
            result = false;
        }
        return result;
    }

// -------------------------- PRIVATE METHODS --------------------------

    private static void validateIfXml(String xmlString) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory spf = SAXParserFactory.newInstance();
        SAXParser sp = spf.newSAXParser();
        XMLReader xr = sp.getXMLReader();
        xr.parse(new InputSource(new StringReader(xmlString)));
    }

    private void validateXmlAgainstXsd(String xmlString) throws SAXException, IOException {
        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(new StringReader(xmlString)));
    }
}

The logic here is quite simple:

  1. If there is an xsdLocation defined, use it for the validation.
  2. If there is none, just check if it is valid XML.

Of course, no code really works without having unit tests in place, so this is a small extract from the various tests, just to show how you can test a validator:

public class XmlStringValidatorTest {

    @Test
    public void givenEmptyString_notValid() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        TestObject testObject = new TestObject("");
        Set<ConstraintViolation<TestObject>> violationSet = validator.validate(testObject);
        assertThat(violationSet).hasViolationOnPath("xml");
    }

    @Test
    public void givenNoXml_notValid() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        TestObject testObject = new TestObject("This is no XML string");
        Set<ConstraintViolation<TestObject>> violationSet = validator.validate(testObject);
        assertThat(violationSet).hasViolationOnPath("xml");
    }

    @Test
    public void givenXml_valid() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        TestObject testObject = new TestObject("<Node>test</Node>");
        Set<ConstraintViolation<TestObject>> violationSet = validator.validate(testObject);
        assertThat(violationSet).hasNoViolations();
    }

    static class TestObject {

        @XmlString
        private String xml;

        TestObject(String xml) {
            this.xml = xml;
        }

        public String getXml() {
            return xml;
        }

        public void setXml(String xml) {
            this.xml = xml;
        }
    }
}

That wraps it up. Our custom validator can be used on an Entity so we avoid invalid XML in our database, or it can be used on a Spring Controller in combination with @Valid @ModelAttribute annotations.

This know-how originated during the development of a PegusApps project.

Advertisements

One thought on “Custom Validator to check if a String contains XML

  1. Pingback: AssertJ custom assertion for ConstraintValidator tests | Wim Deblauwe's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s