View Javadoc
1   /*
2    *    Copyright 2009-2024 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    *       https://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.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Properties;
27  
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  /**
51   * @author Clinton Begin
52   * @author Kazuki Shimizu
53   */
54  public class XMLMapperBuilder extends BaseBuilder {
55  
56    private final XPathParser parser;
57    private final MapperBuilderAssistant builderAssistant;
58    private final Map<String, XNode> sqlFragments;
59    private final String resource;
60  
61    @Deprecated
62    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments,
63        String namespace) {
64      this(reader, configuration, resource, sqlFragments);
65      this.builderAssistant.setCurrentNamespace(namespace);
66    }
67  
68    @Deprecated
69    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource,
70        Map<String, XNode> sqlFragments) {
71      this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration,
72          resource, sqlFragments);
73    }
74  
75    public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
76        Map<String, XNode> sqlFragments, String namespace) {
77      this(inputStream, configuration, resource, sqlFragments);
78      this.builderAssistant.setCurrentNamespace(namespace);
79    }
80  
81    public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
82        Map<String, XNode> sqlFragments) {
83      this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration,
84          resource, sqlFragments);
85    }
86  
87    private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource,
88        Map<String, XNode> sqlFragments) {
89      super(configuration);
90      this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
91      this.parser = parser;
92      this.sqlFragments = sqlFragments;
93      this.resource = resource;
94    }
95  
96    public void parse() {
97      if (!configuration.isResourceLoaded(resource)) {
98        configurationElement(parser.evalNode("/mapper"));
99        configuration.addLoadedResource(resource);
100       bindMapperForNamespace();
101     }
102     configuration.parsePendingResultMaps(false);
103     configuration.parsePendingCacheRefs(false);
104     configuration.parsePendingStatements(false);
105   }
106 
107   public XNode getSqlFragment(String refid) {
108     return sqlFragments.get(refid);
109   }
110 
111   private void configurationElement(XNode context) {
112     try {
113       String namespace = context.getStringAttribute("namespace");
114       if (namespace == null || namespace.isEmpty()) {
115         throw new BuilderException("Mapper's namespace cannot be empty");
116       }
117       builderAssistant.setCurrentNamespace(namespace);
118       cacheRefElement(context.evalNode("cache-ref"));
119       cacheElement(context.evalNode("cache"));
120       parameterMapElement(context.evalNodes("/mapper/parameterMap"));
121       resultMapElements(context.evalNodes("/mapper/resultMap"));
122       sqlElement(context.evalNodes("/mapper/sql"));
123       buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
124     } catch (Exception e) {
125       throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
126     }
127   }
128 
129   private void buildStatementFromContext(List<XNode> list) {
130     if (configuration.getDatabaseId() != null) {
131       buildStatementFromContext(list, configuration.getDatabaseId());
132     }
133     buildStatementFromContext(list, null);
134   }
135 
136   private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
137     for (XNode context : list) {
138       final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,
139           requiredDatabaseId);
140       try {
141         statementParser.parseStatementNode();
142       } catch (IncompleteElementException e) {
143         configuration.addIncompleteStatement(statementParser);
144       }
145     }
146   }
147 
148   private void cacheRefElement(XNode context) {
149     if (context != null) {
150       configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
151       CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant,
152           context.getStringAttribute("namespace"));
153       try {
154         cacheRefResolver.resolveCacheRef();
155       } catch (IncompleteElementException e) {
156         configuration.addIncompleteCacheRef(cacheRefResolver);
157       }
158     }
159   }
160 
161   private void cacheElement(XNode context) {
162     if (context != null) {
163       String type = context.getStringAttribute("type", "PERPETUAL");
164       Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
165       String eviction = context.getStringAttribute("eviction", "LRU");
166       Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
167       Long flushInterval = context.getLongAttribute("flushInterval");
168       Integer size = context.getIntAttribute("size");
169       boolean readWrite = !context.getBooleanAttribute("readOnly", false);
170       boolean blocking = context.getBooleanAttribute("blocking", false);
171       Properties props = context.getChildrenAsProperties();
172       builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
173     }
174   }
175 
176   private void parameterMapElement(List<XNode> list) {
177     for (XNode parameterMapNode : list) {
178       String id = parameterMapNode.getStringAttribute("id");
179       String type = parameterMapNode.getStringAttribute("type");
180       Class<?> parameterClass = resolveClass(type);
181       List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
182       List<ParameterMapping> parameterMappings = new ArrayList<>();
183       for (XNode parameterNode : parameterNodes) {
184         String property = parameterNode.getStringAttribute("property");
185         String javaType = parameterNode.getStringAttribute("javaType");
186         String jdbcType = parameterNode.getStringAttribute("jdbcType");
187         String resultMap = parameterNode.getStringAttribute("resultMap");
188         String mode = parameterNode.getStringAttribute("mode");
189         String typeHandler = parameterNode.getStringAttribute("typeHandler");
190         Integer numericScale = parameterNode.getIntAttribute("numericScale");
191         ParameterMode modeEnum = resolveParameterMode(mode);
192         Class<?> javaTypeClass = resolveClass(javaType);
193         JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
194         Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
195         ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property,
196             javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
197         parameterMappings.add(parameterMapping);
198       }
199       builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
200     }
201   }
202 
203   private void resultMapElements(List<XNode> list) {
204     for (XNode resultMapNode : list) {
205       try {
206         resultMapElement(resultMapNode);
207       } catch (IncompleteElementException e) {
208         // ignore, it will be retried
209       }
210     }
211   }
212 
213   private ResultMap resultMapElement(XNode resultMapNode) {
214     return resultMapElement(resultMapNode, Collections.emptyList(), null);
215   }
216 
217   private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings,
218       Class<?> enclosingType) {
219     ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
220     String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
221         resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
222     Class<?> typeClass = resolveClass(type);
223     if (typeClass == null) {
224       typeClass = inheritEnclosingType(resultMapNode, enclosingType);
225     }
226     Discriminator discriminator = null;
227     List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
228     List<XNode> resultChildren = resultMapNode.getChildren();
229     for (XNode resultChild : resultChildren) {
230       if ("constructor".equals(resultChild.getName())) {
231         processConstructorElement(resultChild, typeClass, resultMappings);
232       } else if ("discriminator".equals(resultChild.getName())) {
233         discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
234       } else {
235         List<ResultFlag> flags = new ArrayList<>();
236         if ("id".equals(resultChild.getName())) {
237           flags.add(ResultFlag.ID);
238         }
239         resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
240       }
241     }
242     String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
243     String extend = resultMapNode.getStringAttribute("extends");
244     Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
245     ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator,
246         resultMappings, autoMapping);
247     try {
248       return resultMapResolver.resolve();
249     } catch (IncompleteElementException e) {
250       configuration.addIncompleteResultMap(resultMapResolver);
251       throw e;
252     }
253   }
254 
255   protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingType) {
256     if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
257       String property = resultMapNode.getStringAttribute("property");
258       if (property != null && enclosingType != null) {
259         MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
260         return metaResultType.getSetterType(property);
261       }
262     } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
263       return enclosingType;
264     }
265     return null;
266   }
267 
268   private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
269     List<XNode> argChildren = resultChild.getChildren();
270     for (XNode argChild : argChildren) {
271       List<ResultFlag> flags = new ArrayList<>();
272       flags.add(ResultFlag.CONSTRUCTOR);
273       if ("idArg".equals(argChild.getName())) {
274         flags.add(ResultFlag.ID);
275       }
276       resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
277     }
278   }
279 
280   private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType,
281       List<ResultMapping> resultMappings) {
282     String column = context.getStringAttribute("column");
283     String javaType = context.getStringAttribute("javaType");
284     String jdbcType = context.getStringAttribute("jdbcType");
285     String typeHandler = context.getStringAttribute("typeHandler");
286     Class<?> javaTypeClass = resolveClass(javaType);
287     Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
288     JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
289     Map<String, String> discriminatorMap = new HashMap<>();
290     for (XNode caseChild : context.getChildren()) {
291       String value = caseChild.getStringAttribute("value");
292       String resultMap = caseChild.getStringAttribute("resultMap",
293           processNestedResultMappings(caseChild, resultMappings, resultType));
294       discriminatorMap.put(value, resultMap);
295     }
296     return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass,
297         discriminatorMap);
298   }
299 
300   private void sqlElement(List<XNode> list) {
301     if (configuration.getDatabaseId() != null) {
302       sqlElement(list, configuration.getDatabaseId());
303     }
304     sqlElement(list, null);
305   }
306 
307   private void sqlElement(List<XNode> list, String requiredDatabaseId) {
308     for (XNode context : list) {
309       String databaseId = context.getStringAttribute("databaseId");
310       String id = context.getStringAttribute("id");
311       id = builderAssistant.applyCurrentNamespace(id, false);
312       if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
313         sqlFragments.put(id, context);
314       }
315     }
316   }
317 
318   private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
319     if (requiredDatabaseId != null) {
320       return requiredDatabaseId.equals(databaseId);
321     }
322     if (databaseId != null) {
323       return false;
324     }
325     if (!this.sqlFragments.containsKey(id)) {
326       return true;
327     }
328     // skip this fragment if there is a previous one with a not null databaseId
329     XNode context = this.sqlFragments.get(id);
330     return context.getStringAttribute("databaseId") == null;
331   }
332 
333   private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
334     String property;
335     if (flags.contains(ResultFlag.CONSTRUCTOR)) {
336       property = context.getStringAttribute("name");
337     } else {
338       property = context.getStringAttribute("property");
339     }
340     String column = context.getStringAttribute("column");
341     String javaType = context.getStringAttribute("javaType");
342     String jdbcType = context.getStringAttribute("jdbcType");
343     String nestedSelect = context.getStringAttribute("select");
344     String nestedResultMap = context.getStringAttribute("resultMap",
345         () -> processNestedResultMappings(context, Collections.emptyList(), resultType));
346     String notNullColumn = context.getStringAttribute("notNullColumn");
347     String columnPrefix = context.getStringAttribute("columnPrefix");
348     String typeHandler = context.getStringAttribute("typeHandler");
349     String resultSet = context.getStringAttribute("resultSet");
350     String foreignColumn = context.getStringAttribute("foreignColumn");
351     boolean lazy = "lazy"
352         .equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
353     Class<?> javaTypeClass = resolveClass(javaType);
354     Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
355     JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
356     return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect,
357         nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
358   }
359 
360   private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings,
361       Class<?> enclosingType) {
362     if (Arrays.asList("association", "collection", "case").contains(context.getName())
363         && context.getStringAttribute("select") == null) {
364       validateCollection(context, enclosingType);
365       ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
366       return resultMap.getId();
367     }
368     return null;
369   }
370 
371   protected void validateCollection(XNode context, Class<?> enclosingType) {
372     if ("collection".equals(context.getName()) && context.getStringAttribute("resultMap") == null
373         && context.getStringAttribute("javaType") == null) {
374       MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
375       String property = context.getStringAttribute("property");
376       if (!metaResultType.hasSetter(property)) {
377         throw new BuilderException(
378             "Ambiguous collection type for property '" + property + "'. You must specify 'javaType' or 'resultMap'.");
379       }
380     }
381   }
382 
383   private void bindMapperForNamespace() {
384     String namespace = builderAssistant.getCurrentNamespace();
385     if (namespace != null) {
386       Class<?> boundType = null;
387       try {
388         boundType = Resources.classForName(namespace);
389       } catch (ClassNotFoundException e) {
390         // ignore, bound type is not required
391       }
392       if (boundType != null && !configuration.hasMapper(boundType)) {
393         // Spring may not know the real resource name so we set a flag
394         // to prevent loading again this resource from the mapper interface
395         // look at MapperAnnotationBuilder#loadXmlResource
396         configuration.addLoadedResource("namespace:" + namespace);
397         configuration.addMapper(boundType);
398       }
399     }
400   }
401 
402 }