001/**
002 * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 * <p>
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 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
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 */
016package com.mybatisflex.core.mybatis;
017
018import org.apache.ibatis.builder.BaseBuilder;
019import org.apache.ibatis.builder.BuilderException;
020import org.apache.ibatis.builder.xml.XMLMapperBuilder;
021import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
022import org.apache.ibatis.datasource.DataSourceFactory;
023import org.apache.ibatis.executor.ErrorContext;
024import org.apache.ibatis.executor.loader.ProxyFactory;
025import org.apache.ibatis.io.Resources;
026import org.apache.ibatis.io.VFS;
027import org.apache.ibatis.logging.Log;
028import org.apache.ibatis.mapping.DatabaseIdProvider;
029import org.apache.ibatis.mapping.Environment;
030import org.apache.ibatis.parsing.XNode;
031import org.apache.ibatis.parsing.XPathParser;
032import org.apache.ibatis.plugin.Interceptor;
033import org.apache.ibatis.reflection.DefaultReflectorFactory;
034import org.apache.ibatis.reflection.MetaClass;
035import org.apache.ibatis.reflection.ReflectorFactory;
036import org.apache.ibatis.reflection.factory.ObjectFactory;
037import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
038import org.apache.ibatis.session.*;
039import org.apache.ibatis.transaction.TransactionFactory;
040import org.apache.ibatis.type.JdbcType;
041
042import javax.sql.DataSource;
043import java.io.InputStream;
044import java.io.Reader;
045import java.util.Properties;
046
047/**
048 * @author Clinton Begin
049 * @author Kazuki Shimizu
050 */
051public class FlexXMLConfigBuilder extends BaseBuilder {
052
053  private boolean parsed;
054  private final XPathParser parser;
055  private String environment;
056  private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
057
058  public FlexXMLConfigBuilder(Reader reader) {
059    this(reader, null, null);
060  }
061
062  public FlexXMLConfigBuilder(Reader reader, String environment) {
063    this(reader, environment, null);
064  }
065
066  public FlexXMLConfigBuilder(Reader reader, String environment, Properties props) {
067    this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
068  }
069
070  public FlexXMLConfigBuilder(InputStream inputStream) {
071    this(inputStream, null, null);
072  }
073
074  public FlexXMLConfigBuilder(InputStream inputStream, String environment) {
075    this(inputStream, environment, null);
076  }
077
078  public FlexXMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
079    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
080  }
081
082  private FlexXMLConfigBuilder(XPathParser parser, String environment, Properties props) {
083    // 目前暂时只能通过这里替换 configuration
084    // 新的版本已经支持自定义 configuration,但需新版本 release:
085    // 详情 https://github.com/mybatis/mybatis-3/commit/d7826d71a7005a8b4d4e0e7a020db0f6c7e253a4
086    super(new FlexConfiguration());
087    ErrorContext.instance().resource("SQL Mapper Configuration");
088    this.configuration.setVariables(props);
089    this.parsed = false;
090    this.environment = environment;
091    this.parser = parser;
092  }
093
094  public Configuration parse() {
095    if (parsed) {
096      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
097    }
098    parsed = true;
099    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}