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.mapping.statement;
17  
18  import com.ibatis.common.io.ReaderInputStream;
19  import com.ibatis.common.jdbc.exception.NestedSQLException;
20  import com.ibatis.sqlmap.client.SqlMapClient;
21  import com.ibatis.sqlmap.client.event.RowHandler;
22  import com.ibatis.sqlmap.engine.cache.CacheKey;
23  import com.ibatis.sqlmap.engine.execution.SqlExecutor;
24  import com.ibatis.sqlmap.engine.impl.SqlMapClientImpl;
25  import com.ibatis.sqlmap.engine.mapping.parameter.ParameterMap;
26  import com.ibatis.sqlmap.engine.mapping.result.ResultMap;
27  import com.ibatis.sqlmap.engine.mapping.sql.Sql;
28  import com.ibatis.sqlmap.engine.scope.ErrorContext;
29  import com.ibatis.sqlmap.engine.scope.StatementScope;
30  import com.ibatis.sqlmap.engine.transaction.Transaction;
31  import com.ibatis.sqlmap.engine.transaction.TransactionException;
32  import com.ibatis.sqlmap.engine.type.DomTypeMarker;
33  import com.ibatis.sqlmap.engine.type.XmlTypeMarker;
34  
35  import java.io.StringReader;
36  import java.sql.Connection;
37  import java.sql.SQLException;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.List;
41  
42  import javax.xml.XMLConstants;
43  import javax.xml.parsers.DocumentBuilder;
44  import javax.xml.parsers.DocumentBuilderFactory;
45  
46  import org.w3c.dom.Document;
47  
48  /**
49   * The Class MappedStatement.
50   */
51  public class MappedStatement {
52  
53    /** The id. */
54    private String id;
55  
56    /** The result set type. */
57    private Integer resultSetType;
58  
59    /** The fetch size. */
60    private Integer fetchSize;
61  
62    /** The result map. */
63    private ResultMap resultMap;
64  
65    /** The parameter map. */
66    private ParameterMap parameterMap;
67  
68    /** The parameter class. */
69    private Class parameterClass;
70  
71    /** The sql. */
72    private Sql sql;
73  
74    /** The base cache key. */
75    private int baseCacheKey;
76  
77    /** The sql map client. */
78    private SqlMapClientImpl sqlMapClient;
79  
80    /** The timeout. */
81    private Integer timeout;
82  
83    /** The additional result maps. */
84    private ResultMap[] additionalResultMaps = {};
85  
86    /** The execute listeners. */
87    private List executeListeners = new ArrayList<>();
88  
89    /** The resource. */
90    private String resource;
91  
92    /**
93     * Gets the statement type.
94     *
95     * @return the statement type
96     */
97    public StatementType getStatementType() {
98      return StatementType.UNKNOWN;
99    }
100 
101   /**
102    * Execute update.
103    *
104    * @param statementScope
105    *          the statement scope
106    * @param trans
107    *          the trans
108    * @param parameterObject
109    *          the parameter object
110    *
111    * @return the int
112    *
113    * @throws SQLException
114    *           the SQL exception
115    */
116   public int executeUpdate(StatementScope statementScope, Transaction trans, Object parameterObject)
117       throws SQLException {
118     ErrorContext errorContext = statementScope.getErrorContext();
119     errorContext.setActivity("preparing the mapped statement for execution");
120     errorContext.setObjectId(this.getId());
121     errorContext.setResource(this.getResource());
122 
123     statementScope.getSession().setCommitRequired(true);
124 
125     try {
126       parameterObject = validateParameter(parameterObject);
127 
128       Sql sql = getSql();
129 
130       errorContext.setMoreInfo("Check the parameter map.");
131       ParameterMap parameterMap = sql.getParameterMap(statementScope, parameterObject);
132 
133       errorContext.setMoreInfo("Check the result map.");
134       ResultMap resultMap = sql.getResultMap(statementScope, parameterObject);
135 
136       statementScope.setResultMap(resultMap);
137       statementScope.setParameterMap(parameterMap);
138 
139       errorContext.setMoreInfo("Check the parameter map.");
140       Object[] parameters = parameterMap.getParameterObjectValues(statementScope, parameterObject);
141 
142       errorContext.setMoreInfo("Check the SQL statement.");
143       String sqlString = sql.getSql(statementScope, parameterObject);
144 
145       errorContext.setActivity("executing mapped statement");
146       errorContext.setMoreInfo("Check the statement or the result map.");
147       int rows = sqlExecuteUpdate(statementScope, trans.getConnection(), sqlString, parameters);
148 
149       errorContext.setMoreInfo("Check the output parameters.");
150       if (parameterObject != null) {
151         postProcessParameterObject(statementScope, parameterObject, parameters);
152       }
153 
154       errorContext.reset();
155       sql.cleanup(statementScope);
156       notifyListeners();
157       return rows;
158     } catch (SQLException e) {
159       errorContext.setCause(e);
160       throw new NestedSQLException(errorContext.toString(), e.getSQLState(), e.getErrorCode(), e);
161     } catch (Exception e) {
162       errorContext.setCause(e);
163       throw new NestedSQLException(errorContext.toString(), e);
164     }
165   }
166 
167   /**
168    * Execute query for object.
169    *
170    * @param statementScope
171    *          the statement scope
172    * @param trans
173    *          the trans
174    * @param parameterObject
175    *          the parameter object
176    * @param resultObject
177    *          the result object
178    *
179    * @return the object
180    *
181    * @throws SQLException
182    *           the SQL exception
183    */
184   public Object executeQueryForObject(StatementScope statementScope, Transaction trans, Object parameterObject,
185       Object resultObject) throws SQLException {
186     try {
187       Object object = null;
188 
189       DefaultRowHandler rowHandler = new DefaultRowHandler();
190       executeQueryWithCallback(statementScope, trans.getConnection(), parameterObject, resultObject, rowHandler,
191           SqlExecutor.NO_SKIPPED_RESULTS, SqlExecutor.NO_MAXIMUM_RESULTS);
192       List list = rowHandler.getList();
193 
194       if (list.size() > 1) {
195         throw new SQLException("Error: executeQueryForObject returned too many results.");
196       }
197       if (!list.isEmpty()) {
198         object = list.get(0);
199       }
200 
201       return object;
202     } catch (TransactionException e) {
203       throw new NestedSQLException("Error getting Connection from Transaction.  Cause: " + e, e);
204     }
205   }
206 
207   /**
208    * Execute query for list.
209    *
210    * @param statementScope
211    *          the statement scope
212    * @param trans
213    *          the trans
214    * @param parameterObject
215    *          the parameter object
216    * @param skipResults
217    *          the skip results
218    * @param maxResults
219    *          the max results
220    *
221    * @return the list
222    *
223    * @throws SQLException
224    *           the SQL exception
225    */
226   public List executeQueryForList(StatementScope statementScope, Transaction trans, Object parameterObject,
227       int skipResults, int maxResults) throws SQLException {
228     try {
229       DefaultRowHandler rowHandler = new DefaultRowHandler();
230       executeQueryWithCallback(statementScope, trans.getConnection(), parameterObject, null, rowHandler, skipResults,
231           maxResults);
232       return rowHandler.getList();
233     } catch (TransactionException e) {
234       throw new NestedSQLException("Error getting Connection from Transaction.  Cause: " + e, e);
235     }
236   }
237 
238   /**
239    * Execute query with row handler.
240    *
241    * @param statementScope
242    *          the statement scope
243    * @param trans
244    *          the trans
245    * @param parameterObject
246    *          the parameter object
247    * @param rowHandler
248    *          the row handler
249    *
250    * @throws SQLException
251    *           the SQL exception
252    */
253   public void executeQueryWithRowHandler(StatementScope statementScope, Transaction trans, Object parameterObject,
254       RowHandler rowHandler) throws SQLException {
255     try {
256       executeQueryWithCallback(statementScope, trans.getConnection(), parameterObject, null, rowHandler,
257           SqlExecutor.NO_SKIPPED_RESULTS, SqlExecutor.NO_MAXIMUM_RESULTS);
258     } catch (TransactionException e) {
259       throw new NestedSQLException("Error getting Connection from Transaction.  Cause: " + e, e);
260     }
261   }
262 
263   //
264   // PROTECTED METHODS
265   //
266 
267   /**
268    * Execute query with callback.
269    *
270    * @param statementScope
271    *          the statement scope
272    * @param conn
273    *          the conn
274    * @param parameterObject
275    *          the parameter object
276    * @param resultObject
277    *          the result object
278    * @param rowHandler
279    *          the row handler
280    * @param skipResults
281    *          the skip results
282    * @param maxResults
283    *          the max results
284    *
285    * @throws SQLException
286    *           the SQL exception
287    */
288   protected void executeQueryWithCallback(StatementScope statementScope, Connection conn, Object parameterObject,
289       Object resultObject, RowHandler rowHandler, int skipResults, int maxResults) throws SQLException {
290     ErrorContext errorContext = statementScope.getErrorContext();
291     errorContext.setActivity("preparing the mapped statement for execution");
292     errorContext.setObjectId(this.getId());
293     errorContext.setResource(this.getResource());
294 
295     try {
296       parameterObject = validateParameter(parameterObject);
297 
298       Sql sql = getSql();
299 
300       errorContext.setMoreInfo("Check the parameter map.");
301       ParameterMap parameterMap = sql.getParameterMap(statementScope, parameterObject);
302 
303       errorContext.setMoreInfo("Check the result map.");
304       ResultMap resultMap = sql.getResultMap(statementScope, parameterObject);
305 
306       statementScope.setResultMap(resultMap);
307       statementScope.setParameterMap(parameterMap);
308 
309       errorContext.setMoreInfo("Check the parameter map.");
310       Object[] parameters = parameterMap.getParameterObjectValues(statementScope, parameterObject);
311 
312       errorContext.setMoreInfo("Check the SQL statement.");
313       String sqlString = sql.getSql(statementScope, parameterObject);
314 
315       errorContext.setActivity("executing mapped statement");
316       errorContext.setMoreInfo("Check the SQL statement or the result map.");
317       RowHandlerCallback callback = new RowHandlerCallback(resultMap, resultObject, rowHandler);
318       sqlExecuteQuery(statementScope, conn, sqlString, parameters, skipResults, maxResults, callback);
319 
320       errorContext.setMoreInfo("Check the output parameters.");
321       if (parameterObject != null) {
322         postProcessParameterObject(statementScope, parameterObject, parameters);
323       }
324 
325       errorContext.reset();
326       sql.cleanup(statementScope);
327       notifyListeners();
328     } catch (SQLException e) {
329       errorContext.setCause(e);
330       throw new NestedSQLException(errorContext.toString(), e.getSQLState(), e.getErrorCode(), e);
331     } catch (Exception e) {
332       errorContext.setCause(e);
333       throw new NestedSQLException(errorContext.toString(), e);
334     }
335   }
336 
337   /**
338    * Post process parameter object.
339    *
340    * @param statementScope
341    *          the statement scope
342    * @param parameterObject
343    *          the parameter object
344    * @param parameters
345    *          the parameters
346    */
347   protected void postProcessParameterObject(StatementScope statementScope, Object parameterObject,
348       Object[] parameters) {
349   }
350 
351   /**
352    * Sql execute update.
353    *
354    * @param statementScope
355    *          the statement scope
356    * @param conn
357    *          the conn
358    * @param sqlString
359    *          the sql string
360    * @param parameters
361    *          the parameters
362    *
363    * @return the int
364    *
365    * @throws SQLException
366    *           the SQL exception
367    */
368   protected int sqlExecuteUpdate(StatementScope statementScope, Connection conn, String sqlString, Object[] parameters)
369       throws SQLException {
370     if (statementScope.getSession().isInBatch()) {
371       getSqlExecutor().addBatch(statementScope, conn, sqlString, parameters);
372       return 0;
373     }
374     return getSqlExecutor().executeUpdate(statementScope, conn, sqlString, parameters);
375   }
376 
377   /**
378    * Sql execute query.
379    *
380    * @param statementScope
381    *          the statement scope
382    * @param conn
383    *          the conn
384    * @param sqlString
385    *          the sql string
386    * @param parameters
387    *          the parameters
388    * @param skipResults
389    *          the skip results
390    * @param maxResults
391    *          the max results
392    * @param callback
393    *          the callback
394    *
395    * @throws SQLException
396    *           the SQL exception
397    */
398   protected void sqlExecuteQuery(StatementScope statementScope, Connection conn, String sqlString, Object[] parameters,
399       int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException {
400     getSqlExecutor().executeQuery(statementScope, conn, sqlString, parameters, skipResults, maxResults, callback);
401   }
402 
403   /**
404    * Validate parameter.
405    *
406    * @param param
407    *          the param
408    *
409    * @return the object
410    *
411    * @throws SQLException
412    *           the SQL exception
413    */
414   protected Object validateParameter(Object param) throws SQLException {
415     Object newParam = param;
416     Class parameterClass = getParameterClass();
417     if (newParam != null && parameterClass != null) {
418       if (DomTypeMarker.class.isAssignableFrom(parameterClass)) {
419         if (XmlTypeMarker.class.isAssignableFrom(parameterClass)) {
420           if (!(newParam instanceof String) && !(newParam instanceof Document)) {
421             throw new SQLException("Invalid parameter object type.  Expected '" + String.class.getName() + "' or '"
422                 + Document.class.getName() + "' but found '" + newParam.getClass().getName() + "'.");
423           }
424           if (!(newParam instanceof Document)) {
425             newParam = stringToDocument((String) newParam);
426           }
427         } else if (!Document.class.isAssignableFrom(newParam.getClass())) {
428           throw new SQLException("Invalid parameter object type.  Expected '" + Document.class.getName()
429               + "' but found '" + newParam.getClass().getName() + "'.");
430         }
431       } else if (!parameterClass.isAssignableFrom(newParam.getClass())) {
432         throw new SQLException("Invalid parameter object type.  Expected '" + parameterClass.getName() + "' but found '"
433             + newParam.getClass().getName() + "'.");
434       }
435     }
436     return newParam;
437   }
438 
439   /**
440    * String to document.
441    *
442    * @param s
443    *          the s
444    *
445    * @return the document
446    */
447   private Document stringToDocument(String s) {
448     try {
449       DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
450       documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
451       documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
452       documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
453       DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
454       return documentBuilder.parse(new ReaderInputStream(new StringReader(s)));
455     } catch (Exception e) {
456       throw new RuntimeException("Error occurred.  Cause: " + e, e);
457     }
458   }
459 
460   /**
461    * Gets the id.
462    *
463    * @return the id
464    */
465   public String getId() {
466     return id;
467   }
468 
469   /**
470    * Gets the result set type.
471    *
472    * @return the result set type
473    */
474   public Integer getResultSetType() {
475     return resultSetType;
476   }
477 
478   /**
479    * Sets the result set type.
480    *
481    * @param resultSetType
482    *          the new result set type
483    */
484   public void setResultSetType(Integer resultSetType) {
485     this.resultSetType = resultSetType;
486   }
487 
488   /**
489    * Gets the fetch size.
490    *
491    * @return the fetch size
492    */
493   public Integer getFetchSize() {
494     return fetchSize;
495   }
496 
497   /**
498    * Sets the fetch size.
499    *
500    * @param fetchSize
501    *          the new fetch size
502    */
503   public void setFetchSize(Integer fetchSize) {
504     this.fetchSize = fetchSize;
505   }
506 
507   /**
508    * Sets the id.
509    *
510    * @param id
511    *          the new id
512    */
513   public void setId(String id) {
514     this.id = id;
515   }
516 
517   /**
518    * Gets the sql.
519    *
520    * @return the sql
521    */
522   public Sql getSql() {
523     return sql;
524   }
525 
526   /**
527    * Sets the sql.
528    *
529    * @param sql
530    *          the new sql
531    */
532   public void setSql(Sql sql) {
533     this.sql = sql;
534   }
535 
536   /**
537    * Gets the result map.
538    *
539    * @return the result map
540    */
541   public ResultMap getResultMap() {
542     return resultMap;
543   }
544 
545   /**
546    * Sets the result map.
547    *
548    * @param resultMap
549    *          the new result map
550    */
551   public void setResultMap(ResultMap resultMap) {
552     this.resultMap = resultMap;
553   }
554 
555   /**
556    * Gets the parameter map.
557    *
558    * @return the parameter map
559    */
560   public ParameterMap getParameterMap() {
561     return parameterMap;
562   }
563 
564   /**
565    * Sets the parameter map.
566    *
567    * @param parameterMap
568    *          the new parameter map
569    */
570   public void setParameterMap(ParameterMap parameterMap) {
571     this.parameterMap = parameterMap;
572   }
573 
574   /**
575    * Gets the parameter class.
576    *
577    * @return the parameter class
578    */
579   public Class getParameterClass() {
580     return parameterClass;
581   }
582 
583   /**
584    * Sets the parameter class.
585    *
586    * @param parameterClass
587    *          the new parameter class
588    */
589   public void setParameterClass(Class parameterClass) {
590     this.parameterClass = parameterClass;
591   }
592 
593   /**
594    * Gets the resource.
595    *
596    * @return the resource
597    */
598   public String getResource() {
599     return resource;
600   }
601 
602   /**
603    * Sets the resource.
604    *
605    * @param resource
606    *          the new resource
607    */
608   public void setResource(String resource) {
609     this.resource = resource;
610   }
611 
612   /**
613    * Gets the cache key.
614    *
615    * @param statementScope
616    *          the statement scope
617    * @param parameterObject
618    *          the parameter object
619    *
620    * @return the cache key
621    */
622   public CacheKey getCacheKey(StatementScope statementScope, Object parameterObject) {
623     Sql sql = statementScope.getSql();
624     ParameterMap pmap = sql.getParameterMap(statementScope, parameterObject);
625     CacheKey cacheKey = pmap.getCacheKey(statementScope, parameterObject);
626     cacheKey.update(id);
627 
628     // I am not sure how any clustered cache solution would ever have had any cache hits against
629     // replicated objects. I could not make it happen
630     // The baseCacheKey value which was being used in the update below is consistent across
631     // JVMInstances on the same machine
632     // but it's not consistent across machines, and therefore breaks clustered caching.
633 
634     // What would happen is the cache values were being replicated across machines but there
635     // were never any cache hits for cached objects on
636     // anything but the original machine an object was created on.
637 
638     // After reviewing this implementation I could not figure out why baseCacheKey is used for
639     // this anyway as it's not needed, so I removed it.
640     // The values used from the pmap.getCacheKey, plus id, plus the params below are unique and
641     // the same across machines, so now I get replicated
642     // cache hits when I force failover in my cluster
643 
644     // I wish I could make a unit test for this, but I can't do it as the old implementation
645     // works on 1 machine, but fails across machines.
646     // cacheKey.update(baseCacheKey);
647 
648     cacheKey.update(sql.getSql(statementScope, parameterObject)); // Fixes bug 953001
649     return cacheKey;
650   }
651 
652   /**
653    * Sets the base cache key.
654    *
655    * @param base
656    *          the new base cache key
657    */
658   public void setBaseCacheKey(int base) {
659     this.baseCacheKey = base;
660   }
661 
662   /**
663    * Adds the execute listener.
664    *
665    * @param listener
666    *          the listener
667    */
668   public void addExecuteListener(ExecuteListener listener) {
669     executeListeners.add(listener);
670   }
671 
672   /**
673    * Notify listeners.
674    */
675   public void notifyListeners() {
676     for (Object executeListener : executeListeners) {
677       ((ExecuteListener) executeListener).onExecuteStatement(this);
678     }
679   }
680 
681   /**
682    * Gets the sql executor.
683    *
684    * @return the sql executor
685    */
686   public SqlExecutor getSqlExecutor() {
687     return sqlMapClient.getSqlExecutor();
688   }
689 
690   /**
691    * Gets the sql map client.
692    *
693    * @return the sql map client
694    */
695   public SqlMapClient getSqlMapClient() {
696     return sqlMapClient;
697   }
698 
699   /**
700    * Sets the sql map client.
701    *
702    * @param sqlMapClient
703    *          the new sql map client
704    */
705   public void setSqlMapClient(SqlMapClient sqlMapClient) {
706     this.sqlMapClient = (SqlMapClientImpl) sqlMapClient;
707   }
708 
709   /**
710    * Inits the request.
711    *
712    * @param statementScope
713    *          the statement scope
714    */
715   public void initRequest(StatementScope statementScope) {
716     statementScope.setStatement(this);
717     statementScope.setParameterMap(parameterMap);
718     statementScope.setResultMap(resultMap);
719     statementScope.setSql(sql);
720   }
721 
722   /**
723    * Gets the timeout.
724    *
725    * @return the timeout
726    */
727   public Integer getTimeout() {
728     return timeout;
729   }
730 
731   /**
732    * Sets the timeout.
733    *
734    * @param timeout
735    *          the new timeout
736    */
737   public void setTimeout(Integer timeout) {
738     this.timeout = timeout;
739   }
740 
741   /**
742    * Adds the result map.
743    *
744    * @param resultMap
745    *          the result map
746    */
747   public void addResultMap(ResultMap resultMap) {
748     List<ResultMap> resultMapList = Arrays.asList(additionalResultMaps);
749     resultMapList = new ArrayList<>(resultMapList);
750     resultMapList.add(resultMap);
751     additionalResultMaps = resultMapList.toArray(new ResultMap[resultMapList.size()]);
752   }
753 
754   /**
755    * Checks for multiple result maps.
756    *
757    * @return true, if successful
758    */
759   public boolean hasMultipleResultMaps() {
760     return additionalResultMaps.length > 0;
761   }
762 
763   /**
764    * Gets the additional result maps.
765    *
766    * @return the additional result maps
767    */
768   public ResultMap[] getAdditionalResultMaps() {
769     return additionalResultMaps;
770   }
771 }