blob: 7aae70cacce3560676b0a388c7854c4961274b29 [file] [log] [blame]
/**
* 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.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.internal.util.MapMaker;
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;
}
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
}
}