XMLMapperBuilder.java

  1. /*
  2.  *    Copyright 2009-2021 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. import java.io.InputStream;
  18. import java.io.Reader;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.HashMap;
  24. import java.util.Iterator;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Properties;

  28. import org.apache.ibatis.builder.BaseBuilder;
  29. import org.apache.ibatis.builder.BuilderException;
  30. import org.apache.ibatis.builder.CacheRefResolver;
  31. import org.apache.ibatis.builder.IncompleteElementException;
  32. import org.apache.ibatis.builder.MapperBuilderAssistant;
  33. import org.apache.ibatis.builder.ResultMapResolver;
  34. import org.apache.ibatis.cache.Cache;
  35. import org.apache.ibatis.executor.ErrorContext;
  36. import org.apache.ibatis.io.Resources;
  37. import org.apache.ibatis.mapping.Discriminator;
  38. import org.apache.ibatis.mapping.ParameterMapping;
  39. import org.apache.ibatis.mapping.ParameterMode;
  40. import org.apache.ibatis.mapping.ResultFlag;
  41. import org.apache.ibatis.mapping.ResultMap;
  42. import org.apache.ibatis.mapping.ResultMapping;
  43. import org.apache.ibatis.parsing.XNode;
  44. import org.apache.ibatis.parsing.XPathParser;
  45. import org.apache.ibatis.reflection.MetaClass;
  46. import org.apache.ibatis.session.Configuration;
  47. import org.apache.ibatis.type.JdbcType;
  48. import org.apache.ibatis.type.TypeHandler;

  49. /**
  50.  * @author Clinton Begin
  51.  * @author Kazuki Shimizu
  52.  */
  53. public class XMLMapperBuilder extends BaseBuilder {

  54.   private final XPathParser parser;
  55.   private final MapperBuilderAssistant builderAssistant;
  56.   private final Map<String, XNode> sqlFragments;
  57.   private final String resource;

  58.   @Deprecated
  59.   public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
  60.     this(reader, configuration, resource, sqlFragments);
  61.     this.builderAssistant.setCurrentNamespace(namespace);
  62.   }

  63.   @Deprecated
  64.   public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  65.     this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()),
  66.         configuration, resource, sqlFragments);
  67.   }

  68.   public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
  69.     this(inputStream, configuration, resource, sqlFragments);
  70.     this.builderAssistant.setCurrentNamespace(namespace);
  71.   }

  72.   public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  73.     this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
  74.         configuration, resource, sqlFragments);
  75.   }

  76.   private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  77.     super(configuration);
  78.     this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
  79.     this.parser = parser;
  80.     this.sqlFragments = sqlFragments;
  81.     this.resource = resource;
  82.   }

  83.   public void parse() {
  84.     if (!configuration.isResourceLoaded(resource)) {
  85.       configurationElement(parser.evalNode("/mapper"));
  86.       configuration.addLoadedResource(resource);
  87.       bindMapperForNamespace();
  88.     }

  89.     parsePendingResultMaps();
  90.     parsePendingCacheRefs();
  91.     parsePendingStatements();
  92.   }

  93.   public XNode getSqlFragment(String refid) {
  94.     return sqlFragments.get(refid);
  95.   }

  96.   private void configurationElement(XNode context) {
  97.     try {
  98.       String namespace = context.getStringAttribute("namespace");
  99.       if (namespace == null || namespace.isEmpty()) {
  100.         throw new BuilderException("Mapper's namespace cannot be empty");
  101.       }
  102.       builderAssistant.setCurrentNamespace(namespace);
  103.       cacheRefElement(context.evalNode("cache-ref"));
  104.       cacheElement(context.evalNode("cache"));
  105.       parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  106.       resultMapElements(context.evalNodes("/mapper/resultMap"));
  107.       sqlElement(context.evalNodes("/mapper/sql"));
  108.       buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  109.     } catch (Exception e) {
  110.       throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  111.     }
  112.   }

  113.   private void buildStatementFromContext(List<XNode> list) {
  114.     if (configuration.getDatabaseId() != null) {
  115.       buildStatementFromContext(list, configuration.getDatabaseId());
  116.     }
  117.     buildStatementFromContext(list, null);
  118.   }

  119.   private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  120.     for (XNode context : list) {
  121.       final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
  122.       try {
  123.         statementParser.parseStatementNode();
  124.       } catch (IncompleteElementException e) {
  125.         configuration.addIncompleteStatement(statementParser);
  126.       }
  127.     }
  128.   }

  129.   private void parsePendingResultMaps() {
  130.     Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
  131.     synchronized (incompleteResultMaps) {
  132.       Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
  133.       while (iter.hasNext()) {
  134.         try {
  135.           iter.next().resolve();
  136.           iter.remove();
  137.         } catch (IncompleteElementException e) {
  138.           // ResultMap is still missing a resource...
  139.         }
  140.       }
  141.     }
  142.   }

  143.   private void parsePendingCacheRefs() {
  144.     Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
  145.     synchronized (incompleteCacheRefs) {
  146.       Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
  147.       while (iter.hasNext()) {
  148.         try {
  149.           iter.next().resolveCacheRef();
  150.           iter.remove();
  151.         } catch (IncompleteElementException e) {
  152.           // Cache ref is still missing a resource...
  153.         }
  154.       }
  155.     }
  156.   }

  157.   private void parsePendingStatements() {
  158.     Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
  159.     synchronized (incompleteStatements) {
  160.       Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
  161.       while (iter.hasNext()) {
  162.         try {
  163.           iter.next().parseStatementNode();
  164.           iter.remove();
  165.         } catch (IncompleteElementException e) {
  166.           // Statement is still missing a resource...
  167.         }
  168.       }
  169.     }
  170.   }

  171.   private void cacheRefElement(XNode context) {
  172.     if (context != null) {
  173.       configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
  174.       CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
  175.       try {
  176.         cacheRefResolver.resolveCacheRef();
  177.       } catch (IncompleteElementException e) {
  178.         configuration.addIncompleteCacheRef(cacheRefResolver);
  179.       }
  180.     }
  181.   }

  182.   private void cacheElement(XNode context) {
  183.     if (context != null) {
  184.       String type = context.getStringAttribute("type", "PERPETUAL");
  185.       Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
  186.       String eviction = context.getStringAttribute("eviction", "LRU");
  187.       Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
  188.       Long flushInterval = context.getLongAttribute("flushInterval");
  189.       Integer size = context.getIntAttribute("size");
  190.       boolean readWrite = !context.getBooleanAttribute("readOnly", false);
  191.       boolean blocking = context.getBooleanAttribute("blocking", false);
  192.       Properties props = context.getChildrenAsProperties();
  193.       builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  194.     }
  195.   }

  196.   private void parameterMapElement(List<XNode> list) {
  197.     for (XNode parameterMapNode : list) {
  198.       String id = parameterMapNode.getStringAttribute("id");
  199.       String type = parameterMapNode.getStringAttribute("type");
  200.       Class<?> parameterClass = resolveClass(type);
  201.       List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
  202.       List<ParameterMapping> parameterMappings = new ArrayList<>();
  203.       for (XNode parameterNode : parameterNodes) {
  204.         String property = parameterNode.getStringAttribute("property");
  205.         String javaType = parameterNode.getStringAttribute("javaType");
  206.         String jdbcType = parameterNode.getStringAttribute("jdbcType");
  207.         String resultMap = parameterNode.getStringAttribute("resultMap");
  208.         String mode = parameterNode.getStringAttribute("mode");
  209.         String typeHandler = parameterNode.getStringAttribute("typeHandler");
  210.         Integer numericScale = parameterNode.getIntAttribute("numericScale");
  211.         ParameterMode modeEnum = resolveParameterMode(mode);
  212.         Class<?> javaTypeClass = resolveClass(javaType);
  213.         JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  214.         Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  215.         ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
  216.         parameterMappings.add(parameterMapping);
  217.       }
  218.       builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
  219.     }
  220.   }

  221.   private void resultMapElements(List<XNode> list) {
  222.     for (XNode resultMapNode : list) {
  223.       try {
  224.         resultMapElement(resultMapNode);
  225.       } catch (IncompleteElementException e) {
  226.         // ignore, it will be retried
  227.       }
  228.     }
  229.   }

  230.   private ResultMap resultMapElement(XNode resultMapNode) {
  231.     return resultMapElement(resultMapNode, Collections.emptyList(), null);
  232.   }

  233.   private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
  234.     ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  235.     String type = resultMapNode.getStringAttribute("type",
  236.         resultMapNode.getStringAttribute("ofType",
  237.             resultMapNode.getStringAttribute("resultType",
  238.                 resultMapNode.getStringAttribute("javaType"))));
  239.     Class<?> typeClass = resolveClass(type);
  240.     if (typeClass == null) {
  241.       typeClass = inheritEnclosingType(resultMapNode, enclosingType);
  242.     }
  243.     Discriminator discriminator = null;
  244.     List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
  245.     List<XNode> resultChildren = resultMapNode.getChildren();
  246.     for (XNode resultChild : resultChildren) {
  247.       if ("constructor".equals(resultChild.getName())) {
  248.         processConstructorElement(resultChild, typeClass, resultMappings);
  249.       } else if ("discriminator".equals(resultChild.getName())) {
  250.         discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
  251.       } else {
  252.         List<ResultFlag> flags = new ArrayList<>();
  253.         if ("id".equals(resultChild.getName())) {
  254.           flags.add(ResultFlag.ID);
  255.         }
  256.         resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
  257.       }
  258.     }
  259.     String id = resultMapNode.getStringAttribute("id",
  260.             resultMapNode.getValueBasedIdentifier());
  261.     String extend = resultMapNode.getStringAttribute("extends");
  262.     Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  263.     ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  264.     try {
  265.       return resultMapResolver.resolve();
  266.     } catch (IncompleteElementException e) {
  267.       configuration.addIncompleteResultMap(resultMapResolver);
  268.       throw e;
  269.     }
  270.   }

  271.   protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingType) {
  272.     if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
  273.       String property = resultMapNode.getStringAttribute("property");
  274.       if (property != null && enclosingType != null) {
  275.         MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
  276.         return metaResultType.getSetterType(property);
  277.       }
  278.     } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
  279.       return enclosingType;
  280.     }
  281.     return null;
  282.   }

  283.   private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
  284.     List<XNode> argChildren = resultChild.getChildren();
  285.     for (XNode argChild : argChildren) {
  286.       List<ResultFlag> flags = new ArrayList<>();
  287.       flags.add(ResultFlag.CONSTRUCTOR);
  288.       if ("idArg".equals(argChild.getName())) {
  289.         flags.add(ResultFlag.ID);
  290.       }
  291.       resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
  292.     }
  293.   }

  294.   private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
  295.     String column = context.getStringAttribute("column");
  296.     String javaType = context.getStringAttribute("javaType");
  297.     String jdbcType = context.getStringAttribute("jdbcType");
  298.     String typeHandler = context.getStringAttribute("typeHandler");
  299.     Class<?> javaTypeClass = resolveClass(javaType);
  300.     Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  301.     JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  302.     Map<String, String> discriminatorMap = new HashMap<>();
  303.     for (XNode caseChild : context.getChildren()) {
  304.       String value = caseChild.getStringAttribute("value");
  305.       String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType));
  306.       discriminatorMap.put(value, resultMap);
  307.     }
  308.     return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
  309.   }

  310.   private void sqlElement(List<XNode> list) {
  311.     if (configuration.getDatabaseId() != null) {
  312.       sqlElement(list, configuration.getDatabaseId());
  313.     }
  314.     sqlElement(list, null);
  315.   }

  316.   private void sqlElement(List<XNode> list, String requiredDatabaseId) {
  317.     for (XNode context : list) {
  318.       String databaseId = context.getStringAttribute("databaseId");
  319.       String id = context.getStringAttribute("id");
  320.       id = builderAssistant.applyCurrentNamespace(id, false);
  321.       if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
  322.         sqlFragments.put(id, context);
  323.       }
  324.     }
  325.   }

  326.   private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
  327.     if (requiredDatabaseId != null) {
  328.       return requiredDatabaseId.equals(databaseId);
  329.     }
  330.     if (databaseId != null) {
  331.       return false;
  332.     }
  333.     if (!this.sqlFragments.containsKey(id)) {
  334.       return true;
  335.     }
  336.     // skip this fragment if there is a previous one with a not null databaseId
  337.     XNode context = this.sqlFragments.get(id);
  338.     return context.getStringAttribute("databaseId") == null;
  339.   }

  340.   private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
  341.     String property;
  342.     if (flags.contains(ResultFlag.CONSTRUCTOR)) {
  343.       property = context.getStringAttribute("name");
  344.     } else {
  345.       property = context.getStringAttribute("property");
  346.     }
  347.     String column = context.getStringAttribute("column");
  348.     String javaType = context.getStringAttribute("javaType");
  349.     String jdbcType = context.getStringAttribute("jdbcType");
  350.     String nestedSelect = context.getStringAttribute("select");
  351.     String nestedResultMap = context.getStringAttribute("resultMap", () ->
  352.         processNestedResultMappings(context, Collections.emptyList(), resultType));
  353.     String notNullColumn = context.getStringAttribute("notNullColumn");
  354.     String columnPrefix = context.getStringAttribute("columnPrefix");
  355.     String typeHandler = context.getStringAttribute("typeHandler");
  356.     String resultSet = context.getStringAttribute("resultSet");
  357.     String foreignColumn = context.getStringAttribute("foreignColumn");
  358.     boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
  359.     Class<?> javaTypeClass = resolveClass(javaType);
  360.     Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  361.     JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  362.     return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  363.   }

  364.   private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
  365.     if (Arrays.asList("association", "collection", "case").contains(context.getName())
  366.         && context.getStringAttribute("select") == null) {
  367.       validateCollection(context, enclosingType);
  368.       ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
  369.       return resultMap.getId();
  370.     }
  371.     return null;
  372.   }

  373.   protected void validateCollection(XNode context, Class<?> enclosingType) {
  374.     if ("collection".equals(context.getName()) && context.getStringAttribute("resultMap") == null
  375.         && context.getStringAttribute("javaType") == null) {
  376.       MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
  377.       String property = context.getStringAttribute("property");
  378.       if (!metaResultType.hasSetter(property)) {
  379.         throw new BuilderException(
  380.             "Ambiguous collection type for property '" + property + "'. You must specify 'javaType' or 'resultMap'.");
  381.       }
  382.     }
  383.   }

  384.   private void bindMapperForNamespace() {
  385.     String namespace = builderAssistant.getCurrentNamespace();
  386.     if (namespace != null) {
  387.       Class<?> boundType = null;
  388.       try {
  389.         boundType = Resources.classForName(namespace);
  390.       } catch (ClassNotFoundException e) {
  391.         // ignore, bound type is not required
  392.       }
  393.       if (boundType != null && !configuration.hasMapper(boundType)) {
  394.         // Spring may not know the real resource name so we set a flag
  395.         // to prevent loading again this resource from the mapper interface
  396.         // look at MapperAnnotationBuilder#loadXmlResource
  397.         configuration.addLoadedResource("namespace:" + namespace);
  398.         configuration.addMapper(boundType);
  399.       }
  400.     }
  401.   }

  402. }