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 }