com.adaptris.core.transform.XmlSchemaValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.adaptris.core.transform.XmlSchemaValidator.java

Source

/*
 * Copyright 2015 Adaptris Ltd.
 * 
 * Licensed 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 com.adaptris.core.transform;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import javax.validation.constraints.NotNull;
import javax.xml.XMLConstants;
import javax.xml.transform.sax.SAXSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.lang3.StringUtils;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.adaptris.annotation.AdvancedConfig;
import com.adaptris.annotation.AutoPopulated;
import com.adaptris.annotation.ComponentProfile;
import com.adaptris.annotation.DisplayOrder;
import com.adaptris.annotation.InputFieldDefault;
import com.adaptris.annotation.InputFieldHint;
import com.adaptris.annotation.Removal;
import com.adaptris.core.AdaptrisConnection;
import com.adaptris.core.AdaptrisMessage;
import com.adaptris.core.CoreException;
import com.adaptris.core.ServiceException;
import com.adaptris.core.SharedConnection;
import com.adaptris.core.cache.Cache;
import com.adaptris.core.cache.ExpiringMapCache;
import com.adaptris.core.fs.FsHelper;
import com.adaptris.core.services.cache.CacheConnection;
import com.adaptris.core.util.Args;
import com.adaptris.core.util.ExceptionHelper;
import com.adaptris.core.util.LifecycleHelper;
import com.adaptris.core.util.LoggingHelper;
import com.adaptris.util.TimeInterval;
import com.thoughtworks.xstream.annotations.XStreamAlias;

/**
 * Used with {@link XmlValidationService} to validate an XML message against a schema.
 * <p>
 * This validates an input XML document against a schema. After first use, it caches the schema for re-use against the URL that was
 * resolved as an expression or from static configuration. This means that until first use, no attempt is made to access the schema
 * URL.
 * </p>
 * 
 * @config xml-schema-validator
 * 
 */
@XStreamAlias("xml-schema-validator")
@DisplayOrder(order = { "schema", "schemaCache" })
@ComponentProfile(summary = "Validate an XML document against a schema", recommended = { CacheConnection.class })
public class XmlSchemaValidator extends MessageValidatorImpl {

    private static final int DEFAULT_CACHE_SIZE = 16;

    @InputFieldHint(expression = true)
    // this will force rfc2396 style validation but we don't know how many people are using
    // file:./relative/path which isn't truly rfc2396 compliant...
    // @UrlExpression
    private String schema;
    @AdvancedConfig
    @Deprecated
    @Removal(version = "3.11.0")
    private String schemaMetadataKey;
    @InputFieldDefault(value = "expiring-map-cache, 16 entries, 2 hours")
    @NotNull
    @AutoPopulated
    @AdvancedConfig
    private AdaptrisConnection schemaCache;

    // transient
    private transient SchemaFactory schemaFactory;
    private transient boolean warningLogged;

    public XmlSchemaValidator() {
        setSchemaCache(new CacheConnection(new ExpiringMapCache()
                .withExpiration(new TimeInterval(2L, TimeUnit.HOURS)).withMaxEntries(DEFAULT_CACHE_SIZE)));
    }

    public XmlSchemaValidator(String schema) {
        this();
        setSchema(schema);
    }

    @Deprecated
    public XmlSchemaValidator(String schema, String metadataKey) {
        this(schema);
        setSchemaMetadataKey(metadataKey);
    }

    @Override
    public void validate(AdaptrisMessage msg) throws CoreException {
        try (InputStream in = msg.getInputStream()) {
            Validator validator = this.obtainSchemaToUse(msg).newValidator();
            validator.setErrorHandler(new ErrorHandlerImp());
            validator.validate(new SAXSource(new InputSource(in)));
        } catch (SAXParseException e) {
            throw new ServiceException(String.format("Error validating message[%s] line [%s] column[%s]",
                    e.getMessage(), e.getLineNumber(), e.getColumnNumber()), e);
        } catch (Exception e) {
            throw ExceptionHelper.wrapServiceException("Failed to validate message", e);
        }
    }

    @Override
    public void init() throws CoreException {
        try {
            if (StringUtils.isBlank(getSchema()) && StringUtils.isBlank(getSchemaMetadataKey())) {
                throw new CoreException("metadata-key & schema are blank");
            }
            LifecycleHelper.init(getSchemaCache());
            schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        } catch (Exception e) {
            throw ExceptionHelper.wrapCoreException(e);
        }
    }

    @Override
    public void start() throws CoreException {
        LifecycleHelper.start(getSchemaCache());
    }

    @Override
    public void stop() {
        LifecycleHelper.stop(getSchemaCache());
    }

    @Override
    public void close() {
        LifecycleHelper.close(getSchemaCache());
    }

    private Schema obtainSchemaToUse(AdaptrisMessage msg) throws Exception {
        String schemaUrl = msg.resolve(getSchema());
        if (StringUtils.isNotBlank(getSchemaMetadataKey())) {
            LoggingHelper.logWarning(warningLogged, () -> {
                warningLogged = true;
            }, "schema-metadata-metadata is deprecated, use expression based schema URL instead.");
            if (msg.containsKey(getSchemaMetadataKey())) {
                schemaUrl = msg.getMetadataValue(getSchemaMetadataKey());
            }
        }
        return resolve(schemaUrl);
    }

    private Schema resolve(String urlString) throws Exception {
        Cache cache = getSchemaCache().retrieveConnection(CacheConnection.class).retrieveCache();
        Schema schema = (Schema) cache.get(urlString);
        if (schema == null) {
            schema = schemaFactory.newSchema(toURL(urlString));
            cache.put(urlString, schema);
        }
        return schema;
    }

    // This should cope with when people type in c:/a/b/c instead of a URL.
    private URL toURL(String urlString) throws IOException, URISyntaxException {
        try {
            return new URL(urlString);
        } catch (MalformedURLException e) {
            return FsHelper.createUrlFromString(urlString, true);
        }
    }

    /**
     * Implementation of ErrorHandler which logs then rethrows exceptions.
     */
    private class ErrorHandlerImp implements ErrorHandler {

        public void error(SAXParseException e) throws SAXException {
            log.debug(e.getMessage());
            throw e;
        }

        public void warning(SAXParseException e) throws SAXException {
            log.debug(e.getMessage());
            throw e;
        }

        public void fatalError(SAXParseException e) throws SAXException {
            log.error(e.getMessage());
            throw e;
        }
    }

    // properties

    /**
     * Sets the schema to validate against. May not be null or empty.
     * 
     * @param s the schema to validate against, normally a URL.
     */
    public void setSchema(String s) {
        this.schema = s;
    }

    /**
     * Returns the schema to validate against.
     * 
     * @return the schema to validate against
     */
    public String getSchema() {
        return schema;
    }

    /**
     * Returns the (optional) metadata key against which a schema can be provided at run time.
     * 
     * @return the (optional) metadata key against which a schema can be provided at run time
     * @deprecated since 3.8.4 use an expression based {@link setSchema(String)} instead.
     */
    @Deprecated
    @Removal(version = "3.11.0", message = "use an expression based schema value instead.")
    public String getSchemaMetadataKey() {
        return schemaMetadataKey;
    }

    /**
     * Sets the (optional) metadata key against which a schema can be provided at run time
     * 
     * @param s the (optional) metadata key against which a schema can be provided at run time
     * @deprecated since 3.8.4 use an expression based {@link setSchema(String)} instead.
     */
    @Deprecated
    @Removal(version = "3.11.0", message = "use an expression based schema value instead.")
    public void setSchemaMetadataKey(String s) {
        this.schemaMetadataKey = s;
    }

    public AdaptrisConnection getSchemaCache() {
        return schemaCache;
    }

    /**
     * Configure the internal cache for schemas.
     * <p>
     * While it is possible to configure a distributed cache (a-la ehcache or JSR107) the {@link javax.xml.validation.Schema} object
     * isn't serializable, so you may run into issues. It will be best to stick with {@link ExpiringMapCache} if you want to enable
     * caching. The default behaviour is to cache 16 schemas for a max of 2 hours (last-access) if you don't explicitly configure it
     * differently.
     * </p>
     * 
     * @param cache the cache, generally a {@link CacheConnection} or {@link SharedConnection}.
     */
    public void setSchemaCache(AdaptrisConnection cache) {
        this.schemaCache = Args.notNull(cache, "schemaCache");
    }

    public XmlSchemaValidator withSchemaCache(AdaptrisConnection c) {
        setSchemaCache(c);
        return this;
    }
}