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