com.fiveamsolutions.nci.commons.search.SearchCallback.java Source code

Java tutorial

Introduction

Here is the source code for com.fiveamsolutions.nci.commons.search.SearchCallback.java

Source

/**
 * The software subject to this notice and license includes both human readable
 * source code form and machine readable, binary, object code form. The COPPA PO
 * Software was developed in conjunction with the National Cancer Institute
 * (NCI) by NCI employees and 5AM Solutions, Inc. (5AM). To the extent
 * government employees are authors, any rights in such works shall be subject
 * to Title 17 of the United States Code, section 105.
 *
 * This COPPA PO Software License (the License) is between NCI and You. You (or
 * Your) shall mean a person or an entity, and all other entities that control,
 * are controlled by, or are under common control with the entity. Control for
 * purposes of this definition means (i) the direct or indirect power to cause
 * the direction or management of such entity, whether by contract or otherwise,
 * or (ii) ownership of fifty percent (50%) or more of the outstanding shares,
 * or (iii) beneficial ownership of such entity.
 *
 * This License is granted provided that You agree to the conditions described
 * below. NCI grants You a non-exclusive, worldwide, perpetual, fully-paid-up,
 * no-charge, irrevocable, transferable and royalty-free right and license in
 * its rights in the COPPA PO Software to (i) use, install, access, operate,
 * execute, copy, modify, translate, market, publicly display, publicly perform,
 * and prepare derivative works of the COPPA PO Software; (ii) distribute and
 * have distributed to and by third parties the COPPA PO Software and any
 * modifications and derivative works thereof; and (iii) sublicense the
 * foregoing rights set out in (i) and (ii) to third parties, including the
 * right to license such rights to further third parties. For sake of clarity,
 * and not by way of limitation, NCI shall have no right of accounting or right
 * of payment from You or Your sub-licensees for the rights granted under this
 * License. This License is granted at no charge to You.
 *
 * Your redistributions of the source code for the Software must retain the
 * above copyright notice, this list of conditions and the disclaimer and
 * limitation of liability of Article 6, below. Your redistributions in object
 * code form must reproduce the above copyright notice, this list of conditions
 * and the disclaimer of Article 6 in the documentation and/or other materials
 * provided with the distribution, if any.
 *
 * Your end-user documentation included with the redistribution, if any, must
 * include the following acknowledgment: This product includes software
 * developed by 5AM and the National Cancer Institute. If You do not include
 * such end-user documentation, You shall include this acknowledgment in the
 * Software itself, wherever such third-party acknowledgments normally appear.
 *
 * You may not use the names "The National Cancer Institute", "NCI", or "5AM"
 * to endorse or promote products derived from this Software. This License does
 * not authorize You to use any trademarks, service marks, trade names, logos or
 * product names of either NCI or 5AM, except as required to comply with the
 * terms of this License.
 *
 * For sake of clarity, and not by way of limitation, You may incorporate this
 * Software into Your proprietary programs and into any third party proprietary
 * programs. However, if You incorporate the Software into third party
 * proprietary programs, You agree that You are solely responsible for obtaining
 * any permission from such third parties required to incorporate the Software
 * into such third party proprietary programs and for informing Your
 * sub-licensees, including without limitation Your end-users, of their
 * obligation to secure any required permissions from such third parties before
 * incorporating the Software into such third party proprietary software
 * programs. In the event that You fail to obtain such permissions, You agree
 * to indemnify NCI for any claims against NCI by such third parties, except to
 * the extent prohibited by law, resulting from Your failure to obtain such
 * permissions.
 *
 * For sake of clarity, and not by way of limitation, You may add Your own
 * copyright statement to Your modifications and to the derivative works, and
 * You may provide additional or different license terms and conditions in Your
 * sublicenses of modifications of the Software, or any derivative works of the
 * Software as a whole, provided Your use, reproduction, and distribution of the
 * Work otherwise complies with the conditions stated in this License.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO
 * EVENT SHALL THE NATIONAL CANCER INSTITUTE, 5AM SOLUTIONS, INC. OR THEIR
 * AFFILIATES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.fiveamsolutions.nci.commons.search;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import com.fiveamsolutions.nci.commons.data.persistent.PersistentObject;
import com.fiveamsolutions.nci.commons.service.GenericSearchService;

/**
 * Callback implementation.
 */
@SuppressWarnings({ "PMD.AvoidStringBufferField", "PMD.TooManyMethods", "PMD.ExcessiveParameterList",
        "PMD.ExcessiveClassLength" })
class SearchCallback extends AbstractQueryGenerationCallback implements SearchableUtils.AnnotationCallback {

    private final StringBuffer selectClause;
    // use == for comparisons, rather than equals()
    private final Map<Object, Object> nestedHistory = new IdentityHashMap<Object, Object>();
    // we need to maintain both uniqueness (only adding a join clause once) but also ordering (so that top-level
    // joins are done first), so use a LinkedHashSet instead of a normal HashSet
    private final Set<String> nestedJoinClauses = new LinkedHashSet<String>();
    private final Map<String, NestedSearchField> nestedSearchFields = new HashMap<String, NestedSearchField>();

    /**
     * @param whereClause string buffer containing the where clause
     * @param selectClause string buffer containing the select clause
     * @param params map of query parameter name to parameter value
     */
    public SearchCallback(StringBuffer whereClause, StringBuffer selectClause, Map<String, Object> params) {
        super(whereClause, params);
        this.selectClause = selectClause;
    }

    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
    public void callback(Method m, Object result, String objAlias) throws Exception {
        this.objectAlias = objAlias;
        String fieldName = StringUtils.uncapitalize(m.getName().substring("get".length()));
        String paramName = objectAlias + "_" + fieldName;
        if (result == null) {
            return;
        }
        SearchOptions searchOptions = new SearchOptions(m);
        validateSettings(searchOptions, result);

        if (result instanceof Collection<?>) {
            processCollectionField((Collection<?>) result, fieldName, this.objectAlias, searchOptions);
        } else if (!ArrayUtils.isEmpty(searchOptions.getFields())) {
            processFieldWithSubProp(result, fieldName, paramName, searchOptions, false);
        } else if (searchOptions.isNested()) {
            processNestedField(result, fieldName, paramName, searchOptions);
        } else {
            processSimpleField(result, fieldName, paramName, searchOptions);
        }
    }

    private void validateSettings(SearchOptions searchOptions, Object result) {

        boolean isCollection = result instanceof Collection<?>;

        boolean isPersistentObject = result instanceof PersistentObject;

        if (!ArrayUtils.isEmpty(searchOptions.getFields()) && searchOptions.isNested()) {
            throw new IllegalArgumentException("Both fields and nested cannot be set at the same time.");
        }

        validateIsHibernateComponentSetting(searchOptions.isHibernateComponent(),
                ArrayUtils.isEmpty(searchOptions.getFields()), isCollection, isPersistentObject);

    }

    private void validateIsHibernateComponentSetting(boolean isHibernateComponent, boolean isFieldsEmpty,
            boolean isCollection, boolean isPersistentObject) {
        if (isHibernateComponent && (isFieldsEmpty || isCollection || isPersistentObject)) {
            throw new IllegalArgumentException(
                    "isHibernateComponent may only be set on a non-collection and non-PersistentObject,"
                            + " and fields must be provided.");
        }
    }

    private void processNestedField(Object result, String fieldName, String paramName,
            SearchOptions searchOptions) {
        if (!nestedHistory.containsKey(result)) {
            if (result instanceof PersistentObject && ((PersistentObject) result).getId() != null) {
                // don't update nestedHistory here, searching only on ID doesn't allow for infinite loops
                // but does allow for the same PersistentObject to be used repeatedly in a search
                searchOptions.setFields(new String[] { "id" });
                processFieldWithSubProp(result, fieldName, paramName, searchOptions, false);
            } else {
                // update nestedHistory here to prevent infinite loops
                nestedHistory.put(result, result);

                String objAlias = objectAlias + "_" + fieldName;
                if (!searchOptions.isHibernateComponent()) {
                    String joinClause = String.format(" join %s.%s %s_%s ", this.objectAlias, fieldName,
                            this.objectAlias, fieldName);
                    selectClause.append(joinClause);
                } else {
                    objAlias = objectAlias + "." + fieldName;
                }

                SearchableUtils.iterateAnnotatedMethods(result, this, objAlias);
            }
        }
    }

    private void processFieldWithSubProp(Object result, String fieldName, String paramName,
            SearchOptions searchOptions, boolean inNestedCollection) {
        for (String currentProp : searchOptions.getFields()) {
            String subPropParamName = paramName + "_" + currentProp;
            Object subPropResult = getSimpleProperty(result, currentProp);

            if (subPropResult != null) {
                if (isStringAndBlank(subPropResult)) {
                    continue;
                }
                if (inNestedCollection) {
                    handleNestedFieldInCollection(subPropParamName, paramName, currentProp, searchOptions,
                            subPropResult);
                } else {
                    handleProcessFieldWithSubProp(subPropResult, fieldName, searchOptions, currentProp,
                            subPropParamName);
                }

            }
        }
    }

    private Object getSimpleProperty(Object bean, String property) {
        Object subPropResult = null;
        try {
            subPropResult = PropertyUtils.getSimpleProperty(bean, property);
        } catch (Exception e) {
            throw new IllegalArgumentException("Unable to process property with name:" + property, e);
        }
        return subPropResult;
    }

    private void handleProcessFieldWithSubProp(Object subPropResult, String fieldName, SearchOptions searchOptions,
            String currentProp, String subPropParamName) {
        whereClause.append(whereOrAnd);
        whereOrAnd = SearchableUtils.AND;
        String hqlField = String.format("%s.%s.%s", this.objectAlias, fieldName, currentProp);
        whereClause.append(determineModeMatchAndCase(subPropResult, subPropParamName,
                searchOptions.getSearchMethod(), searchOptions.isCaseSensitive(), hqlField));
    }

    private void processCollectionField(Collection<?> col, String fieldName, String baseObjectAlias,
            SearchOptions searchOptions)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        validateCollection(col, searchOptions);

        if (col.isEmpty()) {
            // empty collection of values means no search criteria.
            return;
        }

        String collectionTableAlias = baseObjectAlias + "_" + fieldName;
        // join clause is "<alias>.<field> <alias>_<field>"
        String collectionTableJoin = String.format("%s.%s %s", baseObjectAlias, fieldName, collectionTableAlias);

        Object firstCollectionObject = col.iterator().next();

        if (searchOptions.isNested()) {
            nestedJoinClauses.add(collectionTableJoin);
            List<Method> methods = SearchableUtils.getSearchableMethods(firstCollectionObject);
            for (Method m : methods) {
                for (Object collectionObject : col) {
                    Object result = m.invoke(collectionObject);
                    dispatchCollectionNestedField(collectionTableAlias, m, result);
                }
            }

            updateQueryClausesForCollection();

        } else {
            Class<? extends Object> fieldClass = firstCollectionObject.getClass();
            StringBuffer processCollectionPropertiesResult = processCollectionProperties(collectionTableAlias, col,
                    fieldClass, searchOptions);
            if (processCollectionPropertiesResult.length() > 0) {
                // add " join obj.<field> obj_<field>" to select clause
                selectClause
                        .append(String.format(" join %s.%s %s", baseObjectAlias, fieldName, collectionTableAlias));

                // now add the where clauses
                whereClause.append(whereOrAnd);
                whereOrAnd = SearchableUtils.AND;
                whereClause.append(" ( ");
                whereClause.append(processCollectionPropertiesResult);
                whereClause.append(" ) ");
            }
        }
    }

    private void updateQueryClausesForCollection() {
        if (!nestedSearchFields.isEmpty()) {
            for (String joinClause : nestedJoinClauses) {
                selectClause.append(" join ").append(joinClause);
            }
            nestedJoinClauses.clear();

            whereClause.append(whereOrAnd);
            whereOrAnd = SearchableUtils.AND;
            whereClause.append(" ( ");
            String andClause = "";
            for (NestedSearchField nsf : nestedSearchFields.values()) {
                whereClause.append(buildCollectionPropWhereClause(nsf.getContainingTableAlias(), nsf.getMatchMode(),
                        nsf.isCaseSensitive(), andClause, nsf.getFieldName(), nsf.getValues()));
                andClause = SearchableUtils.AND;
            }
            nestedSearchFields.clear();
            whereClause.append(" ) ");
        } else {
            nestedJoinClauses.clear();
        }
    }

    private void dispatchCollectionNestedField(String baseParamName, Method m, Object result)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        String fieldName = StringUtils.uncapitalize(m.getName().substring("get".length()));
        String paramName = baseParamName + "_" + fieldName;
        SearchOptions searchOptions = new SearchOptions(m);
        validateSettings(searchOptions, result);

        if (result != null) {
            if (result instanceof Collection<?>) {
                processCollectionField((Collection<?>) result, fieldName, baseParamName, searchOptions);
            } else if (!ArrayUtils.isEmpty(searchOptions.getFields())) {
                if (!searchOptions.isHibernateComponent()) {
                    nestedJoinClauses.add(String.format("%s.%s %s", baseParamName, fieldName, paramName));
                } else {
                    paramName = String.format("%s.%s", baseParamName, fieldName);
                }
                processFieldWithSubProp(result, fieldName, paramName, searchOptions, true);
            } else if (searchOptions.isNested()) {
                handleNestedCollectionNestedField(baseParamName, result, fieldName, paramName, searchOptions);
            } else {
                // just a simple field
                handleNestedFieldInCollection(paramName, baseParamName, fieldName, searchOptions, result);
            }
        }
    }

    private void handleNestedCollectionNestedField(String baseParamName, Object result, String fieldName,
            String paramName, SearchOptions searchOptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        if (!nestedHistory.containsKey(result)) {

            String paramNameToUse = paramName;
            if (!searchOptions.isHibernateComponent()) {
                nestedJoinClauses.add(String.format("%s.%s %s", baseParamName, fieldName, paramName));
            } else {
                paramNameToUse = String.format("%s_%s", baseParamName, fieldName);
            }
            if (result instanceof PersistentObject && ((PersistentObject) result).getId() != null) {
                // don't update nestedHistory here, searching only on ID doesn't allow for infinite loops
                // but does allow for the same PersistentObject to be used repeatedly in a search
                searchOptions.setFields(new String[] { "id" });
                processFieldWithSubProp(result, fieldName, paramNameToUse, searchOptions, true);
            } else {
                // update nestedHistory here to prevent infinite loops
                nestedHistory.put(result, result);
                List<Method> nestedMethods = SearchableUtils.getSearchableMethods(result);
                for (Method nestedMethod : nestedMethods) {
                    Object nestedResult = nestedMethod.invoke(result);
                    dispatchCollectionNestedField(paramNameToUse, nestedMethod, nestedResult);
                }
            }
        }

    }

    private void handleNestedFieldInCollection(String nestedParamName, String collectionTableAlias,
            String fieldName, SearchOptions searchOptions, Object result) {
        if (result != null && !isStringAndBlank(result)) {
            NestedSearchField nsf = nestedSearchFields.get(nestedParamName);
            if (nsf == null) {
                nsf = new NestedSearchField(collectionTableAlias, fieldName, searchOptions.getSearchMethod(),
                        searchOptions.isCaseSensitive());
                nestedSearchFields.put(nestedParamName, nsf);
            }
            nsf.addValue(result);
        }
    }

    private void validateCollection(Collection<?> col, SearchOptions searchOptions) {
        if (ArrayUtils.isEmpty(searchOptions.getFields()) && !searchOptions.isNested()) {
            throw new IllegalArgumentException("Cannot use the searchable annotation on a collection without"
                    + " specifying at least one field name or nested.");
        }

        if (col.size() > GenericSearchService.MAX_IN_CLAUSE_SIZE) {
            throw new IllegalArgumentException(String.format("Cannot query on more than %s elements.",
                    GenericSearchService.MAX_IN_CLAUSE_SIZE));
        }
    }

    private StringBuffer processCollectionProperties(String collectionTableAlias, Collection<?> col,
            Class<? extends Object> fieldClass, SearchOptions searchOptions)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        StringBuffer result = new StringBuffer();
        String andClause = "";
        for (String propName : searchOptions.getFields()) {
            boolean sensitive = searchOptions.isCaseSensitive();
            Collection<Object> values = getNonNullValues(col, fieldClass, propName, sensitive);

            if (!values.isEmpty()) {
                String searchMethod = searchOptions.getSearchMethod();
                result.append(buildCollectionPropWhereClause(collectionTableAlias, searchMethod, sensitive,
                        andClause, propName, values));
                andClause = SearchableUtils.AND;
            }
        }
        return result;
    }

    private StringBuffer buildCollectionPropWhereClause(String collectionTableAlias, String searchMethod,
            boolean sensitive, String andClause, String propName, Collection<Object> values) {
        boolean exactSearch = searchMethod.equals(Searchable.MATCH_MODE_EXACT);
        // exchange all dots for underscores
        StringBuffer result = new StringBuffer();
        result.append(andClause);
        if (!exactSearch) {
            result.append(
                    addLikeClausesForCollectionProp(collectionTableAlias, propName, values, searchMethod, sensitive)
                            .toString());
        } else {
            result.append(
                    addInClauseForCollectionProp(collectionTableAlias, propName, values, sensitive).toString());
        }
        return result;
    }

    /**
     * Extracts the non-null values from the collection and adjusts them for case insensitivity, if necessary.
     */
    private Collection<Object> getNonNullValues(Collection<?> col, Class<? extends Object> fieldClass,
            String propName, boolean caseSenstive)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Method m2 = fieldClass.getMethod("get" + StringUtils.capitalize(propName));
        Collection<Object> valueCollection = new HashSet<Object>();
        for (Object collectionObj : col) {
            Object val = m2.invoke(collectionObj);
            if (val != null && !isStringAndBlank(val)) {
                if (canBeUsedInLikeExpression(val) && !caseSenstive) {
                    String strValue = (String) val;
                    valueCollection.add(strValue.toLowerCase(Locale.getDefault()));
                } else {
                    valueCollection.add(val);
                }
            }
        }
        return valueCollection;
    }

    private StringBuffer addLikeClausesForCollectionProp(String collectionTableAlias, String propName,
            Collection<Object> valueCollection, String searchMethod, boolean sensitive) {
        StringBuffer result = new StringBuffer();
        result.append(" (");
        int i = 0;
        for (Object val : valueCollection) {
            if (i != 0) {
                result.append(" " + SearchableUtils.OR + " ");
            }

            String subParamName = new String(collectionTableAlias + "_" + propName + "_" + i).trim().replace('.',
                    '_');

            String hqlField = collectionTableAlias + "." + propName;
            result.append(determineModeMatchAndCase(val, subParamName, searchMethod, sensitive, hqlField));

            i++;
        }
        result.append(") ");
        return result;
    }

    private StringBuffer addInClauseForCollectionProp(String collectionTableAlias, String propName,
            Collection<Object> valueCollection, boolean caseSensitive) {
        String hqlField = collectionTableAlias + "." + propName;
        boolean likeable = canBeUsedInLikeExpression(valueCollection.iterator().next());

        StringBuffer result = new StringBuffer();
        result.append(determineSensitivity(hqlField, caseSensitive, likeable));

        String subParamName = new String(collectionTableAlias + "_" + propName).trim().replace('.', '_');
        result.append(String.format(" in (:%s)", subParamName));

        params.put(subParamName, valueCollection);
        return result;
    }

    public Object getSavedState() {
        return whereOrAnd;
    }

    /**
     * Helper class to hold the information about a search field.
     * @author Steve Lustbader
     */
    private class NestedSearchField {
        // the HQL alias for the table containing this field; eg, "obj_researchOrganizations"
        private final String containingTableAlias;
        // the field name for this field; eg, "name."  Can be combined with the containingTableAlias to form either
        // join clauses ("join obj_researchOrganizations.addresses obj_researchOrganizations_addresses") or bind
        // parameter names (":obj_researchOrganizations_name")
        private final String fieldName;
        private final String matchMode;
        private final boolean caseSensitive;
        private final Set<Object> values = new HashSet<Object>();

        /**
         * Constructor.
         * @param containingTableAlias HQL alias for the table containing this field; eg, "obj_researchOrganizations"
         * @param fieldName the field name for this field; eg, "name"
         * @param matchMode match mode for this field
         * @param caseSensitive case sensitivity of this field
         */
        public NestedSearchField(String containingTableAlias, String fieldName, String matchMode,
                boolean caseSensitive) {
            this.containingTableAlias = containingTableAlias;
            this.fieldName = fieldName;
            this.matchMode = matchMode;
            this.caseSensitive = caseSensitive;
        }

        /**
         * @return the matchMode
         */
        public String getMatchMode() {
            return matchMode;
        }

        /**
         * @return the caseSensitive
         */
        public boolean isCaseSensitive() {
            return caseSensitive;
        }

        /**
         * @return the values
         */
        public Set<Object> getValues() {
            return values;
        }

        /**
         * @param value
         */
        public void addValue(Object value) {
            if (canBeUsedInLikeExpression(value) && !caseSensitive) {
                values.add(((String) value).toLowerCase(Locale.getDefault()));
            } else {
                values.add(value);
            }
        }

        /**
         * @return the containingTableAlias
         */
        public String getContainingTableAlias() {
            return containingTableAlias;
        }

        /**
         * @return the fieldName
         */
        public String getFieldName() {
            return fieldName;
        }
    }
}