I managed to implement a solution based on Schema.newValidatorHandler()
. I lost most time with the fact that SaxParser.parse()
only accepts a DefaultHandler
. To insert a custom ContentHandler
, one has to use SaxParser.getXMLReader().setContentHandler()
.
I am aware that this proof of concept is not very robust, because it is parsing the validation error message to extract the maxLength
schema information. So this solution is relying on a very specific SAX implementation.
I looked at schema aware XSLT transformation, but could not find any indication that the schema information can be accessed in the transformation expressions.
Writing my own specialized schema parser is still not completely off the table.
import java.io.IOException;
import java.io.StringReader;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
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.ValidatorHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
public class TestFixMaxLength {
public static void main(String[] args) throws Exception {
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = schemaFactory.newSchema(new StreamSource(TestFixMaxLength.class.getResourceAsStream("schema.xsd")));
// validation on original input should fail
// schema.newValidator().validate(new StreamSource(TestFixMaxLength.class.getResourceAsStream("input.xml")));
CustomContentHandler customContentHandler = new CustomContentHandler();
ValidatorHandler validatorHandler = schema.newValidatorHandler();
validatorHandler.setContentHandler(customContentHandler);
validatorHandler.setErrorHandler(customContentHandler);
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
SAXParser saxParser = saxParserFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(validatorHandler);
xmlReader.parse(new InputSource(TestFixMaxLength.class.getResourceAsStream("input.xml")));
// not: saxParser.parse(TestFixMaxLength.class.getResourceAsStream("input.xml"), ???);
System.out.println();
System.out.println();
System.out.println(customContentHandler.m_outputBuilder.toString());
// validation on corrected input should pass
schema.newValidator().validate(new StreamSource(new StringReader(customContentHandler.m_outputBuilder.toString())));
}
/****************************************************************************************************************************************/
private static class CustomContentHandler extends DefaultHandler {
private StringBuilder m_outputBuilder = new StringBuilder();
private SortedMap<String, String> m_prefixMappings = new TreeMap<>();
private int m_lastValueLength = 0;
private Matcher m_matcher = Pattern.compile(
"cvc-maxLength-valid: Value '(.+?)' with length = '(.+?)' is not facet-valid with respect to maxLength '(.+?)' for type '(.+?)'.",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL).matcher("");
@Override
public void error(SAXParseException e) throws SAXException {
if (e.getMessage().startsWith("cvc-maxLength-valid")) {
System.out.println("error: " + e);
m_matcher.reset(e.getMessage());
if (m_matcher.matches()) {
int maxLength = Integer.parseInt(m_matcher.group(3));
m_outputBuilder.setLength(m_outputBuilder.length() - m_lastValueLength + maxLength);
} else {
System.out.println("unexpected message format");
}
}
}
@Override
public void startDocument() throws SAXException {
System.out.println("startDocument");
}
@Override
public void endDocument() throws SAXException {
System.out.println("endDocument");
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
System.out.println("startPrefixMapping: prefix: " + prefix + ", uri: " + uri);
m_prefixMappings.put(prefix, uri);
}
@Override
public void endPrefixMapping(String prefix) throws SAXException {
System.out.println("endPrefixMapping: prefix: " + prefix);
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
System.out.println("startElement: uri: " + uri + ", localName: " + localName + ", qName: " + qName
+ ", attributes: " + attributes.getLength());
m_outputBuilder.append('<');
m_outputBuilder.append(qName);
for (int i = 0; i < attributes.getLength(); i++) {
m_outputBuilder.append(' ');
m_outputBuilder.append(attributes.getQName(i));
m_outputBuilder.append('=');
m_outputBuilder.append('\"');
m_outputBuilder.append(attributes.getValue(i));
m_outputBuilder.append('\"');
}
if (!m_prefixMappings.isEmpty()) {
for (Entry<String, String> mapping : m_prefixMappings.entrySet()) {
m_outputBuilder.append(" xmlns:");
m_outputBuilder.append(mapping.getKey());
m_outputBuilder.append('=');
m_outputBuilder.append('\"');
m_outputBuilder.append(mapping.getValue());
m_outputBuilder.append('\"');
}
m_prefixMappings.clear();
}
m_outputBuilder.append('>');
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println("endElement: uri: " + uri + ", localName: " + localName + ", qName: " + qName);
m_outputBuilder.append('<');
m_outputBuilder.append('/');
m_outputBuilder.append(qName);
m_outputBuilder.append('>');
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
System.out.println(
"characters: '" + new String(ch, start, length) + "', start: " + start + ", length: " + length);
m_outputBuilder.append(ch, start, length);
m_lastValueLength = length;
}
@Override
public void skippedEntity(String name) throws SAXException {
System.out.println("skippedEntity: name: " + name);
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
System.out.println("ignorableWhitespace: '" + new String(ch, start, length) + "', start: " + start
+ ", length: " + length);
m_outputBuilder.append(ch, start, length);
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
System.out.println("processingInstruction: target: " + target + ", data: " + data);
}
@Override
public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException {
System.out.println("resolveEntity");
return null;
}
}
}