View Javadoc

1   /*
2    * Copyright 2005-2012 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.xml.xsd.commons;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Set;
24  
25  import org.springframework.beans.factory.InitializingBean;
26  import org.springframework.context.ResourceLoaderAware;
27  import org.springframework.core.io.Resource;
28  import org.springframework.core.io.ResourceLoader;
29  import org.springframework.core.io.UrlResource;
30  import org.springframework.util.Assert;
31  import org.springframework.util.StringUtils;
32  import org.springframework.xml.sax.SaxUtils;
33  import org.springframework.xml.validation.XmlValidator;
34  import org.springframework.xml.validation.XmlValidatorFactory;
35  import org.springframework.xml.xsd.XsdSchema;
36  import org.springframework.xml.xsd.XsdSchemaCollection;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.apache.ws.commons.schema.XmlSchema;
41  import org.apache.ws.commons.schema.XmlSchemaCollection;
42  import org.apache.ws.commons.schema.XmlSchemaExternal;
43  import org.apache.ws.commons.schema.XmlSchemaImport;
44  import org.apache.ws.commons.schema.XmlSchemaInclude;
45  import org.apache.ws.commons.schema.XmlSchemaObject;
46  import org.apache.ws.commons.schema.resolver.DefaultURIResolver;
47  import org.apache.ws.commons.schema.resolver.URIResolver;
48  import org.xml.sax.InputSource;
49  
50  /**
51   * Implementation of the {@link XsdSchemaCollection} that uses Apache WS-Commons XML Schema.
52   * <p/>
53   * Setting the {@link #setInline(boolean) inline} flag to <code>true</code> will result in all referenced schemas
54   * (included and imported) being merged into the referred schema. When including the schemas into a WSDL, this greatly
55   * simplifies the deployment of the schemas.
56   *
57   * @author Arjen Poutsma
58   * @see <a href="http://ws.apache.org/commons/XmlSchema/">Commons XML Schema</a>
59   * @since 1.5.0
60   */
61  public class CommonsXsdSchemaCollection implements XsdSchemaCollection, InitializingBean, ResourceLoaderAware {
62  
63      private static final Log logger = LogFactory.getLog(CommonsXsdSchemaCollection.class);
64  
65      private final XmlSchemaCollection schemaCollection = new XmlSchemaCollection();
66  
67      private final List<XmlSchema> xmlSchemas = new ArrayList<XmlSchema>();
68  
69      private Resource[] xsdResources;
70  
71      private boolean inline = false;
72  
73      private URIResolver uriResolver = new ClasspathUriResolver();
74  
75      private ResourceLoader resourceLoader;
76  
77      /**
78       * Constructs a new, empty instance of the <code>CommonsXsdSchemaCollection</code>.
79       * <p/>
80       * A subsequent call to the {@link #setXsds(Resource[])} is required.
81       */
82      public CommonsXsdSchemaCollection() {
83      }
84  
85      /**
86       * Constructs a new instance of the <code>CommonsXsdSchemaCollection</code> based on the given resources.
87       *
88       * @param resources the schema resources to load
89       */
90      public CommonsXsdSchemaCollection(Resource[] resources) {
91          this.xsdResources = resources;
92      }
93  
94      /**
95       * Sets the schema resources to be loaded.
96       *
97       * @param xsdResources the schema resources to be loaded
98       */
99      public void setXsds(Resource[] xsdResources) {
100         this.xsdResources = xsdResources;
101     }
102 
103     /**
104      * Defines whether included schemas should be inlined into the including schema.
105      * <p/>
106      * Defaults to <code>false</code>.
107      */
108     public void setInline(boolean inline) {
109         this.inline = inline;
110     }
111 
112     /**
113      * Sets the WS-Commons uri resolver to use when resolving (relative) schemas.
114      * <p/>
115      * Default is an internal subclass of {@link DefaultURIResolver} which correctly handles schemas on the classpath.
116      */
117     public void setUriResolver(URIResolver uriResolver) {
118         Assert.notNull(uriResolver, "'uriResolver' must not be null");
119         this.uriResolver = uriResolver;
120     }
121 
122     public void setResourceLoader(ResourceLoader resourceLoader) {
123         this.resourceLoader = resourceLoader;
124     }
125 
126     public void afterPropertiesSet() throws IOException {
127         Assert.notEmpty(xsdResources, "'xsds' must not be empty");
128 
129         schemaCollection.setSchemaResolver(uriResolver);
130 
131         Set<XmlSchema> processedIncludes = new HashSet<XmlSchema>();
132         Set<XmlSchema> processedImports = new HashSet<XmlSchema>();
133 
134         for (Resource xsdResource : xsdResources) {
135             Assert.isTrue(xsdResource.exists(), xsdResource + " does not exit");
136             try {
137                 XmlSchema xmlSchema =
138                         schemaCollection.read(SaxUtils.createInputSource(xsdResource));
139                 xmlSchemas.add(xmlSchema);
140 
141                 if (inline) {
142                     inlineIncludes(xmlSchema, processedIncludes, processedImports);
143                     findImports(xmlSchema, processedImports, processedIncludes);
144                 }
145             }
146             catch (Exception ex) {
147                 throw new CommonsXsdSchemaException("Schema [" + xsdResource + "] could not be loaded", ex);
148             }
149         }
150         if (logger.isInfoEnabled()) {
151             logger.info("Loaded " + StringUtils.arrayToCommaDelimitedString(xsdResources));
152         }
153 
154     }
155 
156     public XsdSchema[] getXsdSchemas() {
157         XsdSchema[] result = new XsdSchema[xmlSchemas.size()];
158         for (int i = 0; i < xmlSchemas.size(); i++) {
159             XmlSchema xmlSchema = xmlSchemas.get(i);
160             result[i] = new CommonsXsdSchema(xmlSchema, schemaCollection);
161         }
162         return result;
163     }
164 
165     public XmlValidator createValidator() throws IOException {
166         Resource[] resources = new Resource[xmlSchemas.size()];
167         for (int i = xmlSchemas.size() - 1; i >= 0; i--) {
168             XmlSchema xmlSchema = xmlSchemas.get(i);
169             String sourceUri = xmlSchema.getSourceURI();
170             if (StringUtils.hasLength(sourceUri)) {
171                 resources[i] = new UrlResource(sourceUri);
172             }
173         }
174         return XmlValidatorFactory.createValidator(resources, XmlValidatorFactory.SCHEMA_W3C_XML);
175     }
176 
177     private void inlineIncludes(XmlSchema schema, Set<XmlSchema> processedIncludes, Set<XmlSchema> processedImports) {
178         processedIncludes.add(schema);
179 
180         List<XmlSchemaObject> schemaItems = schema.getItems();
181         for (int i = 0; i < schemaItems.size(); i++) {
182             XmlSchemaObject schemaObject = schemaItems.get(i);
183             if (schemaObject instanceof XmlSchemaInclude) {
184                 XmlSchema includedSchema = ((XmlSchemaInclude) schemaObject).getSchema();
185                 if (!processedIncludes.contains(includedSchema)) {
186                     inlineIncludes(includedSchema, processedIncludes, processedImports);
187                     findImports(includedSchema, processedImports, processedIncludes);
188                     List<XmlSchemaObject> includeItems = includedSchema.getItems();
189                     for (XmlSchemaObject includedItem : includeItems) {
190                         schemaItems.add(includedItem);
191                     }
192                 }
193                 // remove the <include/>
194                 schemaItems.remove(i);
195                 i--;
196             }
197         }
198     }
199 
200     private void findImports(XmlSchema schema, Set<XmlSchema> processedImports, Set<XmlSchema> processedIncludes) {
201         processedImports.add(schema);
202         List<XmlSchemaExternal> externals = schema.getExternals();
203         for (XmlSchemaExternal external : externals) {
204             if (external instanceof XmlSchemaImport) {
205                 XmlSchemaImport schemaImport = (XmlSchemaImport) external;
206                 XmlSchema importedSchema = schemaImport.getSchema();
207                 if (!"http://www.w3.org/XML/1998/namespace".equals(schemaImport.getNamespace()) &&
208                         importedSchema != null && !processedImports.contains(importedSchema)) {
209                     inlineIncludes(importedSchema, processedIncludes, processedImports);
210                     findImports(importedSchema, processedImports, processedIncludes);
211                     xmlSchemas.add(importedSchema);
212                 }
213                 // remove the schemaLocation
214                 external.setSchemaLocation(null);
215             }
216         }
217     }
218 
219     public String toString() {
220         StringBuilder builder = new StringBuilder("CommonsXsdSchemaCollection");
221         builder.append('{');
222         for (int i = 0; i < xmlSchemas.size(); i++) {
223             XmlSchema schema = xmlSchemas.get(i);
224             builder.append(schema.getTargetNamespace());
225             if (i < xmlSchemas.size() - 1) {
226                 builder.append(',');
227             }
228         }
229         builder.append('}');
230         return builder.toString();
231     }
232 
233     private class ClasspathUriResolver extends DefaultURIResolver {
234 
235         @Override
236         public InputSource resolveEntity(String namespace, String schemaLocation, String baseUri) {
237             if (resourceLoader != null) {
238                 Resource resource = resourceLoader.getResource(schemaLocation);
239                 if (resource.exists()) {
240                     return createInputSource(resource);
241                 }
242                 else if (StringUtils.hasLength(baseUri)) {
243                     // let's try and find it relative to the baseUri, see SWS-413
244                     try {
245                         Resource baseUriResource = new UrlResource(baseUri);
246                         resource = baseUriResource.createRelative(schemaLocation);
247                         if (resource.exists()) {
248                             return createInputSource(resource);
249                         }
250                     }
251                     catch (IOException e) {
252                         // fall through
253                     }
254                 }
255                 // let's try and find it on the classpath, see SWS-362
256                 String classpathLocation = ResourceLoader.CLASSPATH_URL_PREFIX + "/" + schemaLocation;
257                 resource = resourceLoader.getResource(classpathLocation);
258                 if (resource.exists()) {
259                     return createInputSource(resource);
260                 }
261             }
262             return super.resolveEntity(namespace, schemaLocation, baseUri);
263         }
264 
265         private InputSource createInputSource(Resource resource) {
266             try {
267                 return SaxUtils.createInputSource(resource);
268             }
269             catch (IOException ex) {
270                 throw new CommonsXsdSchemaException("Could not resolve location", ex);
271             }
272         }
273     }
274 
275 }