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 }