Coverage Report - org.kuali.rice.krad.service.impl.PredicateFactoryLookup
 
Classes in this File Line Coverage Branch Coverage Complexity
PredicateFactoryLookup
56%
23/41
56%
17/30
3.778
 
 1  
 package org.kuali.rice.krad.service.impl;
 2  
 
 3  
 import org.kuali.rice.core.api.criteria.Predicate;
 4  
 import org.kuali.rice.core.api.search.SearchOperator;
 5  
 
 6  
 import java.util.ArrayList;
 7  
 import java.util.Collection;
 8  
 import java.util.List;
 9  
 import java.util.Map;
 10  
 import java.util.regex.Matcher;
 11  
 import java.util.regex.Pattern;
 12  
 
 13  
 import static org.kuali.rice.core.api.criteria.PredicateFactory.*;
 14  
 
 15  
 /**
 16  
  * Contains methods used in the predicate factory related to the lookup framework.
 17  
  * ***************************************************************************************************************
 18  
  * FIXME: issues to talk to the group about.
 19  
  * http://kuali.org/rice/documentation/1.0.3/UG_Global/Documents/lookupwildcards.htm
 20  
  *
 21  
  * 1) Should we support isNotNull, isNull, as wildcards?  Then do we still translate
 22  
  * null values into isNull predicates.  I believe the lookup framework right now
 23  
  * barfs on null values but that is a guess.
 24  
  *
 25  
  * 2) We need to support case insensitivity in the old lookup framework.  Right now the lookup
 26  
  * framework looks at Data dictionary entries.  This can still be configured in the DD
 27  
  * but should be placed in the lookup criteria. We could have a "flag" section on a
 28  
  * lookup sequence like foo.bar=(?i)ba*|bing
 29  
  *
 30  
  * This would translate to
 31  
  *
 32  
  * or(like("foo.bar", "ba*"), equalsIgnoreCase("foo.bar", "bing"))
 33  
  *
 34  
  * Btw.  My flag format was stolen from regex but we could use anything really.
 35  
  * http://download.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#CASE_INSENSITIVE
 36  
  *
 37  
  * I'm currently supporting this.
 38  
  *
 39  
  * 3) In the above example, I used a case insensitive flag but Like doesn't support case
 40  
  * insensitive.  Should it?
 41  
  *
 42  
  * 4) Do we need to support escaping in the lookup framework & criteria api.  Right now the
 43  
  * lookup framework looks at Data dictionary entries.  This can still be configured in the DD
 44  
  * but should be placed in the lookup criteria.  Escaping is tricky and I worry if we support
 45  
  * it in the criteria api then it will make the criteria service much harder to make custom
 46  
  * implementations.  To me it seems it's better to make escaping behavior undefined.
 47  
  *
 48  
  * If we do support an escape character then we should probably also support a flag to treat
 49  
  * escape chars as literal like (?l) (that doesn't exist in java regex)
 50  
  *
 51  
  * http://download.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#LITERAL
 52  
  *
 53  
  * 5) Maybe we should just support what is in the {@link org.kuali.rice.core.framework.logic.SearchOperator} class
 54  
  *
 55  
  * 6) Maybe the predicate class could have a toLookupString, toLookupMap() methods on them to translate
 56  
  * to various formats of criteria?  Or maybe this factory method & related methods should get placed into
 57  
  * krad or somewhere else?
 58  
  * ***************************************************************************************************************
 59  
  */
 60  
 class PredicateFactoryLookup {
 61  
 
 62  1
     private static final Pattern FLAGS_PATTERN = Pattern.compile("^\\(\\?[a-z]+\\)");
 63  
 
 64  0
     private PredicateFactoryLookup() {
 65  0
         throw new IllegalArgumentException("do not call");
 66  
     }
 67  
 
 68  
     /**
 69  
      * This take a criteria map that is commonly used in krad-based
 70  
      * applications and transforms it to a predicate.
 71  
      *
 72  
      * The incoming map takes the form of the following possibilities:
 73  
      *
 74  
      * <ul>
 75  
      * <li>propertyPath=value</li>
 76  
      * <li>propertyPath=criteriaSequence</li>
 77  
      * </ul>
 78  
      *
 79  
      * <p>
 80  
      * The values in the map can either be a String or a Collection<Object>.
 81  
      * A String value is directly parsed into predicates.  If a collection
 82  
      * is found it is recursively parsed into predicates where each entry in the Collection
 83  
      * is anded together.
 84  
      * </p>
 85  
      *
 86  
      * <p>
 87  
      * Note: that the Collection can contain other collections or strings
 88  
      * but eventually must resolve to a string value or string criteria sequence.
 89  
      * </p>
 90  
      *
 91  
      * a simple example of a propertyPath=value:
 92  
      * <pre>
 93  
      * foo.bar=baz
 94  
      * </pre>
 95  
      * would yield
 96  
      * <pre>
 97  
      * equals("foo.bar", "baz")
 98  
      * </pre>
 99  
      *
 100  
      * a simple example of a propertyPath=criteriaSequence
 101  
      * <pre>
 102  
      * foo.bar=ba*|bing
 103  
      * </pre>
 104  
      * would yield
 105  
      * <pre>
 106  
      * or(like("foo.bar", "ba*"), equals("foo.bar", "bing"))
 107  
      * </pre>
 108  
      *
 109  
      * a compound example of a of a propertyPath=criteriaSequence
 110  
      * <pre>
 111  
      * foo.bar=[ba*,bing]      * note: [] shows a collection literal
 112  
      * </pre>
 113  
      * would yield
 114  
      * <pre>
 115  
      * and(like("foo.bar", "ba*"), equals("foo.bar", "bing"))
 116  
      * </pre>
 117  
      *
 118  
      * <p>
 119  
      * Related to null values:  Null values will be translated to isNull predicates.
 120  
      * </p>
 121  
      *
 122  
      * The criteria string may also contain flags similar to regex flags.  The current
 123  
      * supported flags are:
 124  
      *
 125  
      * <ul>
 126  
      *     <li>(?i) case insensitive</li>
 127  
      * </ul>
 128  
      *
 129  
      * To use the 'i' and 'm' flags prepend them to the criteria value, for example:
 130  
      *
 131  
      * (?im)foo
 132  
      *
 133  
      * Only the first flag sequence will be honored.  All others will be treated as literals.
 134  
      *
 135  
      * @param the root class to build the predicate on.  Cannot be null.
 136  
      * @param criteria the crtieria map.  Cannot be null or empty.
 137  
      * @throws IllegalArgumentException if clazz is null or criteria is null or empty
 138  
      */
 139  
     static Predicate fromCriteriaMap(Class<?> clazz, Map<String, ?> criteria) {
 140  0
         if (clazz == null) {
 141  0
             throw new IllegalArgumentException("clazz is null");
 142  
         }
 143  
 
 144  0
         if (criteria == null || criteria.isEmpty()) {
 145  0
             throw new IllegalArgumentException("criteria is null or empty");
 146  
         }
 147  
 
 148  0
         final List<Predicate> toAnd = new ArrayList<Predicate>();
 149  0
         for (Map.Entry<String, ?> entry : criteria.entrySet()) {
 150  0
             final String key = entry.getKey();
 151  0
             if (key == null) {
 152  0
                 throw new IllegalArgumentException("criteria contains a null key");
 153  
             }
 154  0
             toAnd.add(createPredicate(clazz, key, entry.getValue()));
 155  0
         }
 156  
 
 157  0
         return and(toAnd.toArray(new Predicate[]{}));
 158  
     }
 159  
 
 160  
     private static Predicate createPredicate(Class<?> clazz, final String key, final Object value) {
 161  9
         if (value == null) {
 162  1
             return isNull(key);
 163  8
         } else if (value instanceof String) {
 164  6
             final String flagStr = getFlagsStr((String) value);
 165  6
             final String strValue = removeFlag((String) value, flagStr);
 166  6
             return containsOperator(strValue) ? createFromComplexCriteriaValue(clazz, key, strValue, flagStr) :
 167  
                     createFromSimpleCriteriaValue(clazz, key, strValue, flagStr);
 168  2
         } else if (value instanceof Collection) {
 169  2
             final List<Predicate> ps = new ArrayList<Predicate>();
 170  2
             for (Object v : (Collection<?>) value) {
 171  
                 //recurs
 172  3
                 ps.add(createPredicate(clazz, key, v));
 173  
             }
 174  2
             return and(ps.toArray(new Predicate[]{}));
 175  
         } else {
 176  0
             throw new IllegalArgumentException(
 177  
                     "criteria map contained a value that was non supported :" + value.getClass().getName());
 178  
         }
 179  
     }
 180  
 
 181  
     private static Predicate createFromComplexCriteriaValue(Class<?> clazz, final String key, final String strValue,
 182  
             final String flagStr) {
 183  0
         final boolean caseInsensitive = isCaseInsensitive(flagStr);
 184  
 
 185  0
         return null;
 186  
     }
 187  
 
 188  
     private static Predicate createFromSimpleCriteriaValue(Class<?> clazz, final String key, final String strValue,
 189  
             final String flagStr) {
 190  6
         final boolean caseInsensitive = isCaseInsensitive(flagStr);
 191  6
         return caseInsensitive ? equalIgnoreCase(key, strValue) : equal(key, strValue);
 192  
     }
 193  
 
 194  
     private static String removeFlag(final String strValue, final String flagStr) {
 195  6
         return flagStr.length() > 1 ? (strValue).substring(flagStr.length() - 1) : strValue;
 196  
     }
 197  
 
 198  
     //does not handle escaping, assumes non-null
 199  
     private static boolean containsOperator(String value) {
 200  84
         for (SearchOperator o : SearchOperator.values()) {
 201  78
             if (value.contains(o.op())) {
 202  0
                 return true;
 203  
             }
 204  
         }
 205  6
         return false;
 206  
     }
 207  
 
 208  
     private static String getFlagsStr(String criteria) {
 209  12
         Matcher m = FLAGS_PATTERN.matcher(criteria);
 210  12
         if (m.find()) {
 211  6
             return m.group();
 212  
         }
 213  6
         return "";
 214  
     }
 215  
 
 216  
     private static boolean isCaseInsensitive(String flagStr) {
 217  6
         return flagStr.contains("i");
 218  
     }
 219  
 }