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

org.apache.bval.jsr.xml.SchemaManager Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.bval.jsr.xml;

import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.validation.ValidationException;
import javax.xml.XMLConstants;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.UnmarshallerHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.ValidatorHandler;

import org.apache.bval.util.Exceptions;
import org.apache.bval.util.Lazy;
import org.apache.bval.util.StringUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.Reflection;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
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.AttributesImpl;
import org.xml.sax.helpers.XMLFilterImpl;

/**
 * Unmarshals XML converging on latest schema version. Presumes backward compatiblity between schemae.
 */
public class SchemaManager {
    public static class Builder {
        private final Map> data = new LinkedHashMap<>();

        public Builder add(String version, String ns, String resource) {
            data.put(new Key(version, ns), new Lazy<>(() -> SchemaManager.loadSchema(resource)));
            return this;
        }

        public SchemaManager build() {
            return new SchemaManager(data);
        }
    }

    private static class Key implements Comparable {
        private static final Comparator CMP = Comparator.comparing(Key::getVersion).thenComparing(Key::getNs);

        final String version;
        final String ns;

        Key(String version, String ns) {
            super();
            Validate.isTrue(StringUtils.isNotBlank(version), "version cannot be null/empty/blank");
            this.version = version;
            Validate.isTrue(StringUtils.isNotBlank(ns), "ns cannot be null/empty/blank");
            this.ns = ns;
        }

        public String getVersion() {
            return version;
        }

        public String getNs() {
            return ns;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            return Optional.ofNullable(obj).filter(SchemaManager.Key.class::isInstance)
                .map(SchemaManager.Key.class::cast)
                .filter(k -> Objects.equals(this.version, k.version) && Objects.equals(this.ns, k.ns)).isPresent();
        }

        @Override
        public int hashCode() {
            return Objects.hash(version, ns);
        }

        @Override
        public String toString() {
            return String.format("%s:%s", version, ns);
        }

        @Override
        public int compareTo(Key o) {
            return CMP.compare(this, o);
        }
    }

    private class DynamicValidatorHandler extends XMLFilterImpl {
        ContentHandler ch;
        SAXParseException e;

        @Override
        public void setContentHandler(ContentHandler handler) {
            super.setContentHandler(handler);
            this.ch = handler;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            if (getContentHandler() == ch) {
                final String version = Objects.toString(atts.getValue("version"), data.keySet().iterator().next().getVersion());
                final Key schemaKey = new Key(version, uri);
                Exceptions.raiseUnless(data.containsKey(schemaKey), ValidationException::new,
                    "Unknown validation schema %s", schemaKey);

                final Schema schema = data.get(schemaKey).get();
                final ValidatorHandler vh = schema.newValidatorHandler();
                vh.startDocument();
                vh.setContentHandler(ch);
                super.setContentHandler(vh);
            }
            try {
                super.startElement(uri, localName, qName, atts);
            } catch (SAXParseException e) {
                this.e = e;
            }
        }

        @Override
        public void error(SAXParseException e) throws SAXException {
            this.e = e;
            super.error(e);
        }

        @Override
        public void fatalError(SAXParseException e) throws SAXException {
            this.e = e;
            super.fatalError(e);
        }

        void validate() throws SAXParseException {
            if (e != null) {
                throw e;
            }
        }
    }

    //@formatter:off
    private enum XmlAttributeType {
        CDATA, ID, IDREF, IDREFS, NMTOKEN, NMTOKENS, ENTITY, ENTITIES, NOTATION;
        //@formatter:on
    }

    private class SchemaRewriter extends XMLFilterImpl {

        private boolean root = true;
        private Key rootSchemaKey = null;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {

            // if no version attribute is available, we pick up the first known version (aka 1.1 most likely)
            // not sure if that is either possible and correct
            if (root) {
                rootSchemaKey = new Key(
                    Objects.toString(atts.getValue("version"), data.keySet().iterator().next().getVersion()),
                    uri);
            }

            // no matter what we see now, if the namespace from the root element is different, we override the namespace
            // the root version attribute gets also overridden for the root element only
            if (!target.equals(rootSchemaKey)) {
                uri = target.ns;
                if (root) {
                    atts = rewriteVersion(atts);
                    root = false;
                }
            }

            super.startElement(uri, localName, qName, atts);
        }

        private Attributes rewriteVersion(Attributes atts) {
            final AttributesImpl result;
            if (atts instanceof AttributesImpl) {
                result = (AttributesImpl) atts;
            } else {
                result = new AttributesImpl(atts);
            }
            set(result, "", VERSION_ATTRIBUTE, "", XmlAttributeType.CDATA, target.version);
            return result;
        }

        private void set(AttributesImpl attrs, String uri, String localName, String qName, XmlAttributeType type,
            String value) {
            for (int i = 0, sz = attrs.getLength(); i < sz; i++) {
                if (Objects.equals(qName, attrs.getQName(i))
                    || Objects.equals(uri, attrs.getURI(i)) && Objects.equals(localName, attrs.getLocalName(i))) {
                    attrs.setAttribute(i, uri, localName, qName, type.name(), value);
                    return;
                }
            }
            attrs.addAttribute(uri, localName, qName, type.name(), value);
        }
    }

    public static final String VERSION_ATTRIBUTE = "version";

    private static final Logger log = Logger.getLogger(SchemaManager.class.getName());
    private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    private static final SAXParserFactory SAX_PARSER_FACTORY;

    static {
        SAX_PARSER_FACTORY = SAXParserFactory.newInstance();
        SAX_PARSER_FACTORY.setNamespaceAware(true);
    }

    static Schema loadSchema(String resource) {
        final URL schemaUrl = Reflection.loaderFromClassOrThread(SchemaManager.class).getResource(resource);
        try {
            return SCHEMA_FACTORY.newSchema(schemaUrl);
        } catch (SAXException e) {
            log.log(Level.WARNING, String.format("Unable to parse schema: %s", resource), e);
            return null;
        }
    }

    private static Class getObjectFactory(Class type) throws ClassNotFoundException {
        final String className = String.format("%s.%s", type.getPackage().getName(), "ObjectFactory");
        return Reflection.toClass(className, type.getClassLoader());
    }

    private final Key target;
    private final Map> data;
    private final String description;

    private SchemaManager(Map> data) {
        super();
        this.data = Collections.unmodifiableMap(data);
        this.target = data.keySet().stream().skip(data.size() - 1).findFirst().orElseThrow(IllegalStateException::new);
        this.description = target.ns.substring(target.ns.lastIndexOf('/') + 1);
    }

    public Optional getSchema(String ns, String version) {
        return Optional.of(new Key(version, ns)).map(data::get).map(Lazy::get);
    }

    public Optional getSchema(Document document) {
        return Optional.ofNullable(document).map(Document::getDocumentElement)
            .map(e -> getSchema(e.getAttribute(XMLConstants.XMLNS_ATTRIBUTE), e.getAttribute(VERSION_ATTRIBUTE))).get();
    }

    public  Schema requireSchema(Document document, Function exc) throws E {
        return getSchema(document).orElseThrow(() -> Objects.requireNonNull(exc, "exc")
            .apply(String.format("Unknown %s schema", Objects.toString(description, ""))));
    }

    public  T unmarshal(InputSource input, Class type) throws Exception {
        final XMLReader xmlReader = SAX_PARSER_FACTORY.newSAXParser().getXMLReader();

        // validate specified schema:
        final DynamicValidatorHandler schemaValidator = new DynamicValidatorHandler();
        xmlReader.setContentHandler(schemaValidator);

        // rewrite to latest schema, if required:
        final SchemaRewriter schemaRewriter = new SchemaRewriter();
        schemaValidator.setContentHandler(schemaRewriter);

        JAXBContext jc = JAXBContext.newInstance(getObjectFactory(type));
        // unmarshal:
        final UnmarshallerHandler unmarshallerHandler = jc.createUnmarshaller().getUnmarshallerHandler();
        schemaRewriter.setContentHandler(unmarshallerHandler);

        xmlReader.parse(input);
        schemaValidator.validate();

        @SuppressWarnings("unchecked")
        final JAXBElement result = (JAXBElement) unmarshallerHandler.getResult();
        return result.getValue();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy