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}