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