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