blob: 4df1d8d4ebb242b74f344dd2707054d184c93eef [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.openejb.util.proxy;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class QueryProxy implements InvocationHandler {
private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB, QueryProxy.class);
// keywords
public static final String PERSIST_NAME = "save";
public static final String MERGE_NAME = "update";
public static final String REMOVE_NAME = "delete";
public static final String NAMED_QUERY_NAME = "namedQuery";
public static final String NATIVE_QUERY_NAME = "nativeQuery";
public static final String QUERY_NAME = "query";
public static final String FIND_PREFIX = "find";
public static final String BY = "By";
public static final String AND = "And";
// cache for finders of the current instance
private final Map<String, Class<?>> RETURN_TYPES = new ConcurrentHashMap<String, Class<?>>();
private final Map<String, List<String>> CONDITIONS = new ConcurrentHashMap<String, List<String>>();
private EntityManager em;
private static enum QueryType {
NAMED, NATIVE, OTHER
}
public void setEntityManager(EntityManager entityManager) {
em = entityManager;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass().equals(Object.class)) {
return method.invoke(this, args);
}
final String methodName = method.getName();
final Class<?> returnType = method.getReturnType();
// simple cases
if (PERSIST_NAME.equals(methodName)) {
persist(args, returnType);
return null; // void
}
if (MERGE_NAME.equals(methodName)) {
return merge(args, returnType);
}
if (REMOVE_NAME.equals(methodName)) {
remove(args, returnType);
return null; // void
}
// queries
if (NAMED_QUERY_NAME.equals(methodName)) {
return query(method, args, QueryType.NAMED);
}
if (NATIVE_QUERY_NAME.equals(methodName)) {
return query(method, args, QueryType.NATIVE);
}
if (QUERY_NAME.equals(methodName)) {
return query(method, args, QueryType.OTHER);
}
// finders
if (methodName.startsWith(FIND_PREFIX)) {
return find(method, args);
}
throw new IllegalArgumentException("method not yet managed");
}
/**
*
* @param method the method
* @param args queryName (String) -> first parameter, parameters (Map<String, ?>) or (Object[]), first and max (int) -> max follows first
* @param type the query type
* @return the expected result
*/
private Object query(Method method, Object[] args, QueryType type) {
if (args.length < 1) {
throw new IllegalArgumentException("query() needs at least the query name");
}
int matched = 0;
Query query;
if (String.class.isAssignableFrom(args[0].getClass())) {
switch (type) {
case NAMED:
query = em.createNamedQuery((String) args[0]);
break;
case NATIVE:
query = em.createNativeQuery((String) args[0]);
break;
default:
query = em.createQuery((String) args[0]);
}
matched++;
for (int i = 1; i < args.length; i++) {
if (args[i] == null) {
continue;
}
if (Map.class.isAssignableFrom(args[i].getClass())) {
for (Map.Entry<String, ?> entry : ((Map<String, ?>) args[i]).entrySet()) {
query = query.setParameter(entry.getKey(), entry.getValue());
}
matched++;
} else if (args[i].getClass().isArray()) {
Object[] array = (Object[]) args[i];
for (int j = 0; j < array.length; j++) {
query = query.setParameter(j, array[j]);
}
matched++;
} else if (isInt(args[i].getClass())) {
int next = i + 1;
if (args.length == next || !isInt(args[next].getClass())) {
throw new IllegalArgumentException("if you provide a firstResult (first int parameter)" +
"you should provide a maxResult too");
}
int first = (Integer) args[i];
int max = (Integer) args[next];
query = query.setFirstResult(first);
query = query.setMaxResults(max);
matched += 2;
i++;
} else {
throw new IllegalArgumentException("not managed parameter " + args[i]
+ " of type " + args[i].getClass());
}
}
if (matched != args.length) {
throw new IllegalArgumentException("all argument was not used, please check you signature looks like:" +
" <ReturnType> query(String name, Map<String, ?> parameters, int firstResult, int maxResult)");
}
} else {
throw new IllegalArgumentException("query() needs at least the query name of type String");
}
return getQueryResult(method, query);
}
private Class<?> getReturnedType(Method method) {
final String methodName = method.getName();
Class<?> type;
if (RETURN_TYPES.containsKey(methodName)) {
type = RETURN_TYPES.get(methodName);
} else {
type = getGenericType(method.getGenericReturnType());
RETURN_TYPES.put(methodName, type);
}
return type;
}
private Object getQueryResult(Method method, Query query) {
if (Collection.class.isAssignableFrom(method.getReturnType())) {
return query.getResultList();
}
return query.getSingleResult();
}
private Object find(Method method, Object[] args) {
final String methodName = method.getName();
final Class<?> type = getReturnedType(method);
final Query query = createFinderQuery(em, methodName, type, args);
return getQueryResult(method, query);
}
private void remove(Object[] args, Class<?> returnType) {
if (args != null && args.length == 1 && returnType.equals(Void.TYPE)) {
Object entity = args[0];
if (!em.contains(entity)) { // reattach the entity if possible
final Class<?> entityClass = entity.getClass();
final EntityType<? extends Object> et = em.getMetamodel().entity(entityClass);
if (!et.hasSingleIdAttribute()) {
throw new IllegalArgumentException("Dynamic EJB doesn't manage IdClass yet");
}
SingularAttribute<?, ?> id = null; // = et.getId(entityClass); doesn't work with openJPA
for (SingularAttribute<?, ?> sa : et.getSingularAttributes()) {
if (sa.isId()) {
id = sa;
break;
}
}
if (id == null) {
throw new IllegalArgumentException("id field not found");
}
final String idName = id.getName();
Object idValue;
try {
idValue = BeanUtils.getProperty(entity, idName);;
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("can't invoke to get entity id");
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("can't find the method to get entity id");
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("can't access field/method to get entity id");
}
entity = em.getReference(et.getJavaType(), idValue);
if (entity == null) {
throw new IllegalArgumentException("entity " + entity + " is not managed and can't be found.");
}
}
em.remove(entity);
} else {
throw new IllegalArgumentException(REMOVE_NAME + " should have only one parameter and return void");
}
}
private Object merge(Object[] args, Class<?> returnType) {
if (args != null && args.length == 1 && returnType.equals(args[0].getClass())) {
return em.merge(args[0]);
} else {
throw new IllegalArgumentException(MERGE_NAME + " should have only one parameter and return the same" +
" type than the parameter type");
}
}
private void persist(Object[] args, Class<?> returnType) {
if (args != null && args.length == 1 && returnType.equals(Void.TYPE)) {
em.persist(args[0]);
} else {
throw new IllegalArgumentException(PERSIST_NAME + " should have only one parameter and return void");
}
}
private Class<?> getGenericType(Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
if ( pt.getActualTypeArguments().length == 1) {
return (Class<?>) pt.getActualTypeArguments()[0];
}
}
return Class.class.cast(type);
}
private <T> Query createFinderQuery(EntityManager entityManager, String methodName, Class<T> entityType, Object[] args) {
final List<String> conditions = parseMethodName(methodName);
final EntityType<T> et = entityManager.getMetamodel().entity(entityType);
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> query = cb.createQuery();
Root<T> from = query.from(entityType);
query = query.select(from);
int i = 0;
Predicate where = null;
for (String condition : conditions) {
SingularAttribute<? super T, ?> attribute = et.getSingularAttribute(condition);
Path<?> path = from.get(attribute);
Class<?> javaType = attribute.getType().getJavaType();
Predicate currentClause;
if (javaType.equals(String.class)) {
currentClause = cb.like((Expression<String>) path, (String) args[i++]);
} else if (Number.class.isAssignableFrom(javaType) || javaType.isPrimitive()) {
currentClause = cb.equal(path, args[i++]);
} else {
LOGGER.warning("field " + condition + " not found, ignoring");
continue;
}
if (where == null) {
where = currentClause;
} else {
where = cb.and(where, currentClause);
}
}
if (where != null) {
query = query.where(where);
}
// pagination
TypedQuery<?> emQuery = entityManager.createQuery(query);
if (args != null && args.length == conditions.size() + 2
&& isInt(args[args.length - 2].getClass()) && isInt(args[args.length - 1].getClass())) {
int first = (Integer) args[args.length - 2];
int max = (Integer) args[args.length - 1];
emQuery.setFirstResult(first);
emQuery.setMaxResults(max);
}
return emQuery;
}
private boolean isInt(Class<?> aClass) {
return Integer.TYPE.equals(aClass) || Integer.class.equals(aClass);
}
private List<String> parseMethodName(final String methodName) {
List<String> parsed;
if (CONDITIONS.containsKey(methodName)) {
parsed = CONDITIONS.get(methodName);
} else {
parsed = new ArrayList<String>();
String toParse = methodName.substring(FIND_PREFIX.length());
if (toParse.startsWith(BY)) {
toParse = toParse.substring(2);
String[] columns = toParse.split(AND);
for (String column: columns) {
parsed.add(StringUtils.uncapitalize(column));
}
}
CONDITIONS.put(methodName, parsed);
}
return parsed;
}
@Override public String toString() {
return "OpenEJB :: QueryProxy";
}
@Override public InvocationHandler getInvocationHandler() {
return this;
}
}