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@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 JpaFinderProxy(Provider<EntityManager> emProvider)53 public JpaFinderProxy(Provider<EntityManager> emProvider) { 54 this.emProvider = emProvider; 55 } 56 invoke(MethodInvocation methodInvocation)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 getAsCollection(JpaFinderProxy.FinderDescriptor finderDescriptor, List results)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 bindQueryNamedParameters(Query jpaQuery, JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments)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 bindQueryRawParameters(Query jpaQuery, JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments)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 getFinderDescriptor(MethodInvocation invocation)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 */ cacheFinderDescriptor(Method method, FinderDescriptor finderDescriptor)226 private void cacheFinderDescriptor(Method method, FinderDescriptor finderDescriptor) { 227 //write to concurrent map 228 finderCache.put(method, finderDescriptor); 229 } 230 determineReturnType(Class<?> returnClass)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 setQuery(String query)258 void setQuery(String query) { 259 this.query = query; 260 } 261 setNamedQuery(String name)262 void setNamedQuery(String name) { 263 this.name = name; 264 isKeyedQuery = true; 265 } 266 isKeyedQuery()267 public boolean isKeyedQuery() { 268 return isKeyedQuery; 269 } 270 createQuery(EntityManager em)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