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 }