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