org.jaffa.qm.util.PropertyFilter.java Source code

Java tutorial

Introduction

Here is the source code for org.jaffa.qm.util.PropertyFilter.java

Source

/*
 * ====================================================================
 * JAFFA - Java Application Framework For All
 *
 * Copyright (C) 2002 JAFFA Development Group
 *
 *     This library 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 2.1 of the License, or (at your option) any later version.
 *
 *     This library 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 library; if not, write to the Free Software
 *     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Redistribution and use of this software and associated documentation ("Software"),
 * with or without modification, are permitted provided that the following conditions are met:
 * 1.   Redistributions of source code must retain copyright statements and notices.
 *         Redistributions must also contain a copy of this document.
 * 2.   Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3.   The name "JAFFA" must not be used to endorse or promote products derived from
 *    this Software without prior written permission. For written permission,
 *    please contact mail to: jaffagroup@yahoo.com.
 * 4.   Products derived from this Software may not be called "JAFFA" nor may "JAFFA"
 *    appear in their names without prior written permission.
 * 5.   Due credit should be given to the JAFFA Project (http://jaffa.sourceforge.net).
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS 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 org.jaffa.qm.util;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.log4j.Logger;
import org.jaffa.datatypes.IDateBase;
import org.jaffa.util.StringHelper;

/** 
 * This class creates an instance of a filter for a given class, based on a supplied set of mapping rules.
 */
public class PropertyFilter {

    private static Logger log = Logger.getLogger(PropertyFilter.class);
    // Cache of PropertyFilter instances per resultGraphRules[] array for a class
    private static final Map<String, Map<String[], PropertyFilter>> c_propertyFilterCache = new ReferenceMap();
    private String[] m_rules;
    private List<String> m_filteredFields;
    private boolean[] m_filterTypes;
    private Pattern[] m_filterPatterns;

    /**
     * Fetches (or creates if it does not exist) the PropertyFilter instance for the given input.
     */
    public static PropertyFilter getInstance(Class clazz) throws IntrospectionException {
        return getInstance(clazz, null);
    }

    /**
     * Fetches (or creates if it does not exist) the PropertyFilter instance for the given input.
     */
    public static PropertyFilter getInstance(Class clazz, String[] rules) throws IntrospectionException {
        if (rules == null || rules.length == 0)
            rules = new String[] { "*" };
        Map<String[], PropertyFilter> m = c_propertyFilterCache.get(clazz.getName());
        if (m == null) {
            synchronized (c_propertyFilterCache) {
                m = c_propertyFilterCache.get(clazz.getName());
                if (m == null) {
                    m = new ReferenceMap();
                    c_propertyFilterCache.put(clazz.getName(), m);
                    if (log.isInfoEnabled())
                        log.info("Created cache for " + clazz.getName());
                } else {
                    if (log.isDebugEnabled())
                        log.debug("Cache found for " + clazz.getName());
                }
            }
        } else {
            if (log.isDebugEnabled())
                log.debug("Cache found for " + clazz.getName());
        }

        PropertyFilter propertyFilter = get(m, rules);
        if (propertyFilter == null) {
            synchronized (m) {
                propertyFilter = get(m, rules);
                if (propertyFilter == null) {
                    propertyFilter = new PropertyFilter(clazz, rules);
                    m.put(rules, propertyFilter);
                    if (log.isInfoEnabled())
                        log.info("Created cache entry for " + clazz.getName() + " / " + Arrays.toString(rules));
                } else {
                    if (log.isDebugEnabled())
                        log.debug("Cached entry found for " + clazz.getName() + " / " + Arrays.toString(rules));
                }
            }
        } else {
            if (log.isDebugEnabled())
                log.debug("Cached entry found for " + clazz.getName() + " / " + Arrays.toString(rules));
        }
        return propertyFilter;
    }

    /**
     * A convenience method that utilizes the Arrays class to locate an entry in
     * the input Map, that has the same key as the input array.
     */
    private static PropertyFilter get(Map<String[], PropertyFilter> m, String[] a) {
        Collection<Map.Entry<String[], PropertyFilter>> entries = new HashSet<Map.Entry<String[], PropertyFilter>>(
                m.entrySet());
        for (Map.Entry<String[], PropertyFilter> entry : entries) {
            if (Arrays.equals(entry.getKey(), a))
                return entry.getValue();
        }
        return null;
    }

    /**
     * Creates a new instance of PropertyFilter.
     */
    private PropertyFilter(Class clazz, String[] rules) throws IntrospectionException {
        calculateFilter(clazz, rules);
    }

    /** Getter for property rules.
     * @return Value of property rules.
     */
    public String[] getRules() {
        return m_rules;
    }

    /** Returns true if the field is included based on the rules.
     * @param field the field
     * @return true if the field is included based on the rules.
     */
    public boolean isFieldIncluded(String field) {
        return m_filteredFields != null && Collections.binarySearch(m_filteredFields, field) >= 0;
    }

    /** Returns a list of fields to be included based on the rules.
     * @return a list of fields to be included based on the rules.
     */
    public String[] getIncludedFields() {
        return m_filteredFields != null ? m_filteredFields.toArray(new String[m_filteredFields.size()]) : null;
    }

    /** Returns true if the list of included fields contains the fields for given related object.
     * @param fieldPrefix the prefix which identifies a related object.
     * @return true if the list of included fields contains the fields for given related object.
     */
    public boolean areSubFieldsIncluded(String fieldPrefix) {
        return areSubFieldsIncluded(m_filteredFields, fieldPrefix);
    }

    /** Returns true if the list of included fields contains the fields for given related object.
     * @param filteredFields the list of included fields.
     * @param fieldPrefix the prefix which identifies a related object.
     * @return true if the list of included fields contains the fields for given related object.
     */
    private static boolean areSubFieldsIncluded(List<String> filteredFields, String fieldPrefix) {
        if (filteredFields != null) {
            int length = fieldPrefix.length();
            char startChar = fieldPrefix.charAt(0);
            char endChar = fieldPrefix.charAt(length - 1);
            for (String field : filteredFields) {
                if (field.length() > length && field.charAt(0) == startChar && field.charAt(length - 1) == endChar
                        && field.startsWith(fieldPrefix))
                    return true;
            }
        }
        return false;
    }

    /** Get a list of all the possible objects that can be addressed in this domain object graph.
     * @param gdo the Class for a graph data object.
     * @return a list of all possible objects that can be addressed in this domain object graph.
     */
    private static List<String> getFieldList(Class clazz) throws IntrospectionException {
        List<String> fieldList = new ArrayList<String>();
        getFieldList(clazz, fieldList, null, new LinkedList<Class>());
        Collections.sort(fieldList);
        return fieldList;
    }

    private static void getFieldList(Class clazz, List<String> fieldList, String prefix, Deque<Class> classStack)
            throws IntrospectionException {
        //To avoid recursion, bail out if the input Class has already been introspected
        if (classStack.contains(clazz)) {
            if (log.isDebugEnabled())
                log.debug("Fields from " + clazz + " prefixed by " + prefix
                        + " will be ignored, since the class has already been introspected as per the stack "
                        + classStack);
            return;
        } else
            classStack.addFirst(clazz);

        //Introspect the input Class
        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
        if (beanInfo != null) {
            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
            if (pds != null) {
                for (PropertyDescriptor pd : pds) {
                    if (pd.getReadMethod() != null && pd.getWriteMethod() != null) {
                        String name = pd.getName();
                        String qualifieldName = prefix == null || prefix.length() == 0 ? name : prefix + '.' + name;
                        Class type = pd.getPropertyType();
                        if (type.isArray())
                            type = type.getComponentType();
                        if (type == String.class || type == Boolean.class || Number.class.isAssignableFrom(type)
                                || IDateBase.class.isAssignableFrom(type) || Currency.class.isAssignableFrom(type)
                                || type.isPrimitive() || type.isEnum())
                            fieldList.add(qualifieldName);
                        else
                            getFieldList(type, fieldList, qualifieldName, classStack);
                    }
                }
            }
        }

        classStack.removeFirst();
    }

    private void calculateFilter(Class clazz, String[] rules) throws IntrospectionException {
        m_rules = rules != null && rules.length > 0 ? rules : new String[] { "*" };
        List<String> filteredFields = new ArrayList<String>();
        List<String> relToCheck = new LinkedList<String>();

        // Convert rules to RegEx and cache...
        m_filterTypes = new boolean[m_rules.length + 1];
        m_filterPatterns = new Pattern[m_rules.length + 1];
        for (int i = 0; i < m_rules.length; i++) {
            String filter = m_rules[i];
            Pattern p = null;
            boolean exclude = false;
            if (filter != null && filter.length() > 0) {
                exclude = filter.charAt(0) == '-';
                if (exclude || filter.charAt(0) == '+')
                    filter = filter.substring(1);
                /* Convert filter to a regex. Rules are...
                 *  . => \.
                 *  ** => [\w\.]+
                 *  * => [\w]+
                 */
                filter = StringHelper.replace(filter, ".", "\\.");
                filter = StringHelper.replace(filter, "**", "[\\w\\.]+");
                filter = StringHelper.replace(filter, "*", "[\\w]+");
                if (log.isDebugEnabled())
                    log.debug("Converted filter '" + m_rules[i] + "' to pattern '" + filter + "'");
                p = Pattern.compile(filter);
            }
            m_filterTypes[i] = exclude;
            m_filterPatterns[i] = p;
        }

        // Now build list of acceptable fields
        List<String> allFields = getFieldList(clazz);
        if (allFields != null) {
            for (String field : allFields) {
                boolean foreign = field.startsWith("+");
                boolean related = field.startsWith("*");
                if (foreign || related)
                    field = field.substring(1);
                Boolean include = includeField(field);
                if (include != null && include) {
                    filteredFields.add(field);
                    if (related)
                        relToCheck.add(field);
                }
            }
            Collections.sort(filteredFields);
        }

        // Removed related reference if there are no related fields
        for (String field : relToCheck) {
            if (log.isDebugEnabled())
                log.debug("Check related object " + field);
            if (!areSubFieldsIncluded(filteredFields, field)) {
                int i = Collections.binarySearch(filteredFields, field);
                if (i >= 0) {
                    filteredFields.remove(i);
                    if (log.isDebugEnabled())
                        log.debug("Removed related object " + field);
                }
            }
        }

        // Now make sure based on all the filtering, that all parent object referenced
        // are in place. You can't have x.y.z without first having x.y and therefore x
        for (int cursor = 0; cursor < filteredFields.size(); ++cursor) {
            // perform the check recursively since the addition of 'x.y.z' might also require the insertion of 'x.y' and so on..
            String field = (String) filteredFields.get(cursor);
            while (true) {
                int pos = field.lastIndexOf('.');
                if (pos > 0) {
                    field = field.substring(0, pos);
                    int i = Collections.binarySearch(filteredFields, field);
                    if (i < 0) {
                        // Determine the insertion point. Note: the value of i is (-(insertion point) - 1)
                        i = -(i + 1);
                        filteredFields.add(i, field);
                        if (log.isDebugEnabled())
                            log.debug("Added missing parent object " + field);
                        if (i <= cursor)
                            ++cursor;
                    }
                } else {
                    break;
                }
            }
        }

        m_filteredFields = filteredFields;
    }

    /** Returns true if the field matches any rule and is not marked for exclusion.
     * A false is returned if the field matches any rule and is marked for exclusion.
     * A null is returned if the field does not match any rule.
     * <p>
     * This method is expensive and should be used sparingly.
     * It is provided primarily to support custom fields that are
     * not part of the mapping file, and which hence cannot be supported by the
     * more efficient 'isFieldIncluded()' method.
     */
    public Boolean includeField(String field) {
        for (int i = 0; i < m_rules.length; i++) {
            Pattern p = m_filterPatterns[i];
            boolean exclude = m_filterTypes[i];
            if (p != null && p.matcher(field).matches()) {
                if (log.isDebugEnabled())
                    log.debug("field '" + field + "' has been " + (exclude ? "excluded" : "included")
                            + " by pattern '" + p.pattern() + "'");
                return !exclude;
            }
        }
        return null;
    }
}