001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans.factory.xml;
018
019import java.io.FileNotFoundException;
020import java.io.IOException;
021import java.util.Map;
022import java.util.Properties;
023import java.util.concurrent.ConcurrentHashMap;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.xml.sax.EntityResolver;
028import org.xml.sax.InputSource;
029
030import org.springframework.core.io.ClassPathResource;
031import org.springframework.core.io.Resource;
032import org.springframework.core.io.support.PropertiesLoaderUtils;
033import org.springframework.util.Assert;
034import org.springframework.util.CollectionUtils;
035
036/**
037 * {@link EntityResolver} implementation that attempts to resolve schema URLs into
038 * local {@link ClassPathResource classpath resources} using a set of mappings files.
039 *
040 * <p>By default, this class will look for mapping files in the classpath using the
041 * pattern: {@code META-INF/spring.schemas} allowing for multiple files to exist on
042 * the classpath at any one time.
043 *
044 * <p>The format of {@code META-INF/spring.schemas} is a properties file where each line
045 * should be of the form {@code systemId=schema-location} where {@code schema-location}
046 * should also be a schema file in the classpath. Since {@code systemId} is commonly a
047 * URL, one must be careful to escape any ':' characters which are treated as delimiters
048 * in properties files.
049 *
050 * <p>The pattern for the mapping files can be overridden using the
051 * {@link #PluggableSchemaResolver(ClassLoader, String)} constructor.
052 *
053 * @author Rob Harrop
054 * @author Juergen Hoeller
055 * @since 2.0
056 */
057public class PluggableSchemaResolver implements EntityResolver {
058
059        /**
060         * The location of the file that defines schema mappings.
061         * Can be present in multiple JAR files.
062         */
063        public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
064
065
066        private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);
067
068        private final ClassLoader classLoader;
069
070        private final String schemaMappingsLocation;
071
072        /** Stores the mapping of schema URL -> local schema path */
073        private volatile Map<String, String> schemaMappings;
074
075
076        /**
077         * Loads the schema URL -> schema file location mappings using the default
078         * mapping file pattern "META-INF/spring.schemas".
079         * @param classLoader the ClassLoader to use for loading
080         * (can be {@code null}) to use the default ClassLoader)
081         * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
082         */
083        public PluggableSchemaResolver(ClassLoader classLoader) {
084                this.classLoader = classLoader;
085                this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
086        }
087
088        /**
089         * Loads the schema URL -> schema file location mappings using the given
090         * mapping file pattern.
091         * @param classLoader the ClassLoader to use for loading
092         * (can be {@code null}) to use the default ClassLoader)
093         * @param schemaMappingsLocation the location of the file that defines schema mappings
094         * (must not be empty)
095         * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
096         */
097        public PluggableSchemaResolver(ClassLoader classLoader, String schemaMappingsLocation) {
098                Assert.hasText(schemaMappingsLocation, "'schemaMappingsLocation' must not be empty");
099                this.classLoader = classLoader;
100                this.schemaMappingsLocation = schemaMappingsLocation;
101        }
102
103
104        @Override
105        public InputSource resolveEntity(String publicId, String systemId) throws IOException {
106                if (logger.isTraceEnabled()) {
107                        logger.trace("Trying to resolve XML entity with public id [" + publicId +
108                                        "] and system id [" + systemId + "]");
109                }
110
111                if (systemId != null) {
112                        String resourceLocation = getSchemaMappings().get(systemId);
113                        if (resourceLocation == null && systemId.startsWith("https:")) {
114                                // Retrieve canonical http schema mapping even for https declaration
115                                resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
116                        }
117                        if (resourceLocation != null) {
118                                Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
119                                try {
120                                        InputSource source = new InputSource(resource.getInputStream());
121                                        source.setPublicId(publicId);
122                                        source.setSystemId(systemId);
123                                        if (logger.isDebugEnabled()) {
124                                                logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
125                                        }
126                                        return source;
127                                }
128                                catch (FileNotFoundException ex) {
129                                        if (logger.isDebugEnabled()) {
130                                                logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
131                                        }
132                                }
133                        }
134                }
135
136                // Fall back to the parser's default behavior.
137                return null;
138        }
139
140        /**
141         * Load the specified schema mappings lazily.
142         */
143        private Map<String, String> getSchemaMappings() {
144                Map<String, String> schemaMappings = this.schemaMappings;
145                if (schemaMappings == null) {
146                        synchronized (this) {
147                                schemaMappings = this.schemaMappings;
148                                if (schemaMappings == null) {
149                                        if (logger.isDebugEnabled()) {
150                                                logger.debug("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
151                                        }
152                                        try {
153                                                Properties mappings =
154                                                                PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
155                                                if (logger.isDebugEnabled()) {
156                                                        logger.debug("Loaded schema mappings: " + mappings);
157                                                }
158                                                schemaMappings = new ConcurrentHashMap<String, String>(mappings.size());
159                                                CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
160                                                this.schemaMappings = schemaMappings;
161                                        }
162                                        catch (IOException ex) {
163                                                throw new IllegalStateException(
164                                                                "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
165                                        }
166                                }
167                        }
168                }
169                return schemaMappings;
170        }
171
172
173        @Override
174        public String toString() {
175                return "EntityResolver using schema mappings " + getSchemaMappings();
176        }
177
178}