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.lang.Nullable;
034import org.springframework.util.Assert;
035import org.springframework.util.CollectionUtils;
036
037/**
038 * {@link EntityResolver} implementation that attempts to resolve schema URLs into
039 * local {@link ClassPathResource classpath resources} using a set of mappings files.
040 *
041 * <p>By default, this class will look for mapping files in the classpath using the
042 * pattern: {@code META-INF/spring.schemas} allowing for multiple files to exist on
043 * the classpath at any one time.
044 *
045 * <p>The format of {@code META-INF/spring.schemas} is a properties file where each line
046 * should be of the form {@code systemId=schema-location} where {@code schema-location}
047 * should also be a schema file in the classpath. Since {@code systemId} is commonly a
048 * URL, one must be careful to escape any ':' characters which are treated as delimiters
049 * in properties files.
050 *
051 * <p>The pattern for the mapping files can be overridden using the
052 * {@link #PluggableSchemaResolver(ClassLoader, String)} constructor.
053 *
054 * @author Rob Harrop
055 * @author Juergen Hoeller
056 * @since 2.0
057 */
058public class PluggableSchemaResolver implements EntityResolver {
059
060        /**
061         * The location of the file that defines schema mappings.
062         * Can be present in multiple JAR files.
063         */
064        public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
065
066
067        private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);
068
069        @Nullable
070        private final ClassLoader classLoader;
071
072        private final String schemaMappingsLocation;
073
074        /** Stores the mapping of schema URL -> local schema path. */
075        @Nullable
076        private volatile Map<String, String> schemaMappings;
077
078
079        /**
080         * Loads the schema URL -> schema file location mappings using the default
081         * mapping file pattern "META-INF/spring.schemas".
082         * @param classLoader the ClassLoader to use for loading
083         * (can be {@code null}) to use the default ClassLoader)
084         * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
085         */
086        public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
087                this.classLoader = classLoader;
088                this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
089        }
090
091        /**
092         * Loads the schema URL -> schema file location mappings using the given
093         * mapping file pattern.
094         * @param classLoader the ClassLoader to use for loading
095         * (can be {@code null}) to use the default ClassLoader)
096         * @param schemaMappingsLocation the location of the file that defines schema mappings
097         * (must not be empty)
098         * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
099         */
100        public PluggableSchemaResolver(@Nullable ClassLoader classLoader, String schemaMappingsLocation) {
101                Assert.hasText(schemaMappingsLocation, "'schemaMappingsLocation' must not be empty");
102                this.classLoader = classLoader;
103                this.schemaMappingsLocation = schemaMappingsLocation;
104        }
105
106
107        @Override
108        @Nullable
109        public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
110                if (logger.isTraceEnabled()) {
111                        logger.trace("Trying to resolve XML entity with public id [" + publicId +
112                                        "] and system id [" + systemId + "]");
113                }
114
115                if (systemId != null) {
116                        String resourceLocation = getSchemaMappings().get(systemId);
117                        if (resourceLocation == null && systemId.startsWith("https:")) {
118                                // Retrieve canonical http schema mapping even for https declaration
119                                resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
120                        }
121                        if (resourceLocation != null) {
122                                Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
123                                try {
124                                        InputSource source = new InputSource(resource.getInputStream());
125                                        source.setPublicId(publicId);
126                                        source.setSystemId(systemId);
127                                        if (logger.isTraceEnabled()) {
128                                                logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
129                                        }
130                                        return source;
131                                }
132                                catch (FileNotFoundException ex) {
133                                        if (logger.isDebugEnabled()) {
134                                                logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
135                                        }
136                                }
137                        }
138                }
139
140                // Fall back to the parser's default behavior.
141                return null;
142        }
143
144        /**
145         * Load the specified schema mappings lazily.
146         */
147        private Map<String, String> getSchemaMappings() {
148                Map<String, String> schemaMappings = this.schemaMappings;
149                if (schemaMappings == null) {
150                        synchronized (this) {
151                                schemaMappings = this.schemaMappings;
152                                if (schemaMappings == null) {
153                                        if (logger.isTraceEnabled()) {
154                                                logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
155                                        }
156                                        try {
157                                                Properties mappings =
158                                                                PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
159                                                if (logger.isTraceEnabled()) {
160                                                        logger.trace("Loaded schema mappings: " + mappings);
161                                                }
162                                                schemaMappings = new ConcurrentHashMap<>(mappings.size());
163                                                CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
164                                                this.schemaMappings = schemaMappings;
165                                        }
166                                        catch (IOException ex) {
167                                                throw new IllegalStateException(
168                                                                "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
169                                        }
170                                }
171                        }
172                }
173                return schemaMappings;
174        }
175
176
177        @Override
178        public String toString() {
179                return "EntityResolver using schema mappings " + getSchemaMappings();
180        }
181
182}