/* * Copyright 2005-2014 the original author or authors. * * 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 org.springframework.xml.xsd.commons; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaCollection; import org.apache.ws.commons.schema.XmlSchemaExternal; import org.apache.ws.commons.schema.XmlSchemaImport; import org.apache.ws.commons.schema.XmlSchemaInclude; import org.apache.ws.commons.schema.XmlSchemaObject; import org.apache.ws.commons.schema.resolver.DefaultURIResolver; import org.apache.ws.commons.schema.resolver.URIResolver; import org.xml.sax.InputSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.UrlResource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.xml.sax.SaxUtils; import org.springframework.xml.validation.XmlValidator; import org.springframework.xml.validation.XmlValidatorFactory; import org.springframework.xml.xsd.XsdSchema; import org.springframework.xml.xsd.XsdSchemaCollection; /** * Implementation of the {@link XsdSchemaCollection} that uses Apache WS-Commons XML Schema. * * <p>Setting the {@link #setInline(boolean) inline} flag to {@code true} will result in all referenced schemas * (included and imported) being merged into the referred schema. When including the schemas into a WSDL, this greatly * simplifies the deployment of the schemas. * * @author Arjen Poutsma * @see <a href="http://ws.apache.org/commons/XmlSchema/">Commons XML Schema</a> * @since 1.5.0 */ public class CommonsXsdSchemaCollection implements XsdSchemaCollection, InitializingBean, ResourceLoaderAware { private static final Log logger = LogFactory.getLog(CommonsXsdSchemaCollection.class); private final XmlSchemaCollection schemaCollection = new XmlSchemaCollection(); private final List<XmlSchema> xmlSchemas = new ArrayList<XmlSchema>(); private Resource[] xsdResources; private boolean inline = false; private URIResolver uriResolver = new ClasspathUriResolver(); private ResourceLoader resourceLoader; /** * Constructs a new, empty instance of the {@code CommonsXsdSchemaCollection}. * * <p>A subsequent call to the {@link #setXsds(Resource[])} is required. */ public CommonsXsdSchemaCollection() { } /** * Constructs a new instance of the {@code CommonsXsdSchemaCollection} based on the given resources. * * @param resources the schema resources to load */ public CommonsXsdSchemaCollection(Resource... resources) { this.xsdResources = resources; } /** * Sets the schema resources to be loaded. * * @param xsdResources the schema resources to be loaded */ public void setXsds(Resource... xsdResources) { this.xsdResources = xsdResources; } /** * Defines whether included schemas should be inlined into the including schema. * * <p>Defaults to {@code false}. */ public void setInline(boolean inline) { this.inline = inline; } /** * Sets the WS-Commons uri resolver to use when resolving (relative) schemas. * * <p>Default is an internal subclass of {@link DefaultURIResolver} which correctly handles schemas on the classpath. */ public void setUriResolver(URIResolver uriResolver) { Assert.notNull(uriResolver, "'uriResolver' must not be null"); this.uriResolver = uriResolver; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void afterPropertiesSet() throws IOException { Assert.notEmpty(xsdResources, "'xsds' must not be empty"); schemaCollection.setSchemaResolver(uriResolver); Set<XmlSchema> processedIncludes = new HashSet<XmlSchema>(); Set<XmlSchema> processedImports = new HashSet<XmlSchema>(); for (Resource xsdResource : xsdResources) { Assert.isTrue(xsdResource.exists(), xsdResource + " does not exit"); try { XmlSchema xmlSchema = schemaCollection.read(SaxUtils.createInputSource(xsdResource)); xmlSchemas.add(xmlSchema); if (inline) { inlineIncludes(xmlSchema, processedIncludes, processedImports); findImports(xmlSchema, processedImports, processedIncludes); } } catch (Exception ex) { throw new CommonsXsdSchemaException("Schema [" + xsdResource + "] could not be loaded", ex); } } if (logger.isInfoEnabled()) { logger.info("Loaded " + StringUtils.arrayToCommaDelimitedString(xsdResources)); } } @Override public XsdSchema[] getXsdSchemas() { XsdSchema[] result = new XsdSchema[xmlSchemas.size()]; for (int i = 0; i < xmlSchemas.size(); i++) { XmlSchema xmlSchema = xmlSchemas.get(i); result[i] = new CommonsXsdSchema(xmlSchema, schemaCollection); } return result; } @Override public XmlValidator createValidator() { try { Resource[] resources = new Resource[xmlSchemas.size()]; for (int i = xmlSchemas.size() - 1; i >= 0; i--) { XmlSchema xmlSchema = xmlSchemas.get(i); String sourceUri = xmlSchema.getSourceURI(); if (StringUtils.hasLength(sourceUri)) { resources[i] = new UrlResource(sourceUri); } } return XmlValidatorFactory .createValidator(resources, XmlValidatorFactory.SCHEMA_W3C_XML); } catch (IOException ex) { throw new CommonsXsdSchemaException(ex.getMessage(), ex); } } private void inlineIncludes(XmlSchema schema, Set<XmlSchema> processedIncludes, Set<XmlSchema> processedImports) { processedIncludes.add(schema); List<XmlSchemaObject> schemaItems = schema.getItems(); for (int i = 0; i < schemaItems.size(); i++) { XmlSchemaObject schemaObject = schemaItems.get(i); if (schemaObject instanceof XmlSchemaInclude) { XmlSchema includedSchema = ((XmlSchemaInclude) schemaObject).getSchema(); if (!processedIncludes.contains(includedSchema)) { inlineIncludes(includedSchema, processedIncludes, processedImports); findImports(includedSchema, processedImports, processedIncludes); List<XmlSchemaObject> includeItems = includedSchema.getItems(); for (XmlSchemaObject includedItem : includeItems) { schemaItems.add(includedItem); } } // remove the <include/> schemaItems.remove(i); i--; } } } private void findImports(XmlSchema schema, Set<XmlSchema> processedImports, Set<XmlSchema> processedIncludes) { processedImports.add(schema); List<XmlSchemaExternal> externals = schema.getExternals(); for (XmlSchemaExternal external : externals) { if (external instanceof XmlSchemaImport) { XmlSchemaImport schemaImport = (XmlSchemaImport) external; XmlSchema importedSchema = schemaImport.getSchema(); if (!"http://www.w3.org/XML/1998/namespace".equals(schemaImport.getNamespace()) && importedSchema != null && !processedImports.contains(importedSchema)) { inlineIncludes(importedSchema, processedIncludes, processedImports); findImports(importedSchema, processedImports, processedIncludes); xmlSchemas.add(importedSchema); } // remove the schemaLocation external.setSchemaLocation(null); } } } public String toString() { StringBuilder builder = new StringBuilder("CommonsXsdSchemaCollection"); builder.append('{'); for (int i = 0; i < xmlSchemas.size(); i++) { XmlSchema schema = xmlSchemas.get(i); builder.append(schema.getTargetNamespace()); if (i < xmlSchemas.size() - 1) { builder.append(','); } } builder.append('}'); return builder.toString(); } private class ClasspathUriResolver extends DefaultURIResolver { @Override public InputSource resolveEntity(String namespace, String schemaLocation, String baseUri) { if (resourceLoader != null) { Resource resource = resourceLoader.getResource(schemaLocation); if (resource.exists()) { return createInputSource(resource); } else if (StringUtils.hasLength(baseUri)) { // let's try and find it relative to the baseUri, see SWS-413 try { Resource baseUriResource = new UrlResource(baseUri); resource = baseUriResource.createRelative(schemaLocation); if (resource.exists()) { return createInputSource(resource); } } catch (IOException e) { // fall through } } // let's try and find it on the classpath, see SWS-362 String classpathLocation = ResourceLoader.CLASSPATH_URL_PREFIX + "/" + schemaLocation; resource = resourceLoader.getResource(classpathLocation); if (resource.exists()) { return createInputSource(resource); } } return super.resolveEntity(namespace, schemaLocation, baseUri); } private InputSource createInputSource(Resource resource) { try { return SaxUtils.createInputSource(resource); } catch (IOException ex) { throw new CommonsXsdSchemaException("Could not resolve location", ex); } } } }