1 /**
2 * Copyright 2005-2012 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 private static final Pattern FLAGS_PATTERN = Pattern.compile("^\\(\\?[a-z]+\\)");
78
79 private PredicateFactoryLookup() {
80 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 if (clazz == null) {
156 throw new IllegalArgumentException("clazz is null");
157 }
158
159 if (criteria == null || criteria.isEmpty()) {
160 throw new IllegalArgumentException("criteria is null or empty");
161 }
162
163 final List<Predicate> toAnd = new ArrayList<Predicate>();
164 for (Map.Entry<String, ?> entry : criteria.entrySet()) {
165 final String key = entry.getKey();
166 if (key == null) {
167 throw new IllegalArgumentException("criteria contains a null key");
168 }
169 toAnd.add(createPredicate(clazz, key, entry.getValue()));
170 }
171
172 return and(toAnd.toArray(new Predicate[]{}));
173 }
174
175 private static Predicate createPredicate(Class<?> clazz, final String key, final Object value) {
176 if (value == null) {
177 return isNull(key);
178 } else if (value instanceof String) {
179 final String flagStr = getFlagsStr((String) value);
180 final String strValue = removeFlag((String) value, flagStr);
181 return containsOperator(strValue) ? createFromComplexCriteriaValue(clazz, key, strValue, flagStr) :
182 createFromSimpleCriteriaValue(clazz, key, strValue, flagStr);
183 } else if (value instanceof Collection) {
184 final List<Predicate> ps = new ArrayList<Predicate>();
185 for (Object v : (Collection<?>) value) {
186 //recurs
187 ps.add(createPredicate(clazz, key, v));
188 }
189 return and(ps.toArray(new Predicate[]{}));
190 } else {
191 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 final boolean caseInsensitive = isCaseInsensitive(flagStr);
199
200 return null;
201 }
202
203 private static Predicate createFromSimpleCriteriaValue(Class<?> clazz, final String key, final String strValue,
204 final String flagStr) {
205 final boolean caseInsensitive = isCaseInsensitive(flagStr);
206 return caseInsensitive ? equalIgnoreCase(key, strValue) : equal(key, strValue);
207 }
208
209 private static String removeFlag(final String strValue, final String flagStr) {
210 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 for (SearchOperator o : SearchOperator.values()) {
216 if (value.contains(o.op())) {
217 return true;
218 }
219 }
220 return false;
221 }
222
223 private static String getFlagsStr(String criteria) {
224 Matcher m = FLAGS_PATTERN.matcher(criteria);
225 if (m.find()) {
226 return m.group();
227 }
228 return "";
229 }
230
231 private static boolean isCaseInsensitive(String flagStr) {
232 return flagStr.contains("i");
233 }
234 }