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