All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.openejb.util.proxy.QueryProxy Maven / Gradle / Ivy

There is a newer version: 4.7.5
Show newest version
/*
 * 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> RETURN_TYPES = new ConcurrentHashMap>();
    private final Map> CONDITIONS = new ConcurrentHashMap>();

    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) 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 entry : ((Map) 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:" +
                        "  query(String name, Map 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 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  Query createFinderQuery(EntityManager entityManager, String methodName, Class entityType, Object[] args) {
        final List conditions = parseMethodName(methodName);

        final EntityType et = entityManager.getMetamodel().entity(entityType);
        final CriteriaBuilder cb = entityManager.getCriteriaBuilder();

        CriteriaQuery query = cb.createQuery();
        Root from = query.from(entityType);
        query = query.select(from);

        int i = 0;
        Predicate where = null;
        for (String condition : conditions) {
            SingularAttribute 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) 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 parseMethodName(final String methodName) {
        List parsed;
        if (CONDITIONS.containsKey(methodName)) {
            parsed = CONDITIONS.get(methodName);
        } else {
            parsed = new ArrayList();

            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;
    }
}