TestW3CSchema.java

package wstxtest.msv;

import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

import javax.xml.stream.*;

import org.codehaus.stax2.*;
import org.codehaus.stax2.validation.*;

import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;

import wstxtest.vstream.BaseValidationTest;

/**
 * This is a simple base-line "smoke test" that checks that W3C Schema
 * validation works at least minimally.
 */
public class TestW3CSchema
    extends BaseValidationTest
{
    /**
     * Sample schema, using sample 'personal.xsd' found from the web
     */
    protected final static String SIMPLE_NON_NS_SCHEMA = "<?xml version='1.0' encoding='UTF-8'?>\n"
            + "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
            + "<xs:element name='personnel'>\n"
            + "<xs:complexType>\n"
            + "<xs:sequence>\n"
            + "<xs:element ref='person' minOccurs='1' maxOccurs='unbounded'/>\n"
            + "</xs:sequence>\n"
            + "</xs:complexType>\n"
            + "<xs:unique name='unique1'>\n"
            + "<xs:selector xpath='person'/>\n"
            + "<xs:field xpath='name/given'/>\n"
            + "<xs:field xpath='name/family'/>\n"
            + "</xs:unique>\n"
            + "</xs:element>\n"
            + "<xs:element name='person'>\n"
            + "<xs:complexType>\n"
            + "<xs:sequence>\n"
            + "<xs:element ref='name'/>\n"
            + "<xs:element ref='email' minOccurs='0' maxOccurs='unbounded'/>\n"
            + "<xs:element ref='url'   minOccurs='0' maxOccurs='unbounded'/>\n"
            + "<xs:element ref='link'  minOccurs='0' maxOccurs='1'/>\n"
            + "</xs:sequence>\n"
            + "<xs:attribute name='id'  type='xs:ID' use='required'/>\n"
            + "<xs:attribute name='note' type='xs:string'/>\n"
            + "<xs:attribute name='contr' default='false'>\n"
            + "<xs:simpleType>\n"
            + "<xs:restriction base = 'xs:string'>\n"
            + "<xs:enumeration value='true'/>\n"
            + "<xs:enumeration value='false'/>\n"
            + "</xs:restriction>\n"
            + "</xs:simpleType>\n"
            + "</xs:attribute>\n"
            + "<xs:attribute name='salary' type='xs:integer'/>\n"
            + "</xs:complexType>\n"
            + "</xs:element>\n"
            + "<xs:element name='name'>\n"
            + "<xs:complexType>\n"
            + "<xs:all>\n"
            + "<xs:element ref='family'/>\n"
            + "<xs:element ref='given'/>\n"
            + "</xs:all>\n"
            + "</xs:complexType>\n"
            + "</xs:element>\n"
            + "<xs:element name='family' type='xs:string'/>\n"
            + "<xs:element name='given' type='xs:string'/>\n"
            + "<xs:element name='email' type='xs:string'/>\n"
            + "<xs:element name='url'>\n"
            + "<xs:complexType>\n"
            + "<xs:attribute name='href' type='xs:string' default='http://'/>\n"
            + "</xs:complexType>\n"
            + "</xs:element>\n"
            
            + "<xs:element name='link'>\n"
            + "<xs:complexType>\n"
            + "<xs:attribute name='manager' type='xs:IDREF'/>\n"
            + "<xs:attribute name='subordinates' type='xs:IDREFS'/>\n"
            + "</xs:complexType>\n" + "</xs:element>\n" + "</xs:schema>\n";
    
	final static String SIMPLE_XML = "<personnel>"
            + "<person id='a123' contr='true'>" + "    <name>"
            + "<family>Family</family><given>Fred</given>" + "    </name>"
            + "    <url href='urn:something'/>" + "  </person>"
            + "  <person id='b12'>"
            + "    <name><family>Blow</family><given>Joe</given>"
            + "    </name>" + "    <url/>" + "  </person>" + "</personnel>";

    /**
     * Test validation against a simple document valid according to a very
     * simple W3C schema.
     */
    public void testSimpleNonNs() throws XMLStreamException
    {
        XMLValidationSchema schema = parseW3CSchema(SIMPLE_NON_NS_SCHEMA);
        String XML = SIMPLE_XML;
        XMLStreamReader2 sr = getReader(XML);
        sr.validateAgainst(schema);
        
        StringWriter writer = new StringWriter();
        XMLStreamWriter2 sw = (XMLStreamWriter2) getOutputFactory().createXMLStreamWriter(writer);
        sw.validateAgainst(schema);
        sw.copyEventFromReader(sr, false);

        try {
            assertTokenType(START_ELEMENT, sr.next());
            assertEquals("personnel", sr.getLocalName());
            sw.copyEventFromReader(sr, false);
            
            while (sr.hasNext()) {
                /* int type = */sr.next();
                sw.copyEventFromReader(sr, false);
            }
        } catch (XMLValidationException vex) {
            fail("Did not expect validation exception, got: " + vex);
        }
        assertTokenType(END_DOCUMENT, sr.getEventType());
        assertEquals(XML.replace("'", "\""), writer.toString());
    }
    
    public void testSimplePartialNonNs() throws XMLStreamException
    {
        XMLValidationSchema schema = parseW3CSchema(SIMPLE_NON_NS_SCHEMA);
        String XML = SIMPLE_XML;
        XMLStreamReader2 sr = getReader(XML);
        
        StringWriter writer = new StringWriter();
        XMLStreamWriter2 sw = (XMLStreamWriter2) getOutputFactory().createXMLStreamWriter(writer);
        sw.copyEventFromReader(sr, false);

        assertTokenType(START_ELEMENT, sr.next());
        assertEquals("personnel", sr.getLocalName());
        sw.copyEventFromReader(sr, false);
        sr.validateAgainst(schema);
        sw.validateAgainst(schema);
        try {
            assertTokenType(START_ELEMENT, sr.next());
            assertEquals("person", sr.getLocalName());
            sw.copyEventFromReader(sr, false);
            while (sr.hasNext()) {
                /* int type = */sr.next();
                sw.copyEventFromReader(sr, false);
            }
        } catch (XMLValidationException vex) {
            fail("Did not expect validation exception, got: " + vex);
        }
        assertTokenType(END_DOCUMENT, sr.getEventType());
        assertEquals(XML.replace("'", "\""), writer.toString());
    }
    
    /**
     * Test validation of a simple document that is invalid according to the
     * simple test schema.
     */
    public void testSimpleNonNsMissingId() throws XMLStreamException
    {
        XMLValidationSchema schema = parseW3CSchema(SIMPLE_NON_NS_SCHEMA);
        String XML = "<personnel><person>"
            + "<name><family>F</family><given>G</given>"
            + "</name></person></personnel>";
        verifyFailure(XML, schema, "missing id attribute",
                      "is missing \"id\" attribute");
    }
    
    public void testSimpleNonNsUndefinedId() throws XMLStreamException
    {
        XMLValidationSchema schema = parseW3CSchema(SIMPLE_NON_NS_SCHEMA);
        String XML = "<personnel><person id='a1'>"
            + "<name><family>F</family><given>G</given>"
            + "</name><link manager='m3' /></person></personnel>";
        verifyFailure(XML, schema, "undefined referenced id ('m3')",
                      "Undefined ID 'm3'", true, true, false);
    }

    public void testSimpleDataTypes() throws XMLStreamException
    {
        // Another sample schema, from
        String SCHEMA = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
            + "<xs:element name='item'>\n"
            + " <xs:complexType>\n"
            + "  <xs:sequence>\n"
            + "   <xs:element name='quantity' type='xs:positiveInteger'/>"
            + "   <xs:element name='price' type='xs:decimal'/>"
            + "  </xs:sequence>"
            + " </xs:complexType>"
            + "</xs:element>"
            + "</xs:schema>";
        
        XMLValidationSchema schema = parseW3CSchema(SCHEMA);
        
        // First, valid doc:
        String XML = "<item><quantity>3  </quantity><price>\r\n4.05</price></item>";
        XMLStreamReader2 sr = getReader(XML);
        sr.validateAgainst(schema);
        
        try {
            assertTokenType(START_ELEMENT, sr.next());
            assertEquals("item", sr.getLocalName());
            
            assertTokenType(START_ELEMENT, sr.next());
            assertEquals("quantity", sr.getLocalName());
            String str = sr.getElementText();
            assertEquals("3", str.trim());
            
            assertTokenType(START_ELEMENT, sr.next());
            assertEquals("price", sr.getLocalName());
            str = sr.getElementText();
            assertEquals("4.05", str.trim());
            
            assertTokenType(END_ELEMENT, sr.next());
            assertTokenType(END_DOCUMENT, sr.next());
        } catch (XMLValidationException vex) {
            fail("Did not expect validation exception, got: " + vex);
        }
        sr.close();
        
        // validate the same document on the writer side
        sr = getReader(XML);
        StringWriter writer = new StringWriter();
        XMLStreamWriter2 sw = (XMLStreamWriter2) getOutputFactory().createXMLStreamWriter(writer);
        sw.validateAgainst(schema);
        sw.copyEventFromReader(sr, true);
        while (sr.hasNext()) {
            /* int type = */sr.next();
            sw.copyEventFromReader(sr, true);
        }
        assertEquals(XML.replace("\r", ""), writer.toString());

        // Then invalid (wrong type for value)
        XML = "<item><quantity>34b</quantity><price>1.00</price></item>";
        sr.validateAgainst(schema);
        verifyFailure(XML, schema, "invalid 'positive integer' datatype",
                      "does not satisfy the \"positiveInteger\"");
        sr.close();
        
        // Another invalid, empty value
        XML = "<item><quantity>    </quantity><price>1.00</price></item>";
        sr.validateAgainst(schema);
        // 12-Nov-2008, TSa: still having MSV bug here, need to suppress failure
        verifyFailure(XML, schema, "invalid (missing) positive integer value",
                      "does not satisfy the \"positiveInteger\"", false);
        sr.close();
        
        // Another invalid, missing value
        XML = "<item><quantity></quantity><price>1.00</price></item>";
        sr.validateAgainst(schema);
        // 12-Nov-2008, TSa: still having MSV bug here, need to suppress failure
        verifyFailure(XML, schema, "invalid (missing) positive integer value",
                      "does not satisfy the \"positiveInteger\"", false);
        sr.close();
    }

    public void testSimpleText() throws XMLStreamException
    {
        String SCHEMA = "<?xml version='1.0' encoding='utf-8' ?>\n"
            + "<xs:schema elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
            + "<xs:element name='root' type='xs:string' />"
            + "</xs:schema>";
        XMLValidationSchema schema = parseW3CSchema(SCHEMA);
        
        String XML = null;
        for (ValidationMode mode : ValidationMode.values()) {
            // First, 3 valid docs:
            XML = "<root>xyz</root>";
            mode.validate(schema, XML);
            
            XML = "<root />";
            mode.validate(schema, XML, "<root/>");
            
            XML = "<root></root>";
            mode.validate(schema, XML, "<root/>");
        }
        
        
        // Then invalid?
        XML = "<foobar />";
        XMLStreamReader2 sr = getReader(XML);
        sr.validateAgainst(schema);
        verifyFailure(XML, schema, "should warn about wrong root element",
                      "tag name \"foobar\" is not allowed", false);
    }

    /**
     * Test for reproducing [WSTX-191]
     */
    public void testConstrainedText() throws XMLStreamException
    {
        String SCHEMA = "<?xml version='1.0' encoding='UTF-8'?>\n"
            + "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns='http://www.mondomaine.fr/framework'\n"
            + "  targetNamespace='http://www.mondomaine.fr/framework' elementFormDefault='qualified' version='1.2'>\n"
            + " <xs:element name='catalog'>\n"
            + "  <xs:complexType>\n"
            + "   <xs:sequence>\n"
            + "    <xs:element ref='description' minOccurs='1' maxOccurs='5'/>\n"
            + "   </xs:sequence>\n" + "  </xs:complexType>\n"
            + " </xs:element>\n"
            + " <xs:element name='description' nillable='true'>\n"
            + "  <xs:simpleType>\n"
            + "   <xs:restriction base='xs:string'>\n"
            + "    <xs:maxLength value='255'/>\n"
            + "   </xs:restriction>\n" + "  </xs:simpleType>\n"
            + " </xs:element>\n" + "</xs:schema>\n";
        
        XMLValidationSchema schema = parseW3CSchema(SCHEMA);
        
        // first cases where there is text, and 1 to 5 descs
        _testValidDesc(schema, "<description>Du Texte</description>");
        _testValidDesc(
                       schema,
                       "<description>1</description><description>2</description><description>3</description>");
        _testValidDesc(schema,
                       "<description><![CDATA[Du Texte]]></description>");
        _testValidDesc(schema,
                       "<description>??</description><description><![CDATA[...]]></description>");
        _testValidDesc(schema, "<description></description>", "<description/>");
        _testValidDesc(schema, "<description />", "<description/>");
        _testValidDesc(schema, "<description><![CDATA[]]></description>");
    }
    
    private void _testValidDesc(XMLValidationSchema schema, String descSnippet) throws XMLStreamException {
        _testValidDesc(schema, descSnippet, descSnippet);
    }
    private void _testValidDesc(XMLValidationSchema schema, String descSnippet, String expectedSnippet) throws XMLStreamException
    {
        for (ValidationMode mode : ValidationMode.values()) {
            // These should all be valid according to the schema
            String XML = "<catalog xmlns='http://www.mondomaine.fr/framework'>"
                + descSnippet + "</catalog>";
            String expectedXML = "<catalog xmlns='http://www.mondomaine.fr/framework'>"
                    + expectedSnippet + "</catalog>";
            mode.validate(schema, XML, expectedXML.replace("'", "\""));
        }
    }

    public void testValidationHandler() throws XMLStreamException
    {
        String SCHEMA = "<?xml version='1.0' encoding='utf-8' ?>\n"
            + "<xs:schema elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
            + "<xs:element name='root' type='xs:string' />"
            + "</xs:schema>";
        XMLValidationSchema schema = parseW3CSchema(SCHEMA);
        
        // Then invalid?
        String XML = "<foobar />";
        XMLStreamReader2 sr = getReader(XML);
        sr.setValidationProblemHandler(problem -> {
            throw new LocalValidationError(problem);
        });
        sr.validateAgainst(schema);
        try {
            while (sr.hasNext()) {
                /* int type = */sr.next();
            }
            fail("Expected LocalValidationError");
        } catch (LocalValidationError lve) {
            assertEquals("tag name \"foobar\" is not allowed. Possible tag names are: <root>", lve.problem.getMessage());
        }
    }

    /*
    ///////////////////////////////////////////////////////////////////////
    // Helper methods
    ///////////////////////////////////////////////////////////////////////
    */
    
    XMLStreamReader2 getReader(String contents) throws XMLStreamException
    {
        XMLInputFactory2 f = getInputFactory();
        setValidating(f, false);
        return constructStreamReader(f, contents);
    }

    /*
    ///////////////////////////////////////////////////////////////////////
    // Helper classes
    ///////////////////////////////////////////////////////////////////////
    */

    public static class LocalValidationError extends RuntimeException
    {
        private static final long serialVersionUID = 1L;

        protected XMLValidationProblem problem;
        
        LocalValidationError(XMLValidationProblem problem) {
            this.problem = problem;
        }

        public XMLValidationProblem getProblem() {
            return problem;
        }
    }

}