View Javadoc
1   /**
2    * Copyright 2005-2016 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 }