gr.abiss.calipso.tiers.specifications.GenericSpecifications.java Source code

Java tutorial

Introduction

Here is the source code for gr.abiss.calipso.tiers.specifications.GenericSpecifications.java

Source

/**
 * calipso-hub-framework - A full stack, high level framework for lazy application hackers.
 * Copyright  2005 Manos Batsis (manosbatsis gmail)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package gr.abiss.calipso.tiers.specifications;

import gr.abiss.calipso.tiers.annotation.CurrentPrincipal;
import gr.abiss.calipso.tiers.annotation.CurrentPrincipalField;
import gr.abiss.calipso.userDetails.util.SecurityUtil;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Transient;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Persistable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;

import com.fasterxml.jackson.annotation.JsonIgnore;

/**
 * A generic specifications class that builds predicates for any implementation
 * of org.springframework.data.domain.Persistable
 */
public class GenericSpecifications {

    private static final String SIMPLE_SEARCH_PARAM_NAME = "_all";

    static final Logger LOGGER = LoggerFactory.getLogger(GenericSpecifications.class);

    private static final HashMap<String, Field> FIELD_CACHE = new HashMap<String, Field>();
    private static final HashMap<Class, List<Field>> SIMPLE_SEARCH_FIELDs_CACHE = new HashMap<Class, List<Field>>();
    protected static final String SEARCH_MODE = "_searchmode";

    private static final StringPredicateFactory stringPredicateFactory = new StringPredicateFactory();
    private static final BooleanPredicateFactory booleanPredicateFactory = new BooleanPredicateFactory();
    private static final DatePredicateFactory datePredicateFactory = new DatePredicateFactory();

    private static final NumberPredicateFactory<Short> shortPredicateFactory = new NumberPredicateFactory<Short>(
            Short.class);
    private static final NumberPredicateFactory<Integer> integerPredicateFactory = new NumberPredicateFactory<Integer>(
            Integer.class);
    private static final NumberPredicateFactory<Long> longPredicateFactory = new NumberPredicateFactory<Long>(
            Long.class);
    private static final NumberPredicateFactory<Double> doublePredicateFactory = new NumberPredicateFactory<Double>(
            Double.class);

    private static final AnyToOnePredicateFactory anyToOnePredicateFactory = new AnyToOnePredicateFactory();
    private static final AnyToOneToOnePropertyPredicateFactory anyToOneToOnePredicateFactory = new AnyToOneToOnePropertyPredicateFactory();
    private static final CurrentPrincipalPredicateFactory currentPrincipalPredicateFactory = new CurrentPrincipalPredicateFactory();

    private static final HashMap<Class, IPredicateFactory> factoryForClassMap = new HashMap<Class, IPredicateFactory>();

    protected static final String OR = "OR";
    protected static final String AND = "AND";
    private static List<String> IGNORED_FIELD_NAMES;

    static {
        factoryForClassMap.put(String.class, stringPredicateFactory);
        factoryForClassMap.put(Boolean.class, booleanPredicateFactory);
        factoryForClassMap.put(Date.class, datePredicateFactory);

        factoryForClassMap.put(Short.class, shortPredicateFactory);
        factoryForClassMap.put(Integer.class, integerPredicateFactory);
        factoryForClassMap.put(Long.class, longPredicateFactory);
        factoryForClassMap.put(Double.class, doublePredicateFactory);

        // init ignore list
        String[] ignoredFieldNames = { "page", "direction", "properties", "size", "totalPages", "_searchmode",
                "_all", "totalElements" };
        IGNORED_FIELD_NAMES = new ArrayList<String>(ignoredFieldNames.length);
        for (int i = 0; i < ignoredFieldNames.length; i++) {
            IGNORED_FIELD_NAMES.add(ignoredFieldNames[i]);
        }
    }

    /**
     * Get an appropriate predicate factory for the given field class
     * @param field
     * @return
     */
    private static IPredicateFactory<?> getPredicateFactoryForClass(Field field) {
        Class clazz = field.getType();
        if (clazz.isEnum()) {
            return new EnumStringPredicateFactory(clazz);
        } else if (Persistable.class.isAssignableFrom(clazz)) {
            if (field.isAnnotationPresent(CurrentPrincipal.class)) {
                return currentPrincipalPredicateFactory;
            } else {
                return anyToOnePredicateFactory;
            }
        } else {
            return factoryForClassMap.get(clazz);
        }
    }

    /**
     * Get a (cached) field for the given class' member name
     * @param clazz
     * @param fieldName the member name
     * @return
     */
    public static Field getField(Class<?> clazz, String fieldName) {
        Field field = null;
        if (!IGNORED_FIELD_NAMES.contains(fieldName)) {

            String key = clazz.getName() + "#" + fieldName;
            field = FIELD_CACHE.get(key);

            // find it if not cached
            if (field == null && !FIELD_CACHE.containsKey(key)) {
                Class<?> tmpClass = clazz;
                do {
                    for (Field tmpField : tmpClass.getDeclaredFields()) {
                        String candidateName = tmpField.getName();
                        if (candidateName.equals(fieldName)) {
                            // field.setAccessible(true);
                            FIELD_CACHE.put(key, tmpField);
                            field = tmpField;
                            break;
                        }
                    }
                    tmpClass = tmpClass.getSuperclass();
                } while (tmpClass != null && field == null);
            }
            if (field == null) {
                LOGGER.warn("Field '" + fieldName + "' not found on class " + clazz);
                // HashMap handles null values so we can use containsKey to cach
                // invalid fields and hence skip the reflection scan
                FIELD_CACHE.put(key, null);
            }
            // throw new RuntimeException("Field '" + fieldName +
            // "' not found on class " + clazz);
        }

        return field;
    }

    /**
     * Dynamically create a specification for the given class and search
     * parameters. This is the entry point for query specifications construction.
     * @param clazz the entity type to query for
     * @param searchTerms the search terms to match
     * @return the result specification
     */
    @SuppressWarnings("rawtypes")
    public static Specification<Persistable> matchAll(final Class clazz, final Map<String, String[]> searchTerms) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("matchAll, entity: " + clazz.getSimpleName() + ", searchTerms: " + searchTerms);
        }
        return new Specification<Persistable>() {
            @Override
            public Predicate toPredicate(@SuppressWarnings("rawtypes") Root<Persistable> root,
                    CriteriaQuery<?> query, CriteriaBuilder cb) {
                return GenericSpecifications.buildRootPredicate(clazz, searchTerms, root, cb);
            }
        };
    }

    /**
     * Get the root predicate, either a conjunction or disjunction
     * @param clazz the entity type to query for
     * @param searchTerms the search terms to match
     * @param root the criteria root
     * @param cb the criteria builder
     * @return the resulting predicate
     */
    protected static Predicate buildRootPredicate(final Class clazz, final Map<String, String[]> searchTerms,
            Root<Persistable> root, CriteriaBuilder cb) {

        // build a list of criteria/predicates
        LinkedList<Predicate> predicates = buildSearchPredicates(clazz, searchTerms, root, cb);

        // wrap list in AND/OR junction
        Predicate predicate;
        if (searchTerms.containsKey(SEARCH_MODE) && searchTerms.get(SEARCH_MODE)[0].equalsIgnoreCase(OR)
        // A disjunction of zero predicates is false so...
                && predicates.size() > 0) {
            predicate = cb.or(predicates.toArray(new Predicate[predicates.size()]));
        } else {
            predicate = cb.and(predicates.toArray(new Predicate[predicates.size()]));
        }

        // return the resulting junction
        return predicate;
    }

    /**
     * Build the list of predicates corresponding to the given search terms
     * @param clazz the entity type to query for
     * @param searchTerms the search terms to match
     * @param root the criteria root
     * @param cb the criteria builder
     * @return the list of predicates corresponding to the search terms
     */
    protected static LinkedList<Predicate> buildSearchPredicates(final Class clazz,
            final Map<String, String[]> searchTerms, Root<Persistable> root, CriteriaBuilder cb) {

        LinkedList<Predicate> predicates = new LinkedList<Predicate>();

        if (!CollectionUtils.isEmpty(searchTerms)) {
            Set<String> propertyNames = searchTerms.keySet();
            // storage for nested junctions
            NestedJunctions junctions = new NestedJunctions();
            for (String propertyName : propertyNames) {
                String[] values = searchTerms.get(propertyName);
                // store if nested junction or add a predicate
                if (!junctions.addIfNestedJunction(propertyName, values)) {
                    addPredicate(clazz, root, cb, predicates, values, propertyName);
                }
            }
            // add stored junctions
            Map<String, Map<String, String[]>> andJunctions = junctions.getAndJunctions();
            addNestedJunctionPredicates(clazz, root, cb, predicates, andJunctions, AND);
            Map<String, Map<String, String[]>> orJunctions = junctions.getOrJunctions();
            addNestedJunctionPredicates(clazz, root, cb, predicates, orJunctions, OR);
        }
        // return the list of predicates
        return predicates;
    }

    /**
     * Add a predicate to the given list if valid
     * @param clazz the entity type to query for
     * @param root the criteria root
     * @param cb the criteria builder
     * @param predicates the list to add the predicate into
     * @param propertyValues the predicate values
     * @param propertyName the predicate name
     */
    protected static void addPredicate(final Class clazz, Root<Persistable> root, CriteriaBuilder cb,
            LinkedList<Predicate> predicates, String[] propertyValues, String propertyName) {
        // dot notation only supports toOne.toOne.id
        if (propertyName.contains(".")) {
            predicates
                    .add(anyToOneToOnePredicateFactory.getPredicate(root, cb, propertyName, null, propertyValues));
        } else {// normal single step predicate
            Field field = GenericSpecifications.getField(clazz, propertyName);
            if (field != null) {
                Class fieldType = field.getType();
                IPredicateFactory predicateFactory = getPredicateFactoryForClass(field);
                if (predicateFactory != null) {
                    predicates
                            .add(predicateFactory.getPredicate(root, cb, propertyName, fieldType, propertyValues));
                }
            }
        }
    }

    //TODO refactor nested junctions and add operators
    protected static void addNestedJunctionPredicates(final Class clazz, Root<Persistable> root, CriteriaBuilder cb,
            LinkedList<Predicate> predicates, Map<String, Map<String, String[]>> andJunctions, String mode) {
        if (!CollectionUtils.isEmpty(andJunctions)) {
            String[] searchMode = { mode };
            for (Map<String, String[]> params : andJunctions.values()) {
                params.put(SEARCH_MODE, searchMode);
                // TODO
                Predicate nestedPredicate = buildRootPredicate(clazz, params, root, cb/*, true*/);
                if (nestedPredicate != null) {
                    predicates.add(nestedPredicate);
                }
            }
        }
    }
}