View Javadoc
1   /*
2    *    Copyright 2009-2022 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  package org.apache.ibatis.builder.xml;
17  
18  import java.io.InputStream;
19  import java.io.Reader;
20  import java.util.Properties;
21  
22  import javax.sql.DataSource;
23  
24  import org.apache.ibatis.builder.BaseBuilder;
25  import org.apache.ibatis.builder.BuilderException;
26  import org.apache.ibatis.datasource.DataSourceFactory;
27  import org.apache.ibatis.executor.ErrorContext;
28  import org.apache.ibatis.executor.loader.ProxyFactory;
29  import org.apache.ibatis.io.Resources;
30  import org.apache.ibatis.io.VFS;
31  import org.apache.ibatis.logging.Log;
32  import org.apache.ibatis.mapping.DatabaseIdProvider;
33  import org.apache.ibatis.mapping.Environment;
34  import org.apache.ibatis.parsing.XNode;
35  import org.apache.ibatis.parsing.XPathParser;
36  import org.apache.ibatis.plugin.Interceptor;
37  import org.apache.ibatis.reflection.DefaultReflectorFactory;
38  import org.apache.ibatis.reflection.MetaClass;
39  import org.apache.ibatis.reflection.ReflectorFactory;
40  import org.apache.ibatis.reflection.factory.ObjectFactory;
41  import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
42  import org.apache.ibatis.session.AutoMappingBehavior;
43  import org.apache.ibatis.session.AutoMappingUnknownColumnBehavior;
44  import org.apache.ibatis.session.Configuration;
45  import org.apache.ibatis.session.ExecutorType;
46  import org.apache.ibatis.session.LocalCacheScope;
47  import org.apache.ibatis.transaction.TransactionFactory;
48  import org.apache.ibatis.type.JdbcType;
49  
50  /**
51   * @author Clinton Begin
52   * @author Kazuki Shimizu
53   */
54  public class XMLConfigBuilder extends BaseBuilder {
55  
56    private boolean parsed;
57    private final XPathParser parser;
58    private String environment;
59    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
60  
61    public XMLConfigBuilder(Reader reader) {
62      this(reader, null, null);
63    }
64  
65    public XMLConfigBuilder(Reader reader, String environment) {
66      this(reader, environment, null);
67    }
68  
69    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
70      this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
71    }
72  
73    public XMLConfigBuilder(InputStream inputStream) {
74      this(inputStream, null, null);
75    }
76  
77    public XMLConfigBuilder(InputStream inputStream, String environment) {
78      this(inputStream, environment, null);
79    }
80  
81    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
82      this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
83    }
84  
85    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
86      super(new Configuration());
87      ErrorContext.instance().resource("SQL Mapper Configuration");
88      this.configuration.setVariables(props);
89      this.parsed = false;
90      this.environment = environment;
91      this.parser = parser;
92    }
93  
94    public Configuration parse() {
95      if (parsed) {
96        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
97      }
98      parsed = true;
99      parseConfiguration(parser.evalNode("/configuration"));
100     return configuration;
101   }
102 
103   private void parseConfiguration(XNode root) {
104     try {
105       // issue #117 read properties first
106       propertiesElement(root.evalNode("properties"));
107       Properties settings = settingsAsProperties(root.evalNode("settings"));
108       loadCustomVfs(settings);
109       loadCustomLogImpl(settings);
110       typeAliasesElement(root.evalNode("typeAliases"));
111       pluginElement(root.evalNode("plugins"));
112       objectFactoryElement(root.evalNode("objectFactory"));
113       objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
114       reflectorFactoryElement(root.evalNode("reflectorFactory"));
115       settingsElement(settings);
116       // read it after objectFactory and objectWrapperFactory issue #631
117       environmentsElement(root.evalNode("environments"));
118       databaseIdProviderElement(root.evalNode("databaseIdProvider"));
119       typeHandlerElement(root.evalNode("typeHandlers"));
120       mapperElement(root.evalNode("mappers"));
121     } catch (Exception e) {
122       throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
123     }
124   }
125 
126   private Properties settingsAsProperties(XNode context) {
127     if (context == null) {
128       return new Properties();
129     }
130     Properties props = context.getChildrenAsProperties();
131     // Check that all settings are known to the configuration class
132     MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
133     for (Object key : props.keySet()) {
134       if (!metaConfig.hasSetter(String.valueOf(key))) {
135         throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
136       }
137     }
138     return props;
139   }
140 
141   private void loadCustomVfs(Properties props) throws ClassNotFoundException {
142     String value = props.getProperty("vfsImpl");
143     if (value != null) {
144       String[] clazzes = value.split(",");
145       for (String clazz : clazzes) {
146         if (!clazz.isEmpty()) {
147           @SuppressWarnings("unchecked")
148           Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
149           configuration.setVfsImpl(vfsImpl);
150         }
151       }
152     }
153   }
154 
155   private void loadCustomLogImpl(Properties props) {
156     Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
157     configuration.setLogImpl(logImpl);
158   }
159 
160   private void typeAliasesElement(XNode parent) {
161     if (parent != null) {
162       for (XNode child : parent.getChildren()) {
163         if ("package".equals(child.getName())) {
164           String typeAliasPackage = child.getStringAttribute("name");
165           configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
166         } else {
167           String alias = child.getStringAttribute("alias");
168           String type = child.getStringAttribute("type");
169           try {
170             Class<?> clazz = Resources.classForName(type);
171             if (alias == null) {
172               typeAliasRegistry.registerAlias(clazz);
173             } else {
174               typeAliasRegistry.registerAlias(alias, clazz);
175             }
176           } catch (ClassNotFoundException e) {
177             throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
178           }
179         }
180       }
181     }
182   }
183 
184   private void pluginElement(XNode parent) throws Exception {
185     if (parent != null) {
186       for (XNode child : parent.getChildren()) {
187         String interceptor = child.getStringAttribute("interceptor");
188         Properties properties = child.getChildrenAsProperties();
189         Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
190         interceptorInstance.setProperties(properties);
191         configuration.addInterceptor(interceptorInstance);
192       }
193     }
194   }
195 
196   private void objectFactoryElement(XNode context) throws Exception {
197     if (context != null) {
198       String type = context.getStringAttribute("type");
199       Properties properties = context.getChildrenAsProperties();
200       ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
201       factory.setProperties(properties);
202       configuration.setObjectFactory(factory);
203     }
204   }
205 
206   private void objectWrapperFactoryElement(XNode context) throws Exception {
207     if (context != null) {
208       String type = context.getStringAttribute("type");
209       ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();
210       configuration.setObjectWrapperFactory(factory);
211     }
212   }
213 
214   private void reflectorFactoryElement(XNode context) throws Exception {
215     if (context != null) {
216       String type = context.getStringAttribute("type");
217       ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();
218       configuration.setReflectorFactory(factory);
219     }
220   }
221 
222   private void propertiesElement(XNode context) throws Exception {
223     if (context != null) {
224       Properties defaults = context.getChildrenAsProperties();
225       String resource = context.getStringAttribute("resource");
226       String url = context.getStringAttribute("url");
227       if (resource != null && url != null) {
228         throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
229       }
230       if (resource != null) {
231         defaults.putAll(Resources.getResourceAsProperties(resource));
232       } else if (url != null) {
233         defaults.putAll(Resources.getUrlAsProperties(url));
234       }
235       Properties vars = configuration.getVariables();
236       if (vars != null) {
237         defaults.putAll(vars);
238       }
239       parser.setVariables(defaults);
240       configuration.setVariables(defaults);
241     }
242   }
243 
244   private void settingsElement(Properties props) {
245     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
246     configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
247     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
248     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
249     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
250     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
251     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
252     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
253     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
254     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
255     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
256     configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
257     configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
258     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
259     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
260     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
261     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
262     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
263     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
264     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
265     configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
266     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
267     configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
268     configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
269     configuration.setLogPrefix(props.getProperty("logPrefix"));
270     configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
271     configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
272     configuration.setArgNameBasedConstructorAutoMapping(booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
273     configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
274     configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
275   }
276 
277   private void environmentsElement(XNode context) throws Exception {
278     if (context != null) {
279       if (environment == null) {
280         environment = context.getStringAttribute("default");
281       }
282       for (XNode child : context.getChildren()) {
283         String id = child.getStringAttribute("id");
284         if (isSpecifiedEnvironment(id)) {
285           TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
286           DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
287           DataSource dataSource = dsFactory.getDataSource();
288           Environment.Builder environmentBuilder = new Environment.Builder(id)
289               .transactionFactory(txFactory)
290               .dataSource(dataSource);
291           configuration.setEnvironment(environmentBuilder.build());
292           break;
293         }
294       }
295     }
296   }
297 
298   private void databaseIdProviderElement(XNode context) throws Exception {
299     DatabaseIdProvider databaseIdProvider = null;
300     if (context != null) {
301       String type = context.getStringAttribute("type");
302       // awful patch to keep backward compatibility
303       if ("VENDOR".equals(type)) {
304         type = "DB_VENDOR";
305       }
306       Properties properties = context.getChildrenAsProperties();
307       databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
308       databaseIdProvider.setProperties(properties);
309     }
310     Environment environment = configuration.getEnvironment();
311     if (environment != null && databaseIdProvider != null) {
312       String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
313       configuration.setDatabaseId(databaseId);
314     }
315   }
316 
317   private TransactionFactory transactionManagerElement(XNode context) throws Exception {
318     if (context != null) {
319       String type = context.getStringAttribute("type");
320       Properties props = context.getChildrenAsProperties();
321       TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
322       factory.setProperties(props);
323       return factory;
324     }
325     throw new BuilderException("Environment declaration requires a TransactionFactory.");
326   }
327 
328   private DataSourceFactory dataSourceElement(XNode context) throws Exception {
329     if (context != null) {
330       String type = context.getStringAttribute("type");
331       Properties props = context.getChildrenAsProperties();
332       DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
333       factory.setProperties(props);
334       return factory;
335     }
336     throw new BuilderException("Environment declaration requires a DataSourceFactory.");
337   }
338 
339   private void typeHandlerElement(XNode parent) {
340     if (parent != null) {
341       for (XNode child : parent.getChildren()) {
342         if ("package".equals(child.getName())) {
343           String typeHandlerPackage = child.getStringAttribute("name");
344           typeHandlerRegistry.register(typeHandlerPackage);
345         } else {
346           String javaTypeName = child.getStringAttribute("javaType");
347           String jdbcTypeName = child.getStringAttribute("jdbcType");
348           String handlerTypeName = child.getStringAttribute("handler");
349           Class<?> javaTypeClass = resolveClass(javaTypeName);
350           JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
351           Class<?> typeHandlerClass = resolveClass(handlerTypeName);
352           if (javaTypeClass != null) {
353             if (jdbcType == null) {
354               typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
355             } else {
356               typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
357             }
358           } else {
359             typeHandlerRegistry.register(typeHandlerClass);
360           }
361         }
362       }
363     }
364   }
365 
366   private void mapperElement(XNode parent) throws Exception {
367     if (parent != null) {
368       for (XNode child : parent.getChildren()) {
369         if ("package".equals(child.getName())) {
370           String mapperPackage = child.getStringAttribute("name");
371           configuration.addMappers(mapperPackage);
372         } else {
373           String resource = child.getStringAttribute("resource");
374           String url = child.getStringAttribute("url");
375           String mapperClass = child.getStringAttribute("class");
376           if (resource != null && url == null && mapperClass == null) {
377             ErrorContext.instance().resource(resource);
378             try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
379               XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
380               mapperParser.parse();
381             }
382           } else if (resource == null && url != null && mapperClass == null) {
383             ErrorContext.instance().resource(url);
384             try(InputStream inputStream = Resources.getUrlAsStream(url)){
385               XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
386               mapperParser.parse();
387             }
388           } else if (resource == null && url == null && mapperClass != null) {
389             Class<?> mapperInterface = Resources.classForName(mapperClass);
390             configuration.addMapper(mapperInterface);
391           } else {
392             throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
393           }
394         }
395       }
396     }
397   }
398 
399   private boolean isSpecifiedEnvironment(String id) {
400     if (environment == null) {
401       throw new BuilderException("No environment specified.");
402     }
403     if (id == null) {
404       throw new BuilderException("Environment requires an id attribute.");
405     }
406     return environment.equals(id);
407   }
408 
409 }