View Javadoc
1   /*
2    * AutoDAO - Generic DAO on steroids implementation for Java.
3    *
4    * Copyright 2008-2012  Marat Radchenko <slonopotamus@users.sourceforge.net>
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package net.sf.autodao.impl;
19  
20  import java.lang.annotation.Annotation;
21  import java.lang.reflect.Method;
22  import java.util.Collection;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import net.sf.autodao.Finder;
29  import net.sf.autodao.Named;
30  import net.sf.autodao.QueryArgumentTransformer;
31  import org.jetbrains.annotations.NotNull;
32  import org.jetbrains.annotations.Nullable;
33  import org.springframework.core.annotation.AnnotationUtils;
34  import static org.springframework.util.StringUtils.hasText;
35  
36  /**
37   * Performs finder method arguments validation.
38   *
39   * <p>NOT FOR PUBLIC USE.
40   */
41  public abstract class ParametersChecker implements Utils.ParameterCallback {
42    private final boolean singleFind;
43    @NotNull
44    private final Method method;
45    @NotNull
46    private final Map<Class<?>, QueryArgumentTransformer<?, ?>> transformers;
47  
48    @NotNull
49    private final Set<String> names = new HashSet<>();
50    private boolean limit;
51    private boolean offset;
52    private int indexed;
53  
54    protected ParametersChecker(@NotNull final Method method,
55                                @NotNull final Map<Class<?>, QueryArgumentTransformer<?, ?>> transformers) {
56      final Finder f = AnnotationUtils.getAnnotation(method, Finder.class);
57      if (f == null)
58        throw new SpecificationViolationException("Method is neither a finder method nor is implemented in AutoDAO", method);
59  
60      final int sum
61          = (hasText(f.query()) ? 1 : 0)
62          + (hasText(f.queryName()) ? 1 : 0)
63          + (hasText(f.sqlQuery()) ? 1 : 0)
64          + (hasText(f.sqlQueryName()) ? 1 : 0);
65      if (sum < 1)
66        throw new SpecificationViolationException("You need to specify at least one of: query, queryName, sqlQuery, sqlQueryName", method);
67  
68      if (sum > 1)
69        throw new SpecificationViolationException("You can specify only one of: query, queryName, sqlQuery, sqlQueryName", method);
70  
71      this.singleFind = validateReturnType(method);
72      this.method = method;
73      this.transformers = transformers;
74    }
75  
76    @NotNull
77    protected final Method getMethod() {
78      return method;
79    }
80  
81    @Nullable
82    protected abstract Class<?> getExpectedType(int paramIndex);
83  
84    @Nullable
85    protected abstract Class<?> getExpectedType(@NotNull String paramName);
86  
87    protected abstract int getExpectedOrdinalParametersCount();
88  
89    @Nullable
90    protected abstract Set<String> getExpectedNamedParameters();
91  
92    /**
93     * @param method method to validate.
94     * @return <code>true</code> if method is single-find, <code>false</code> otherwise.
95     * @throws SpecificationViolationException
96     */
97    private static boolean validateReturnType(@NotNull final Method method) {
98      final Finder f = AnnotationUtils.findAnnotation(method, Finder.class);
99      final boolean singleFind = !Collection.class.isAssignableFrom(method.getReturnType());
100     if (!singleFind) {
101       final Class<?> returnType = method.getReturnType();
102       if (!returnType.isAssignableFrom(f.returnAs())) {
103         throw new SpecificationViolationException(
104             "@Finder.returnAs value (" + f.returnAs().getName()
105                 + ") cannot be cast to method return type (" + returnType.getName() + ")", method
106         );
107       } else if (f.returnAs() != List.class) {
108         if (f.returnAs().isInterface()) {
109           throw new SpecificationViolationException(
110               "@Finder.returnAs value (" + f.returnAs().getName() + ") cannot be an interface", method
111           );
112         } else {
113           try {
114             f.returnAs().getConstructor();
115           } catch (NoSuchMethodException e) {
116             throw new SpecificationViolationException(
117                 "@Finder.returnAs value (" + f.returnAs().getName() + ") doesn't have no-arg constructor.", method
118             );
119           }
120         }
121       }
122     }
123     return singleFind;
124   }
125 
126   public void check() {
127     Utils.visitMethodParameters(method, this);
128 
129 
130     int queryIndexedCount = getExpectedOrdinalParametersCount();
131     if (queryIndexedCount >= 0 && indexed != queryIndexedCount)
132       throw new SpecificationViolationException(String.format("Not enough indexed params (query has %s but method has %s)", queryIndexedCount, indexed), method);
133 
134     final Set<?> expectedNamedParameters = getExpectedNamedParameters();
135     if (expectedNamedParameters != null && !names.equals(expectedNamedParameters))
136       throw new SpecificationViolationException(String.format("Wrong named parameters (query has %s but method has %s)", expectedNamedParameters, names), method);
137   }
138 
139   @Override
140   public void visit(final int index, @NotNull final Class<?> type, @NotNull final Annotation[] annotations) {
141     boolean processed = false;
142     if (Utils.getLimit(annotations) != null) {
143       limitCheck(type);
144       processed = true;
145     }
146     if (Utils.getOffset(annotations) != null) {
147       offsetCheck(type);
148       processed = true;
149     }
150     final Named named = Utils.getNamed(annotations);
151     if (processed) {
152       if (named != null)
153         throw new SpecificationViolationException("@Named cannot be present on @Limit/@Offset param", method);
154     } else {
155       if (named == null) {
156         if (!names.isEmpty())
157           throw new SpecificationViolationException("You're not allowed to mix @Named and indexed params", method);
158 
159         final int paramIndex = index + 1
160             - (limit ? 1 : 0)
161             - (offset ? 1 : 0);
162         final Class<?> expectedType = getExpectedType(paramIndex);
163         if (expectedType != null) {
164           final Class<?> actualType = getActualParameterType(type, false);
165           if (!expectedType.isAssignableFrom(actualType)) {
166             final String msg = String.format("Indexed parameter '%s' of type %s cannot be assigned to query parameter of type %s", paramIndex, actualType.getName(), expectedType.getName());
167             throw new SpecificationViolationException(msg, method);
168           }
169         }
170         indexed++;
171       } else {
172         if (indexed > 0)
173           throw new SpecificationViolationException("You're not allowed to mix @Named and indexed params", method);
174 
175         final String name = named.value();
176         final Class<?> expectedType = getExpectedType(name);
177         if (expectedType != null) {
178           final Class<?> actualClass = getActualParameterType(type, true);
179           if (!Collection.class.isAssignableFrom(actualClass) && !expectedType.isAssignableFrom(actualClass)) {
180             final String msg = String.format("Named parameter '%s' of type %s cannot be assigned to query parameter of type %s", name, actualClass.getName(), expectedType.getName());
181             throw new SpecificationViolationException(msg, method);
182           }
183         }
184         if (!names.add(name)) {
185           throw new SpecificationViolationException("Duplicate named parameters '" + name + "'", method);
186         }
187       }
188     }
189   }
190 
191   @NotNull
192   private Class<?> getActualParameterType(@NotNull final Class<?> declaredType, final boolean handleArrays) {
193     final QueryArgumentTransformer<?, ?> transformer = transformers.get(declaredType);
194     final Class<?> transformedType = transformer == null ? declaredType : transformer.getTargetType();
195     // TODO: check collection params too
196     final Class<?> arrayUnwrappedType = handleArrays && transformedType.isArray() ? transformedType.getComponentType() : transformedType;
197     return primitive(arrayUnwrappedType);
198   }
199 
200   /**
201    * Copied from commons-binutils (org.apache.commons.beanutils.converters.AbstractConverter).
202    * <p/>
203    * Change primitive Class types to the associated wrapper class.
204    *
205    * @param type The class type to check.
206    * @return The converted type.
207    */
208   @SuppressWarnings({ "IfStatementWithTooManyBranches", "ObjectEquality" })
209   @NotNull
210   private static Class<?> primitive(@NotNull final Class<?> type) {
211     if (!type.isPrimitive()) {
212       return type;
213     } else if (type == Integer.TYPE) {
214       return Integer.class;
215     } else if (type == Double.TYPE) {
216       return Double.class;
217     } else if (type == Long.TYPE) {
218       return Long.class;
219     } else if (type == Boolean.TYPE) {
220       return Boolean.class;
221     } else if (type == Float.TYPE) {
222       return Float.class;
223     } else if (type == Short.TYPE) {
224       return Short.class;
225     } else if (type == Byte.TYPE) {
226       return Byte.class;
227     } else if (type == Character.TYPE) {
228       return Character.class;
229     } else {
230       return type;
231     }
232   }
233 
234   private void limitCheck(final Class<?> type) {
235     limitOffsetCheck(limit, type);
236     limit = true;
237   }
238 
239   private void offsetCheck(final Class<?> type) {
240     limitOffsetCheck(offset, type);
241     offset = true;
242   }
243 
244   private void limitOffsetCheck(final boolean value, final Class<?> type) {
245     if (value)
246       throw new SpecificationViolationException("@Limit/@Offset can appear only on one parameter", method);
247 
248     if (singleFind)
249       throw new SpecificationViolationException("@Limit/@Offset isn't allowed on single-object finders", method);
250 
251     if (type != Integer.class && type != Integer.TYPE)
252       throw new SpecificationViolationException("@Limit and @Offset argument type must be either int or java.lang.Integer", method);
253   }
254 }