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}