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.binding;
17  
18  import java.lang.reflect.Array;
19  import java.lang.reflect.Method;
20  import java.lang.reflect.ParameterizedType;
21  import java.lang.reflect.Type;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Optional;
26  
27  import org.apache.ibatis.annotations.Flush;
28  import org.apache.ibatis.annotations.MapKey;
29  import org.apache.ibatis.cursor.Cursor;
30  import org.apache.ibatis.mapping.MappedStatement;
31  import org.apache.ibatis.mapping.SqlCommandType;
32  import org.apache.ibatis.mapping.StatementType;
33  import org.apache.ibatis.reflection.MetaObject;
34  import org.apache.ibatis.reflection.ParamNameResolver;
35  import org.apache.ibatis.reflection.TypeParameterResolver;
36  import org.apache.ibatis.session.Configuration;
37  import org.apache.ibatis.session.ResultHandler;
38  import org.apache.ibatis.session.RowBounds;
39  import org.apache.ibatis.session.SqlSession;
40  
41  /**
42   * @author Clinton Begin
43   * @author Eduardo Macarron
44   * @author Lasse Voss
45   * @author Kazuki Shimizu
46   */
47  public class MapperMethod {
48  
49    private final SqlCommand command;
50    private final MethodSignature method;
51  
52    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
53      this.command = new SqlCommand(config, mapperInterface, method);
54      this.method = new MethodSignature(config, mapperInterface, method);
55    }
56  
57    public Object execute(SqlSession sqlSession, Object[] args) {
58      Object result;
59      switch (command.getType()) {
60        case INSERT: {
61          Object param = method.convertArgsToSqlCommandParam(args);
62          result = rowCountResult(sqlSession.insert(command.getName(), param));
63          break;
64        }
65        case UPDATE: {
66          Object param = method.convertArgsToSqlCommandParam(args);
67          result = rowCountResult(sqlSession.update(command.getName(), param));
68          break;
69        }
70        case DELETE: {
71          Object param = method.convertArgsToSqlCommandParam(args);
72          result = rowCountResult(sqlSession.delete(command.getName(), param));
73          break;
74        }
75        case SELECT:
76          if (method.returnsVoid() && method.hasResultHandler()) {
77            executeWithResultHandler(sqlSession, args);
78            result = null;
79          } else if (method.returnsMany()) {
80            result = executeForMany(sqlSession, args);
81          } else if (method.returnsMap()) {
82            result = executeForMap(sqlSession, args);
83          } else if (method.returnsCursor()) {
84            result = executeForCursor(sqlSession, args);
85          } else {
86            Object param = method.convertArgsToSqlCommandParam(args);
87            result = sqlSession.selectOne(command.getName(), param);
88            if (method.returnsOptional()
89                && (result == null || !method.getReturnType().equals(result.getClass()))) {
90              result = Optional.ofNullable(result);
91            }
92          }
93          break;
94        case FLUSH:
95          result = sqlSession.flushStatements();
96          break;
97        default:
98          throw new BindingException("Unknown execution method for: " + command.getName());
99      }
100     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
101       throw new BindingException("Mapper method '" + command.getName()
102           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
103     }
104     return result;
105   }
106 
107   private Object rowCountResult(int rowCount) {
108     final Object result;
109     if (method.returnsVoid()) {
110       result = null;
111     } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
112       result = rowCount;
113     } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
114       result = (long) rowCount;
115     } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
116       result = rowCount > 0;
117     } else {
118       throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
119     }
120     return result;
121   }
122 
123   private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
124     MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
125     if (!StatementType.CALLABLE.equals(ms.getStatementType())
126         && void.class.equals(ms.getResultMaps().get(0).getType())) {
127       throw new BindingException("method " + command.getName()
128           + " needs either a @ResultMap annotation, a @ResultType annotation,"
129           + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
130     }
131     Object param = method.convertArgsToSqlCommandParam(args);
132     if (method.hasRowBounds()) {
133       RowBounds rowBounds = method.extractRowBounds(args);
134       sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
135     } else {
136       sqlSession.select(command.getName(), param, method.extractResultHandler(args));
137     }
138   }
139 
140   private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
141     List<E> result;
142     Object param = method.convertArgsToSqlCommandParam(args);
143     if (method.hasRowBounds()) {
144       RowBounds rowBounds = method.extractRowBounds(args);
145       result = sqlSession.selectList(command.getName(), param, rowBounds);
146     } else {
147       result = sqlSession.selectList(command.getName(), param);
148     }
149     // issue #510 Collections & arrays support
150     if (!method.getReturnType().isAssignableFrom(result.getClass())) {
151       if (method.getReturnType().isArray()) {
152         return convertToArray(result);
153       } else {
154         return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
155       }
156     }
157     return result;
158   }
159 
160   private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
161     Cursor<T> result;
162     Object param = method.convertArgsToSqlCommandParam(args);
163     if (method.hasRowBounds()) {
164       RowBounds rowBounds = method.extractRowBounds(args);
165       result = sqlSession.selectCursor(command.getName(), param, rowBounds);
166     } else {
167       result = sqlSession.selectCursor(command.getName(), param);
168     }
169     return result;
170   }
171 
172   private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
173     Object collection = config.getObjectFactory().create(method.getReturnType());
174     MetaObject metaObject = config.newMetaObject(collection);
175     metaObject.addAll(list);
176     return collection;
177   }
178 
179   @SuppressWarnings("unchecked")
180   private <E> Object convertToArray(List<E> list) {
181     Class<?> arrayComponentType = method.getReturnType().getComponentType();
182     Object array = Array.newInstance(arrayComponentType, list.size());
183     if (arrayComponentType.isPrimitive()) {
184       for (int i = 0; i < list.size(); i++) {
185         Array.set(array, i, list.get(i));
186       }
187       return array;
188     } else {
189       return list.toArray((E[]) array);
190     }
191   }
192 
193   private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
194     Map<K, V> result;
195     Object param = method.convertArgsToSqlCommandParam(args);
196     if (method.hasRowBounds()) {
197       RowBounds rowBounds = method.extractRowBounds(args);
198       result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
199     } else {
200       result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
201     }
202     return result;
203   }
204 
205   public static class ParamMap<V> extends HashMap<String, V> {
206 
207     private static final long serialVersionUID = -2212268410512043556L;
208 
209     @Override
210     public V get(Object key) {
211       if (!super.containsKey(key)) {
212         throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
213       }
214       return super.get(key);
215     }
216 
217   }
218 
219   public static class SqlCommand {
220 
221     private final String name;
222     private final SqlCommandType type;
223 
224     public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
225       final String methodName = method.getName();
226       final Class<?> declaringClass = method.getDeclaringClass();
227       MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
228           configuration);
229       if (ms == null) {
230         if (method.getAnnotation(Flush.class) != null) {
231           name = null;
232           type = SqlCommandType.FLUSH;
233         } else {
234           throw new BindingException("Invalid bound statement (not found): "
235               + mapperInterface.getName() + "." + methodName);
236         }
237       } else {
238         name = ms.getId();
239         type = ms.getSqlCommandType();
240         if (type == SqlCommandType.UNKNOWN) {
241           throw new BindingException("Unknown execution method for: " + name);
242         }
243       }
244     }
245 
246     public String getName() {
247       return name;
248     }
249 
250     public SqlCommandType getType() {
251       return type;
252     }
253 
254     private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
255         Class<?> declaringClass, Configuration configuration) {
256       String statementId = mapperInterface.getName() + "." + methodName;
257       if (configuration.hasStatement(statementId)) {
258         return configuration.getMappedStatement(statementId);
259       } else if (mapperInterface.equals(declaringClass)) {
260         return null;
261       }
262       for (Class<?> superInterface : mapperInterface.getInterfaces()) {
263         if (declaringClass.isAssignableFrom(superInterface)) {
264           MappedStatement ms = resolveMappedStatement(superInterface, methodName,
265               declaringClass, configuration);
266           if (ms != null) {
267             return ms;
268           }
269         }
270       }
271       return null;
272     }
273   }
274 
275   public static class MethodSignature {
276 
277     private final boolean returnsMany;
278     private final boolean returnsMap;
279     private final boolean returnsVoid;
280     private final boolean returnsCursor;
281     private final boolean returnsOptional;
282     private final Class<?> returnType;
283     private final String mapKey;
284     private final Integer resultHandlerIndex;
285     private final Integer rowBoundsIndex;
286     private final ParamNameResolver paramNameResolver;
287 
288     public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
289       Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
290       if (resolvedReturnType instanceof Class<?>) {
291         this.returnType = (Class<?>) resolvedReturnType;
292       } else if (resolvedReturnType instanceof ParameterizedType) {
293         this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
294       } else {
295         this.returnType = method.getReturnType();
296       }
297       this.returnsVoid = void.class.equals(this.returnType);
298       this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
299       this.returnsCursor = Cursor.class.equals(this.returnType);
300       this.returnsOptional = Optional.class.equals(this.returnType);
301       this.mapKey = getMapKey(method);
302       this.returnsMap = this.mapKey != null;
303       this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
304       this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
305       this.paramNameResolver = new ParamNameResolver(configuration, method);
306     }
307 
308     public Object convertArgsToSqlCommandParam(Object[] args) {
309       return paramNameResolver.getNamedParams(args);
310     }
311 
312     public boolean hasRowBounds() {
313       return rowBoundsIndex != null;
314     }
315 
316     public RowBounds extractRowBounds(Object[] args) {
317       return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
318     }
319 
320     public boolean hasResultHandler() {
321       return resultHandlerIndex != null;
322     }
323 
324     public ResultHandler extractResultHandler(Object[] args) {
325       return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
326     }
327 
328     public Class<?> getReturnType() {
329       return returnType;
330     }
331 
332     public boolean returnsMany() {
333       return returnsMany;
334     }
335 
336     public boolean returnsMap() {
337       return returnsMap;
338     }
339 
340     public boolean returnsVoid() {
341       return returnsVoid;
342     }
343 
344     public boolean returnsCursor() {
345       return returnsCursor;
346     }
347 
348     /**
349      * return whether return type is {@code java.util.Optional}.
350      *
351      * @return return {@code true}, if return type is {@code java.util.Optional}
352      * @since 3.5.0
353      */
354     public boolean returnsOptional() {
355       return returnsOptional;
356     }
357 
358     private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
359       Integer index = null;
360       final Class<?>[] argTypes = method.getParameterTypes();
361       for (int i = 0; i < argTypes.length; i++) {
362         if (paramType.isAssignableFrom(argTypes[i])) {
363           if (index == null) {
364             index = i;
365           } else {
366             throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
367           }
368         }
369       }
370       return index;
371     }
372 
373     public String getMapKey() {
374       return mapKey;
375     }
376 
377     private String getMapKey(Method method) {
378       String mapKey = null;
379       if (Map.class.isAssignableFrom(method.getReturnType())) {
380         final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
381         if (mapKeyAnnotation != null) {
382           mapKey = mapKeyAnnotation.value();
383         }
384       }
385       return mapKey;
386     }
387   }
388 
389 }