View Javadoc
1   /*
2    * Copyright 2004-2026 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 com.ibatis.sqlmap.engine.builder.xml;
17  
18  import com.ibatis.common.resources.Resources;
19  import com.ibatis.common.xml.NodeletException;
20  import com.ibatis.common.xml.NodeletParser;
21  import com.ibatis.common.xml.NodeletUtils;
22  import com.ibatis.sqlmap.client.SqlMapException;
23  import com.ibatis.sqlmap.engine.cache.CacheController;
24  import com.ibatis.sqlmap.engine.config.CacheModelConfig;
25  import com.ibatis.sqlmap.engine.config.ParameterMapConfig;
26  import com.ibatis.sqlmap.engine.config.ResultMapConfig;
27  import com.ibatis.sqlmap.engine.mapping.statement.DeleteStatement;
28  import com.ibatis.sqlmap.engine.mapping.statement.InsertStatement;
29  import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement;
30  import com.ibatis.sqlmap.engine.mapping.statement.ProcedureStatement;
31  import com.ibatis.sqlmap.engine.mapping.statement.SelectStatement;
32  import com.ibatis.sqlmap.engine.mapping.statement.UpdateStatement;
33  
34  import java.io.InputStream;
35  import java.io.Reader;
36  import java.util.Properties;
37  
38  /**
39   * The Class SqlMapParser.
40   */
41  public class SqlMapParser {
42  
43    /** The parser. */
44    private final NodeletParser parser;
45  
46    /** The state. */
47    private XmlParserState state;
48  
49    /** The statement parser. */
50    private SqlStatementParser statementParser;
51  
52    /**
53     * Instantiates a new sql map parser.
54     *
55     * @param state
56     *          the state
57     */
58    public SqlMapParser(XmlParserState state) {
59      this.parser = new NodeletParser();
60      this.state = state;
61  
62      parser.setValidation(true);
63      parser.setEntityResolver(new SqlMapClasspathEntityResolver());
64  
65      statementParser = new SqlStatementParser(this.state);
66  
67      addSqlMapNodelets();
68      addSqlNodelets();
69      addTypeAliasNodelets();
70      addCacheModelNodelets();
71      addParameterMapNodelets();
72      addResultMapNodelets();
73      addStatementNodelets();
74  
75    }
76  
77    /**
78     * Parses the.
79     *
80     * @param reader
81     *          the reader
82     *
83     * @throws NodeletException
84     *           the nodelet exception
85     */
86    public void parse(Reader reader) throws NodeletException {
87      parser.parse(reader);
88    }
89  
90    /**
91     * Parses the.
92     *
93     * @param inputStream
94     *          the input stream
95     *
96     * @throws NodeletException
97     *           the nodelet exception
98     */
99    public void parse(InputStream inputStream) throws NodeletException {
100     parser.parse(inputStream);
101   }
102 
103   /**
104    * Adds the sql map nodelets.
105    */
106   private void addSqlMapNodelets() {
107     parser.addNodelet("/sqlMap", node -> {
108       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
109       state.setNamespace(attributes.getProperty("namespace"));
110     });
111   }
112 
113   /**
114    * Adds the sql nodelets.
115    */
116   private void addSqlNodelets() {
117     parser.addNodelet("/sqlMap/sql", node -> {
118       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
119       String id = attributes.getProperty("id");
120       if (state.isUseStatementNamespaces()) {
121         id = state.applyNamespace(id);
122       }
123       if (state.getSqlIncludes().containsKey(id)) {
124         throw new SqlMapException("Duplicate <sql>-include '" + id + "' found.");
125       }
126       state.getSqlIncludes().put(id, node);
127     });
128   }
129 
130   /**
131    * Adds the type alias nodelets.
132    */
133   private void addTypeAliasNodelets() {
134     parser.addNodelet("/sqlMap/typeAlias", node -> {
135       Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps());
136       String alias = prop.getProperty("alias");
137       String type = prop.getProperty("type");
138       state.getConfig().getTypeHandlerFactory().putTypeAlias(alias, type);
139     });
140   }
141 
142   /**
143    * Adds the cache model nodelets.
144    */
145   private void addCacheModelNodelets() {
146     parser.addNodelet("/sqlMap/cacheModel", node -> {
147       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
148       String id = state.applyNamespace(attributes.getProperty("id"));
149       String type = attributes.getProperty("type");
150       String readOnlyAttr = attributes.getProperty("readOnly");
151       Boolean readOnly = readOnlyAttr == null || readOnlyAttr.isEmpty() ? null
152           : Boolean.valueOf("true".equals(readOnlyAttr));
153       String serializeAttr = attributes.getProperty("serialize");
154       Boolean serialize = serializeAttr == null || serializeAttr.isEmpty() ? null
155           : Boolean.valueOf("true".equals(serializeAttr));
156       type = state.getConfig().getTypeHandlerFactory().resolveAlias(type);
157       Class clazz = Resources.classForName(type);
158       if (readOnly == null) {
159         readOnly = Boolean.TRUE;
160       }
161       if (serialize == null) {
162         serialize = Boolean.FALSE;
163       }
164       CacheModelConfig cacheConfig = state.getConfig().newCacheModelConfig(id,
165           (CacheController) Resources.instantiate(clazz), readOnly.booleanValue(), serialize.booleanValue());
166       state.setCacheConfig(cacheConfig);
167     });
168     parser.addNodelet("/sqlMap/cacheModel/end()",
169         node -> state.getCacheConfig().setControllerProperties(state.getCacheProps()));
170     parser.addNodelet("/sqlMap/cacheModel/property", node -> {
171       state.getConfig().getErrorContext().setMoreInfo("Check the cache model properties.");
172       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
173       String name = attributes.getProperty("name");
174       String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
175       state.getCacheProps().setProperty(name, value);
176     });
177     parser.addNodelet("/sqlMap/cacheModel/flushOnExecute", node -> {
178       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
179       String statement = childAttributes.getProperty("statement");
180       state.getCacheConfig().addFlushTriggerStatement(statement);
181     });
182     parser.addNodelet("/sqlMap/cacheModel/flushInterval", node -> {
183       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
184       try {
185         int milliseconds = childAttributes.getProperty("milliseconds") == null ? 0
186             : Integer.parseInt(childAttributes.getProperty("milliseconds"));
187         int seconds = childAttributes.getProperty("seconds") == null ? 0
188             : Integer.parseInt(childAttributes.getProperty("seconds"));
189         int minutes = childAttributes.getProperty("minutes") == null ? 0
190             : Integer.parseInt(childAttributes.getProperty("minutes"));
191         int hours = childAttributes.getProperty("hours") == null ? 0
192             : Integer.parseInt(childAttributes.getProperty("hours"));
193         state.getCacheConfig().setFlushInterval(hours, minutes, seconds, milliseconds);
194       } catch (NumberFormatException e) {
195         throw new RuntimeException("Error building cache in '" + "resourceNAME"
196             + "'.  Flush interval milliseconds must be a valid long integer value.  Cause: " + e, e);
197       }
198     });
199   }
200 
201   /**
202    * Adds the parameter map nodelets.
203    */
204   private void addParameterMapNodelets() {
205     parser.addNodelet("/sqlMap/parameterMap/end()", node -> {
206       state.getConfig().getErrorContext().setMoreInfo(null);
207       state.getConfig().getErrorContext().setObjectId(null);
208       state.setParamConfig(null);
209     });
210     parser.addNodelet("/sqlMap/parameterMap", node -> {
211       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
212       String id = state.applyNamespace(attributes.getProperty("id"));
213       String parameterClassName = attributes.getProperty("class");
214       parameterClassName = state.getConfig().getTypeHandlerFactory().resolveAlias(parameterClassName);
215       try {
216         state.getConfig().getErrorContext().setMoreInfo("Check the parameter class.");
217         ParameterMapConfig paramConf = state.getConfig().newParameterMapConfig(id,
218             Resources.classForName(parameterClassName));
219         state.setParamConfig(paramConf);
220       } catch (Exception e) {
221         throw new SqlMapException("Error configuring ParameterMap.  Could not set ParameterClass.  Cause: " + e, e);
222       }
223     });
224     parser.addNodelet("/sqlMap/parameterMap/parameter", node -> {
225       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
226       String propertyName = childAttributes.getProperty("property");
227       String jdbcType = childAttributes.getProperty("jdbcType");
228       String type = childAttributes.getProperty("typeName");
229       String javaType = childAttributes.getProperty("javaType");
230       String resultMap = state.applyNamespace(childAttributes.getProperty("resultMap"));
231       String nullValue = childAttributes.getProperty("nullValue");
232       String mode = childAttributes.getProperty("mode");
233       String callback = childAttributes.getProperty("typeHandler");
234       String numericScaleProp = childAttributes.getProperty("numericScale");
235 
236       callback = state.getConfig().getTypeHandlerFactory().resolveAlias(callback);
237       Object typeHandlerImpl = null;
238       if (callback != null) {
239         typeHandlerImpl = Resources.instantiate(callback);
240       }
241 
242       javaType = state.getConfig().getTypeHandlerFactory().resolveAlias(javaType);
243       Class javaClass = null;
244       try {
245         if (javaType != null && !javaType.isEmpty()) {
246           javaClass = Resources.classForName(javaType);
247         }
248       } catch (ClassNotFoundException e) {
249         throw new RuntimeException("Error setting javaType on parameter mapping.  Cause: " + e);
250       }
251 
252       Integer numericScale = null;
253       if (numericScaleProp != null) {
254         numericScale = Integer.valueOf(numericScaleProp);
255       }
256 
257       state.getParamConfig().addParameterMapping(propertyName, javaClass, jdbcType, nullValue, mode, type, numericScale,
258           typeHandlerImpl, resultMap);
259     });
260   }
261 
262   /**
263    * Adds the result map nodelets.
264    */
265   private void addResultMapNodelets() {
266     parser.addNodelet("/sqlMap/resultMap/end()", node -> {
267       state.getConfig().getErrorContext().setMoreInfo(null);
268       state.getConfig().getErrorContext().setObjectId(null);
269     });
270     parser.addNodelet("/sqlMap/resultMap", node -> {
271       Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
272       String id = state.applyNamespace(attributes.getProperty("id"));
273       String resultClassName = attributes.getProperty("class");
274       String extended = state.applyNamespace(attributes.getProperty("extends"));
275       String xmlName = attributes.getProperty("xmlName");
276       String groupBy = attributes.getProperty("groupBy");
277 
278       resultClassName = state.getConfig().getTypeHandlerFactory().resolveAlias(resultClassName);
279       Class resultClass;
280       try {
281         state.getConfig().getErrorContext().setMoreInfo("Check the result class.");
282         resultClass = Resources.classForName(resultClassName);
283       } catch (Exception e) {
284         throw new RuntimeException("Error configuring Result.  Could not set ResultClass.  Cause: " + e, e);
285       }
286       ResultMapConfig resultConf = state.getConfig().newResultMapConfig(id, resultClass, groupBy, extended, xmlName);
287       state.setResultConfig(resultConf);
288     });
289     parser.addNodelet("/sqlMap/resultMap/result", node -> {
290       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
291       String propertyName = childAttributes.getProperty("property");
292       String nullValue = childAttributes.getProperty("nullValue");
293       String jdbcType = childAttributes.getProperty("jdbcType");
294       String javaType = childAttributes.getProperty("javaType");
295       String columnName = childAttributes.getProperty("column");
296       String columnIndexProp = childAttributes.getProperty("columnIndex");
297       String statementName = childAttributes.getProperty("select");
298       String resultMapName = childAttributes.getProperty("resultMap");
299       String callback = childAttributes.getProperty("typeHandler");
300       String notNullColumn = childAttributes.getProperty("notNullColumn");
301 
302       state.getConfig().getErrorContext().setMoreInfo("Check the result mapping property type or name.");
303       Class javaClass = null;
304       try {
305         javaType = state.getConfig().getTypeHandlerFactory().resolveAlias(javaType);
306         if (javaType != null && !javaType.isEmpty()) {
307           javaClass = Resources.classForName(javaType);
308         }
309       } catch (ClassNotFoundException e) {
310         throw new RuntimeException("Error setting java type on result discriminator mapping.  Cause: " + e);
311       }
312 
313       state.getConfig().getErrorContext().setMoreInfo("Check the result mapping typeHandler attribute '" + callback
314           + "' (must be a TypeHandler or TypeHandlerCallback implementation).");
315       Object typeHandlerImpl = null;
316       try {
317         if (callback != null && !callback.isEmpty()) {
318           callback = state.getConfig().getTypeHandlerFactory().resolveAlias(callback);
319           typeHandlerImpl = Resources.instantiate(callback);
320         }
321       } catch (Exception e) {
322         throw new RuntimeException("Error occurred during custom type handler configuration.  Cause: " + e, e);
323       }
324 
325       Integer columnIndex = null;
326       if (columnIndexProp != null) {
327         try {
328           columnIndex = Integer.valueOf(columnIndexProp);
329         } catch (Exception e) {
330           throw new RuntimeException("Error parsing column index.  Cause: " + e, e);
331         }
332       }
333 
334       state.getResultConfig().addResultMapping(propertyName, columnName, columnIndex, javaClass, jdbcType, nullValue,
335           notNullColumn, statementName, resultMapName, typeHandlerImpl);
336     });
337 
338     parser.addNodelet("/sqlMap/resultMap/discriminator/subMap", node -> {
339       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
340       String value = childAttributes.getProperty("value");
341       String resultMap = childAttributes.getProperty("resultMap");
342       resultMap = state.applyNamespace(resultMap);
343       state.getResultConfig().addDiscriminatorSubMap(value, resultMap);
344     });
345 
346     parser.addNodelet("/sqlMap/resultMap/discriminator", node -> {
347       Properties childAttributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
348       String nullValue = childAttributes.getProperty("nullValue");
349       String jdbcType = childAttributes.getProperty("jdbcType");
350       String javaType = childAttributes.getProperty("javaType");
351       String columnName = childAttributes.getProperty("column");
352       String columnIndexProp = childAttributes.getProperty("columnIndex");
353       String callback = childAttributes.getProperty("typeHandler");
354 
355       state.getConfig().getErrorContext().setMoreInfo("Check the disriminator type or name.");
356       Class javaClass = null;
357       try {
358         javaType = state.getConfig().getTypeHandlerFactory().resolveAlias(javaType);
359         if (javaType != null && !javaType.isEmpty()) {
360           javaClass = Resources.classForName(javaType);
361         }
362       } catch (ClassNotFoundException e) {
363         throw new RuntimeException("Error setting java type on result discriminator mapping.  Cause: " + e);
364       }
365 
366       state.getConfig().getErrorContext().setMoreInfo("Check the result mapping discriminator typeHandler attribute '"
367           + callback + "' (must be a TypeHandlerCallback implementation).");
368       Object typeHandlerImpl = null;
369       try {
370         if (callback != null && !callback.isEmpty()) {
371           callback = state.getConfig().getTypeHandlerFactory().resolveAlias(callback);
372           typeHandlerImpl = Resources.instantiate(callback);
373         }
374       } catch (Exception e) {
375         throw new RuntimeException("Error occurred during custom type handler configuration.  Cause: " + e, e);
376       }
377 
378       Integer columnIndex = null;
379       if (columnIndexProp != null) {
380         try {
381           columnIndex = Integer.valueOf(columnIndexProp);
382         } catch (Exception e) {
383           throw new RuntimeException("Error parsing column index.  Cause: " + e, e);
384         }
385       }
386 
387       state.getResultConfig().setDiscriminator(columnName, columnIndex, javaClass, jdbcType, nullValue,
388           typeHandlerImpl);
389     });
390   }
391 
392   /**
393    * Adds the statement nodelets.
394    */
395   protected void addStatementNodelets() {
396     parser.addNodelet("/sqlMap/statement", node -> statementParser.parseGeneralStatement(node, new MappedStatement()));
397     parser.addNodelet("/sqlMap/insert", node -> statementParser.parseGeneralStatement(node, new InsertStatement()));
398     parser.addNodelet("/sqlMap/update", node -> statementParser.parseGeneralStatement(node, new UpdateStatement()));
399     parser.addNodelet("/sqlMap/delete", node -> statementParser.parseGeneralStatement(node, new DeleteStatement()));
400     parser.addNodelet("/sqlMap/select", node -> statementParser.parseGeneralStatement(node, new SelectStatement()));
401     parser.addNodelet("/sqlMap/procedure",
402         node -> statementParser.parseGeneralStatement(node, new ProcedureStatement()));
403   }
404 
405 }