View Javadoc

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      private static final Pattern FLAGS_PATTERN = Pattern.compile("^\\(\\?[a-z]+\\)");
63  
64      private PredicateFactoryLookup() {
65          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         if (clazz == null) {
141             throw new IllegalArgumentException("clazz is null");
142         }
143 
144         if (criteria == null || criteria.isEmpty()) {
145             throw new IllegalArgumentException("criteria is null or empty");
146         }
147 
148         final List<Predicate> toAnd = new ArrayList<Predicate>();
149         for (Map.Entry<String, ?> entry : criteria.entrySet()) {
150             final String key = entry.getKey();
151             if (key == null) {
152                 throw new IllegalArgumentException("criteria contains a null key");
153             }
154             toAnd.add(createPredicate(clazz, key, entry.getValue()));
155         }
156 
157         return and(toAnd.toArray(new Predicate[]{}));
158     }
159 
160     private static Predicate createPredicate(Class<?> clazz, final String key, final Object value) {
161         if (value == null) {
162             return isNull(key);
163         } else if (value instanceof String) {
164             final String flagStr = getFlagsStr((String) value);
165             final String strValue = removeFlag((String) value, flagStr);
166             return containsOperator(strValue) ? createFromComplexCriteriaValue(clazz, key, strValue, flagStr) :
167                     createFromSimpleCriteriaValue(clazz, key, strValue, flagStr);
168         } else if (value instanceof Collection) {
169             final List<Predicate> ps = new ArrayList<Predicate>();
170             for (Object v : (Collection<?>) value) {
171                 //recurs
172                 ps.add(createPredicate(clazz, key, v));
173             }
174             return and(ps.toArray(new Predicate[]{}));
175         } else {
176             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         final boolean caseInsensitive = isCaseInsensitive(flagStr);
184 
185         return null;
186     }
187 
188     private static Predicate createFromSimpleCriteriaValue(Class<?> clazz, final String key, final String strValue,
189             final String flagStr) {
190         final boolean caseInsensitive = isCaseInsensitive(flagStr);
191         return caseInsensitive ? equalIgnoreCase(key, strValue) : equal(key, strValue);
192     }
193 
194     private static String removeFlag(final String strValue, final String flagStr) {
195         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         for (SearchOperator o : SearchOperator.values()) {
201             if (value.contains(o.op())) {
202                 return true;
203             }
204         }
205         return false;
206     }
207 
208     private static String getFlagsStr(String criteria) {
209         Matcher m = FLAGS_PATTERN.matcher(criteria);
210         if (m.find()) {
211             return m.group();
212         }
213         return "";
214     }
215 
216     private static boolean isCaseInsensitive(String flagStr) {
217         return flagStr.contains("i");
218     }
219 }