JpaFinderProxy.java revision 150d2a9b62030f48b5cf770e42be1b12c61d983f
1/**
2 * Copyright (C) 2010 Google, Inc.
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
17package com.google.inject.persist.jpa;
18
19import com.google.inject.Inject;
20import com.google.inject.Provider;
21import com.google.inject.Singleton;
22import com.google.inject.internal.util.MapMaker;
23import com.google.inject.name.Named;
24import com.google.inject.persist.finder.Finder;
25import com.google.inject.persist.finder.FirstResult;
26import com.google.inject.persist.finder.MaxResults;
27import java.lang.annotation.Annotation;
28import java.lang.reflect.Constructor;
29import java.lang.reflect.InvocationTargetException;
30import java.lang.reflect.Method;
31import java.util.Collection;
32import java.util.List;
33import java.util.Map;
34import javax.persistence.EntityManager;
35import javax.persistence.Query;
36import org.aopalliance.intercept.MethodInterceptor;
37import org.aopalliance.intercept.MethodInvocation;
38
39/**
40 * TODO(dhanji): Make this work!!
41 *
42 * @author Dhanji R. Prasanna (dhanji@gmail.com)
43 */
44@Singleton
45class JpaFinderProxy implements MethodInterceptor {
46  private final Map<Method, FinderDescriptor> finderCache = new MapMaker().weakKeys().makeMap();
47  private final Provider<EntityManager> emProvider;
48
49  @Inject
50  public JpaFinderProxy(Provider<EntityManager> emProvider) {
51    this.emProvider = emProvider;
52  }
53
54  public Object invoke(MethodInvocation methodInvocation) throws Throwable {
55    EntityManager em = emProvider.get();
56
57    //obtain a cached finder descriptor (or create a new one)
58    JpaFinderProxy.FinderDescriptor finderDescriptor = getFinderDescriptor(methodInvocation);
59
60    Object result = null;
61
62    //execute as query (named params or otherwise)
63    Query jpaQuery = finderDescriptor.createQuery(em);
64    if (finderDescriptor.isBindAsRawParameters) {
65      bindQueryRawParameters(jpaQuery, finderDescriptor, methodInvocation.getArguments());
66    } else {
67      bindQueryNamedParameters(jpaQuery, finderDescriptor, methodInvocation.getArguments());
68    }
69
70    //depending upon return type, decorate or return the result as is
71    if (JpaFinderProxy.ReturnType.PLAIN.equals(finderDescriptor.returnType)) {
72      result = jpaQuery.getSingleResult();
73    } else if (JpaFinderProxy.ReturnType.COLLECTION.equals(finderDescriptor.returnType)) {
74      result = getAsCollection(finderDescriptor, jpaQuery.getResultList());
75    } else if (JpaFinderProxy.ReturnType.ARRAY.equals(finderDescriptor.returnType)) {
76      result = jpaQuery.getResultList().toArray();
77    }
78
79    return result;
80  }
81
82  private Object getAsCollection(JpaFinderProxy.FinderDescriptor finderDescriptor,
83      List results) {
84    Collection<?> collection;
85    try {
86      collection = (Collection) finderDescriptor.returnCollectionTypeConstructor.newInstance();
87    } catch (InstantiationException e) {
88      throw new RuntimeException(
89          "Specified collection class of Finder's returnAs could not be instantated: "
90              + finderDescriptor.returnCollectionType, e);
91    } catch (IllegalAccessException e) {
92      throw new RuntimeException(
93          "Specified collection class of Finder's returnAs could not be instantated (do not have access privileges): "
94              + finderDescriptor.returnCollectionType, e);
95    } catch (InvocationTargetException e) {
96      throw new RuntimeException(
97          "Specified collection class of Finder's returnAs could not be instantated (it threw an exception): "
98              + finderDescriptor.returnCollectionType, e);
99    }
100
101    collection.addAll(results);
102    return collection;
103  }
104
105  private void bindQueryNamedParameters(Query jpaQuery,
106      JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments) {
107    for (int i = 0; i < arguments.length; i++) {
108      Object argument = arguments[i];
109      Object annotation = descriptor.parameterAnnotations[i];
110
111      if (null == annotation)
112      //noinspection UnnecessaryContinue
113      {
114        continue;   //skip param as it's not bindable
115      } else if (annotation instanceof Named) {
116        Named named = (Named) annotation;
117        jpaQuery.setParameter(named.value(), argument);
118      } else if (annotation instanceof FirstResult) {
119        jpaQuery.setFirstResult((Integer) argument);
120      } else if (annotation instanceof MaxResults) {
121        jpaQuery.setMaxResults((Integer) argument);
122      }
123    }
124  }
125
126  private void bindQueryRawParameters(Query jpaQuery,
127      JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments) {
128    for (int i = 0, index = 1; i < arguments.length; i++) {
129      Object argument = arguments[i];
130      Object annotation = descriptor.parameterAnnotations[i];
131
132      if (null == annotation) {
133        //bind it as a raw param (1-based index, yes I know its different from Hibernate, blargh)
134        jpaQuery.setParameter(index, argument);
135        index++;
136      } else if (annotation instanceof FirstResult) {
137        jpaQuery.setFirstResult((Integer) argument);
138      } else if (annotation instanceof MaxResults) {
139        jpaQuery.setMaxResults((Integer) argument);
140      }
141    }
142  }
143
144  private JpaFinderProxy.FinderDescriptor getFinderDescriptor(MethodInvocation invocation) {
145    Method method = invocation.getMethod();
146    JpaFinderProxy.FinderDescriptor finderDescriptor = finderCache.get(method);
147    if (null != finderDescriptor) {
148      return finderDescriptor;
149    }
150
151    //otherwise reflect and cache finder info...
152    finderDescriptor = new JpaFinderProxy.FinderDescriptor();
153
154    //determine return type
155    finderDescriptor.returnClass = invocation.getMethod().getReturnType();
156    finderDescriptor.returnType = determineReturnType(finderDescriptor.returnClass);
157
158    //determine finder query characteristics
159    Finder finder = invocation.getMethod().getAnnotation(Finder.class);
160    String query = finder.query();
161    if (!"".equals(query.trim())) {
162      finderDescriptor.setQuery(query);
163    } else {
164      finderDescriptor.setNamedQuery(finder.namedQuery());
165    }
166
167    //determine parameter annotations
168    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
169    Object[] discoveredAnnotations = new Object[parameterAnnotations.length];
170    for (int i = 0; i < parameterAnnotations.length; i++) {
171      Annotation[] annotations = parameterAnnotations[i];
172      //each annotation per param
173      for (Annotation annotation : annotations) {
174        //discover the named, first or max annotations then break out
175        Class<? extends Annotation> annotationType = annotation.annotationType();
176        if (Named.class.equals(annotationType)) {
177          discoveredAnnotations[i] = annotation;
178          finderDescriptor.isBindAsRawParameters = false;
179          break;
180        } else if (FirstResult.class.equals(annotationType)) {
181          discoveredAnnotations[i] = annotation;
182          break;
183        } else if (MaxResults.class.equals(annotationType)) {
184          discoveredAnnotations[i] = annotation;
185          break;
186        }   //leave as null for no binding
187      }
188    }
189
190    //set the discovered set to our finder cache object
191    finderDescriptor.parameterAnnotations = discoveredAnnotations;
192
193    //discover the returned collection implementation if this finder returns a collection
194    if (JpaFinderProxy.ReturnType.COLLECTION.equals(finderDescriptor.returnType)
195        && finderDescriptor.returnClass != Collection.class) {
196      System.out.println("-----" + finderDescriptor.returnClass);
197      finderDescriptor.returnCollectionType = finder.returnAs();
198      try {
199        finderDescriptor.returnCollectionTypeConstructor = finderDescriptor.returnCollectionType
200            .getConstructor();
201        finderDescriptor.returnCollectionTypeConstructor.setAccessible(true);   //UGH!
202      } catch (NoSuchMethodException e) {
203        throw new RuntimeException(
204            "Finder's collection return type specified has no default constructor! returnAs: "
205                + finderDescriptor.returnCollectionType, e);
206      }
207    }
208
209    //cache it
210    cacheFinderDescriptor(method, finderDescriptor);
211
212    return finderDescriptor;
213  }
214
215  /**
216   * writes to a chm (used to provide copy-on-write but this is bettah!)
217   *
218   * @param method The key
219   * @param finderDescriptor The descriptor to cache
220   */
221  private void cacheFinderDescriptor(Method method, FinderDescriptor finderDescriptor) {
222    //write to concurrent map
223    finderCache.put(method, finderDescriptor);
224  }
225
226  private JpaFinderProxy.ReturnType determineReturnType(Class<?> returnClass) {
227    if (Collection.class.isAssignableFrom(returnClass)) {
228      return JpaFinderProxy.ReturnType.COLLECTION;
229    } else if (returnClass.isArray()) {
230      return JpaFinderProxy.ReturnType.ARRAY;
231    }
232
233    return JpaFinderProxy.ReturnType.PLAIN;
234  }
235
236  /**
237   * A wrapper data class that caches information about a finder method.
238   */
239  private static class FinderDescriptor {
240    private volatile boolean isKeyedQuery = false;
241    volatile boolean isBindAsRawParameters = true;
242        //should we treat the query as having ? instead of :named params
243    volatile JpaFinderProxy.ReturnType returnType;
244    volatile Class<?> returnClass;
245    volatile Class<? extends Collection> returnCollectionType;
246    volatile Constructor returnCollectionTypeConstructor;
247    volatile Object[] parameterAnnotations;
248        //contract is: null = no bind, @Named = param, @FirstResult/@MaxResults for paging
249
250    private String query;
251    private String name;
252
253    void setQuery(String query) {
254      this.query = query;
255    }
256
257    void setNamedQuery(String name) {
258      this.name = name;
259      isKeyedQuery = true;
260    }
261
262    public boolean isKeyedQuery() {
263      return isKeyedQuery;
264    }
265
266    Query createQuery(EntityManager em) {
267      return isKeyedQuery ? em.createNamedQuery(name) : em.createQuery(query);
268    }
269  }
270
271  private static enum ReturnType {
272    PLAIN, COLLECTION, ARRAY
273  }
274}
275