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