All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.docbook.xsltng.extensions.ValidateRNG Maven / Gradle / Ivy

package org.docbook.xsltng.extensions;

import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.lib.ExtensionFunctionCall;
import net.sf.saxon.lib.ExtensionFunctionDefinition;
import net.sf.saxon.ma.map.KeyValuePair;
import net.sf.saxon.ma.map.MapItem;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.om.Sequence;
import net.sf.saxon.om.StructuredQName;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.XdmArray;
import net.sf.saxon.s9api.XdmAtomicValue;
import net.sf.saxon.s9api.XdmMap;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.BuiltInAtomicType;
import net.sf.saxon.value.SequenceType;
import net.sf.saxon.value.StringValue;
import org.iso_relax.verifier.Schema;
import org.iso_relax.verifier.Verifier;
import org.iso_relax.verifier.VerifierConfigurationException;
import org.iso_relax.verifier.VerifierFactory;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

public class ValidateRNG extends ExtensionFunctionDefinition {
    private static final StructuredQName qName =
            new StructuredQName("", "http://docbook.org/extensions/xslt", "validate-with-relax-ng");

    @Override
    public StructuredQName getFunctionQName() {
        return qName;
    }

    @Override
    public int getMinimumNumberOfArguments() {
        return 2;
    }

    @Override
    public int getMaximumNumberOfArguments() {
        return 3;
    }

    @Override
    public SequenceType[] getArgumentTypes() {
        return new SequenceType[]{SequenceType.SINGLE_NODE, SequenceType.SINGLE_ITEM, SequenceType.OPTIONAL_ITEM};
    }

    @Override
    public SequenceType getResultType(SequenceType[] suppliedArgumentTypes) {
        return SequenceType.SINGLE_ITEM;
    }

    public ExtensionFunctionCall makeCallExpression() {
        return new ValidateRNG.PropertiesCall();
    }

    private class PropertiesCall extends ExtensionFunctionCall {
        public Sequence call(XPathContext context, Sequence[] sequences) throws XPathException {
            NodeInfo source = (NodeInfo) sequences[0].head();
            HashMap options = new HashMap<>();
            NodeInfo schema = null;
            String schemaFile = null;
            Item item = null;

            if (sequences.length > 2) {
                item = sequences[2].head();
                if (item != null) {
                    if (item instanceof MapItem) {
                        options = parseMap((MapItem) item);
                    } else {
                        throw new IllegalArgumentException("ext:validate-with-relax-ng options must be a map");
                    }
                }
            }

            item = sequences[1].head();
            if (item instanceof StringValue) {
                schemaFile = item.getStringValue();
            } else {
                if (item instanceof NodeInfo) {
                    schema = (NodeInfo) item;
                } else {
                    throw new IllegalArgumentException("ext:validate-with-relax-ng invalid schema");
                }
            }

            boolean assertValid = getBooleanOption(options, "assert-valid", true);
            boolean dtdCompatibility = getBooleanOption(options, "dtd-compatibility", false);

            boolean valid = false;
            RNGErrorHandler handler = new RNGErrorHandler();

            try {
                VerifierFactory factory = VerifierFactory.newInstance("http://relaxng.org/ns/structure/1.0");
                Processor processor = (Processor) context.getConfiguration().getProcessor();

                Schema docSchema = null;
                CharArrayWriter writer = null;
                Serializer serializer = null;
                ByteArrayInputStream istream = null;

                if (schema == null) {
                    // Cheap and cheerful.
                    try {
                        URL usource = new URL(schemaFile);
                        docSchema = factory.compileSchema(usource.openStream());
                    } catch (MalformedURLException mue) {
                        docSchema = factory.compileSchema(new File(schemaFile));
                    }
                } else {
                    // Hack!
                    writer = new CharArrayWriter();
                    serializer = processor.newSerializer();
                    serializer.setOutputWriter(writer);
                    serializer.serialize(schema);
                    istream = new ByteArrayInputStream(writer.toString().getBytes(StandardCharsets.UTF_8));
                    docSchema = factory.compileSchema(istream, schema.getBaseURI());
                }

                writer = new CharArrayWriter();
                serializer = processor.newSerializer();
                serializer.setOutputWriter(writer);
                serializer.serialize(source);
                istream = new ByteArrayInputStream(writer.toString().getBytes(StandardCharsets.UTF_8));
                InputSource docSource = new InputSource(istream);

                Verifier verifier = docSchema.newVerifier();
                verifier.setErrorHandler(handler);
                valid = verifier.verify(docSource);
            } catch (VerifierConfigurationException | SaxonApiException | IOException | SAXException vce) {
                throw new RuntimeException(vce);
            }

            if (!valid && assertValid) {
                throw new RuntimeException("Invalid document: " + handler.getError().getMessage());
            }

            XdmMap map = new XdmMap();
            map = map.put(new XdmAtomicValue("valid"), new XdmAtomicValue(valid));
            map = map.put(new XdmAtomicValue("document"), new XdmNode(source));
            if (!valid) {
                map = map.put(new XdmAtomicValue("errors"), handler.getErrors());
            }
    
            return map.getUnderlyingValue();
        }
    }

    private boolean getBooleanOption(HashMap options, String name, boolean defvalue) {
        if (options.containsKey(name)) {
            String value = options.get(name);
            if ("true".equals(value) || "false".equals(value)) {
                return "true".equals(value);
            }
            if ("1".equals(value) || "0".equals(value)) {
                return "1".equals(value);
            }
            if ("yes".equals(value) || "no".equals(value)) {
                return "yes".equals(value);
            }
            throw new IllegalArgumentException("Boolean option " + name + " cannot be " + value);
        }
        return defvalue;
    }

    private HashMap parseMap(MapItem item) throws XPathException {
        HashMap options = new HashMap<>();
        for (KeyValuePair kv : item.keyValuePairs()) {
            String key = null;
            if (kv.key.getItemType() == BuiltInAtomicType.STRING) {
                key = kv.key.getStringValue();
            } else {
                throw new IllegalArgumentException("Option map keys must be strings");
            }
            String value = kv.value.getStringValue();
            options.put(key, value);
        }
        return options;
    }

    class RNGErrorHandler implements ErrorHandler {
        SAXParseException error = null;
        XdmArray errors = new XdmArray();

        public SAXParseException getError() {
            return error;
        }

        public XdmArray getErrors() {
            return errors;
        }

        private void addError(SAXParseException err, String etype) {
            XdmMap map = new XdmMap();
            map = map.put(new XdmAtomicValue("type"), new XdmAtomicValue(etype));
            map = map.put(new XdmAtomicValue("error"), new XdmAtomicValue(err.getMessage()));
            if (err.getLineNumber() > 0) {
                map = map.put(new XdmAtomicValue("line"), new XdmAtomicValue(err.getLineNumber()));
            }
            if (err.getColumnNumber() > 0) {
                map = map.put(new XdmAtomicValue("column"), new XdmAtomicValue(err.getColumnNumber()));
            }
            errors = errors.addMember(map);
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
            addError(exception, "warning");
        }

        @Override
        public void error(SAXParseException exception) throws SAXException {
            addError(exception, "error");
            if (error == null) {
                error = exception;
            }
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
            addError(exception, "fatal-error");
            error = exception;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy