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