/* * Copyright (C) 2010 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.inject.persist.jpa; import com.google.common.collect.MapMaker; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.google.inject.persist.finder.Finder; import com.google.inject.persist.finder.FirstResult; import com.google.inject.persist.finder.MaxResults; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.Query; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; /** * TODO(dhanji): Make this work!! * * @author Dhanji R. Prasanna (dhanji@gmail.com) */ @Singleton class JpaFinderProxy implements MethodInterceptor { private final Map<Method, FinderDescriptor> finderCache = new MapMaker().weakKeys().makeMap(); private final Provider<EntityManager> emProvider; @Inject public JpaFinderProxy(Provider<EntityManager> emProvider) { this.emProvider = emProvider; } @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { EntityManager em = emProvider.get(); //obtain a cached finder descriptor (or create a new one) JpaFinderProxy.FinderDescriptor finderDescriptor = getFinderDescriptor(methodInvocation); Object result = null; //execute as query (named params or otherwise) Query jpaQuery = finderDescriptor.createQuery(em); if (finderDescriptor.isBindAsRawParameters) { bindQueryRawParameters(jpaQuery, finderDescriptor, methodInvocation.getArguments()); } else { bindQueryNamedParameters(jpaQuery, finderDescriptor, methodInvocation.getArguments()); } //depending upon return type, decorate or return the result as is if (JpaFinderProxy.ReturnType.PLAIN.equals(finderDescriptor.returnType)) { result = jpaQuery.getSingleResult(); } else if (JpaFinderProxy.ReturnType.COLLECTION.equals(finderDescriptor.returnType)) { result = getAsCollection(finderDescriptor, jpaQuery.getResultList()); } else if (JpaFinderProxy.ReturnType.ARRAY.equals(finderDescriptor.returnType)) { result = jpaQuery.getResultList().toArray(); } return result; } private Object getAsCollection(JpaFinderProxy.FinderDescriptor finderDescriptor, List results) { Collection<?> collection; try { collection = (Collection) finderDescriptor.returnCollectionTypeConstructor.newInstance(); } catch (InstantiationException e) { throw new RuntimeException( "Specified collection class of Finder's returnAs could not be instantated: " + finderDescriptor.returnCollectionType, e); } catch (IllegalAccessException e) { throw new RuntimeException( "Specified collection class of Finder's returnAs could not be instantated (do not have access privileges): " + finderDescriptor.returnCollectionType, e); } catch (InvocationTargetException e) { throw new RuntimeException( "Specified collection class of Finder's returnAs could not be instantated (it threw an exception): " + finderDescriptor.returnCollectionType, e); } collection.addAll(results); return collection; } private void bindQueryNamedParameters( Query jpaQuery, JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments) { for (int i = 0; i < arguments.length; i++) { Object argument = arguments[i]; Object annotation = descriptor.parameterAnnotations[i]; if (null == annotation) //noinspection UnnecessaryContinue { continue; //skip param as it's not bindable } else if (annotation instanceof Named) { Named named = (Named) annotation; jpaQuery.setParameter(named.value(), argument); } else if (annotation instanceof javax.inject.Named) { javax.inject.Named named = (javax.inject.Named) annotation; jpaQuery.setParameter(named.value(), argument); } else if (annotation instanceof FirstResult) { jpaQuery.setFirstResult((Integer) argument); } else if (annotation instanceof MaxResults) { jpaQuery.setMaxResults((Integer) argument); } } } private void bindQueryRawParameters( Query jpaQuery, JpaFinderProxy.FinderDescriptor descriptor, Object[] arguments) { for (int i = 0, index = 1; i < arguments.length; i++) { Object argument = arguments[i]; Object annotation = descriptor.parameterAnnotations[i]; if (null == annotation) { //bind it as a raw param (1-based index, yes I know its different from Hibernate, blargh) jpaQuery.setParameter(index, argument); index++; } else if (annotation instanceof FirstResult) { jpaQuery.setFirstResult((Integer) argument); } else if (annotation instanceof MaxResults) { jpaQuery.setMaxResults((Integer) argument); } } } private JpaFinderProxy.FinderDescriptor getFinderDescriptor(MethodInvocation invocation) { Method method = invocation.getMethod(); JpaFinderProxy.FinderDescriptor finderDescriptor = finderCache.get(method); if (null != finderDescriptor) { return finderDescriptor; } //otherwise reflect and cache finder info... finderDescriptor = new JpaFinderProxy.FinderDescriptor(); //determine return type finderDescriptor.returnClass = invocation.getMethod().getReturnType(); finderDescriptor.returnType = determineReturnType(finderDescriptor.returnClass); //determine finder query characteristics Finder finder = invocation.getMethod().getAnnotation(Finder.class); String query = finder.query(); if (!"".equals(query.trim())) { finderDescriptor.setQuery(query); } else { finderDescriptor.setNamedQuery(finder.namedQuery()); } //determine parameter annotations Annotation[][] parameterAnnotations = method.getParameterAnnotations(); Object[] discoveredAnnotations = new Object[parameterAnnotations.length]; for (int i = 0; i < parameterAnnotations.length; i++) { Annotation[] annotations = parameterAnnotations[i]; //each annotation per param for (Annotation annotation : annotations) { //discover the named, first or max annotations then break out Class<? extends Annotation> annotationType = annotation.annotationType(); if (Named.class.equals(annotationType) || javax.inject.Named.class.equals(annotationType)) { discoveredAnnotations[i] = annotation; finderDescriptor.isBindAsRawParameters = false; break; } else if (FirstResult.class.equals(annotationType)) { discoveredAnnotations[i] = annotation; break; } else if (MaxResults.class.equals(annotationType)) { discoveredAnnotations[i] = annotation; break; } //leave as null for no binding } } //set the discovered set to our finder cache object finderDescriptor.parameterAnnotations = discoveredAnnotations; //discover the returned collection implementation if this finder returns a collection if (JpaFinderProxy.ReturnType.COLLECTION.equals(finderDescriptor.returnType) && finderDescriptor.returnClass != Collection.class) { finderDescriptor.returnCollectionType = finder.returnAs(); try { finderDescriptor.returnCollectionTypeConstructor = finderDescriptor.returnCollectionType.getConstructor(); finderDescriptor.returnCollectionTypeConstructor.setAccessible(true); //UGH! } catch (NoSuchMethodException e) { throw new RuntimeException( "Finder's collection return type specified has no default constructor! returnAs: " + finderDescriptor.returnCollectionType, e); } } //cache it cacheFinderDescriptor(method, finderDescriptor); return finderDescriptor; } /** * writes to a chm (used to provide copy-on-write but this is bettah!) * * @param method The key * @param finderDescriptor The descriptor to cache */ private void cacheFinderDescriptor(Method method, FinderDescriptor finderDescriptor) { //write to concurrent map finderCache.put(method, finderDescriptor); } private JpaFinderProxy.ReturnType determineReturnType(Class<?> returnClass) { if (Collection.class.isAssignableFrom(returnClass)) { return JpaFinderProxy.ReturnType.COLLECTION; } else if (returnClass.isArray()) { return JpaFinderProxy.ReturnType.ARRAY; } return JpaFinderProxy.ReturnType.PLAIN; } /** A wrapper data class that caches information about a finder method. */ private static class FinderDescriptor { private volatile boolean isKeyedQuery = false; volatile boolean isBindAsRawParameters = true; //should we treat the query as having ? instead of :named params volatile JpaFinderProxy.ReturnType returnType; volatile Class<?> returnClass; volatile Class<? extends Collection> returnCollectionType; volatile Constructor returnCollectionTypeConstructor; volatile Object[] parameterAnnotations; //contract is: null = no bind, @Named = param, @FirstResult/@MaxResults for paging private String query; private String name; void setQuery(String query) { this.query = query; } void setNamedQuery(String name) { this.name = name; isKeyedQuery = true; } public boolean isKeyedQuery() { return isKeyedQuery; } Query createQuery(EntityManager em) { return isKeyedQuery ? em.createNamedQuery(name) : em.createQuery(query); } } private static enum ReturnType { PLAIN, COLLECTION, ARRAY } }