001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.criteria; 017 018import org.apache.commons.lang.StringUtils; 019import org.joda.time.DateTime; 020import org.kuali.rice.core.api.criteria.AndPredicate; 021import org.kuali.rice.core.api.criteria.CompositePredicate; 022import org.kuali.rice.core.api.criteria.CountFlag; 023import org.kuali.rice.core.api.criteria.CriteriaLookupService; 024import org.kuali.rice.core.api.criteria.CriteriaValue; 025import org.kuali.rice.core.api.criteria.EqualIgnoreCasePredicate; 026import org.kuali.rice.core.api.criteria.EqualPredicate; 027import org.kuali.rice.core.api.criteria.GenericQueryResults; 028import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate; 029import org.kuali.rice.core.api.criteria.GreaterThanPredicate; 030import org.kuali.rice.core.api.criteria.InIgnoreCasePredicate; 031import org.kuali.rice.core.api.criteria.InPredicate; 032import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate; 033import org.kuali.rice.core.api.criteria.LessThanPredicate; 034import org.kuali.rice.core.api.criteria.LikePredicate; 035import org.kuali.rice.core.api.criteria.LookupCustomizer; 036import org.kuali.rice.core.api.criteria.MultiValuedPredicate; 037import org.kuali.rice.core.api.criteria.NotEqualIgnoreCasePredicate; 038import org.kuali.rice.core.api.criteria.NotEqualPredicate; 039import org.kuali.rice.core.api.criteria.NotInIgnoreCasePredicate; 040import org.kuali.rice.core.api.criteria.NotInPredicate; 041import org.kuali.rice.core.api.criteria.NotLikePredicate; 042import org.kuali.rice.core.api.criteria.NotNullPredicate; 043import org.kuali.rice.core.api.criteria.NullPredicate; 044import org.kuali.rice.core.api.criteria.OrPredicate; 045import org.kuali.rice.core.api.criteria.Predicate; 046import org.kuali.rice.core.api.criteria.PropertyPathPredicate; 047import org.kuali.rice.core.api.criteria.QueryByCriteria; 048import org.kuali.rice.core.api.criteria.SingleValuedPredicate; 049import org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria; 050 051import javax.persistence.EntityManager; 052import javax.persistence.PersistenceContext; 053import javax.persistence.Query; 054import java.sql.Timestamp; 055import java.util.ArrayList; 056import java.util.HashSet; 057import java.util.List; 058import java.util.Set; 059 060public class CriteriaLookupDaoJpa implements CriteriaLookupDao { 061 @PersistenceContext 062 private EntityManager entityManager; 063 064 @Override 065 public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria) { 066 return lookup(queryClass, criteria, LookupCustomizer.Builder.<T>create().build()); 067 } 068 069 @Override 070 public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer) { 071 if (queryClass == null) { 072 throw new IllegalArgumentException("queryClass is null"); 073 } 074 075 if (criteria == null) { 076 throw new IllegalArgumentException("criteria is null"); 077 } 078 079 if (customizer == null) { 080 throw new IllegalArgumentException("customizer is null"); 081 } 082 083 final Criteria parent = new Criteria(queryClass.getClass().getName()); 084 085 if (criteria.getPredicate() != null) { 086 addPredicate(criteria.getPredicate(), parent, customizer.getPredicateTransform()); 087 } 088 089 switch (criteria.getCountFlag()) { 090 case ONLY: 091 return forCountOnly(queryClass, criteria, parent); 092 case NONE: 093 return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform()); 094 case INCLUDE: 095 return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform()); 096 default: throw new UnsupportedCountFlagException(criteria.getCountFlag()); 097 } 098 } 099 100 /** gets results where the actual rows are requested. */ 101 private <T> GenericQueryResults<T> forRowResults(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria, CountFlag flag, LookupCustomizer.Transform<T, T> transform) { 102 final Query jpaQuery = new org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery(); 103 final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create(); 104 105 //ojb's is 1 based, our query api is zero based 106 final int startAtIndex = criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() + 1 : 1; 107 jpaQuery.setFirstResult(startAtIndex); 108 109 if (criteria.getMaxResults() != null) { 110 //not subtracting one from MaxResults in order to retrieve 111 //one extra row so that the MoreResultsAvailable field can be set 112 jpaQuery.setMaxResults(criteria.getMaxResults()); 113 } 114 115 @SuppressWarnings("unchecked") 116 final List<T> rows = new ArrayList<T>(jpaQuery.getResultList()); 117 if (flag == CountFlag.INCLUDE) { 118 results.setTotalRowCount(rows.size()); 119 } 120 121 if (criteria.getMaxResults() != null && rows.size() > criteria.getMaxResults()) { 122 results.setMoreResultsAvailable(true); 123 //remove the extra row that was returned 124 rows.remove(criteria.getMaxResults().intValue()); 125 } 126 127 results.setResults(transformResults(rows, transform)); 128 return results.build(); 129 } 130 131 private static <T> List<T> transformResults(List<T> results, LookupCustomizer.Transform<T, T> transform) { 132 final List<T> list = new ArrayList<T>(); 133 for (T r : results) { 134 list.add(transform.apply(r)); 135 } 136 return list; 137 } 138 139 /** gets results where only the count is requested. */ 140 private <T> GenericQueryResults<T> forCountOnly(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria) { 141 final Query jpaQuery = new org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery(); 142 final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create(); 143 // TODO : There has to be a better way to do this. 144 results.setTotalRowCount(jpaQuery.getResultList().size()); 145 146 return results.build(); 147 } 148 149 /** adds a predicate to a Criteria.*/ 150 private void addPredicate(Predicate p, Criteria parent, LookupCustomizer.Transform<Predicate, Predicate> transform) { 151 p = transform.apply(p); 152 153 if (p instanceof PropertyPathPredicate) { 154 final String pp = ((PropertyPathPredicate) p).getPropertyPath(); 155 if (p instanceof NotNullPredicate) { 156 parent.notNull(pp); 157 } else if (p instanceof NullPredicate) { 158 parent.isNull(pp); 159 } else if (p instanceof SingleValuedPredicate) { 160 addSingleValuePredicate((SingleValuedPredicate) p, parent); 161 } else if (p instanceof MultiValuedPredicate) { 162 addMultiValuePredicate((MultiValuedPredicate) p, parent); 163 } else { 164 throw new UnsupportedPredicateException(p); 165 } 166 } else if (p instanceof CompositePredicate) { 167 addCompositePredicate((CompositePredicate) p, parent, transform); 168 } else { 169 throw new UnsupportedPredicateException(p); 170 } 171 } 172 173 /** adds a single valued predicate to a Criteria. */ 174 private void addSingleValuePredicate(SingleValuedPredicate p, Criteria parent) { 175 final Object value = getVal(p.getValue()); 176 final String pp = p.getPropertyPath(); 177 if (p instanceof EqualPredicate) { 178 parent.eq(pp, value); 179 } else if (p instanceof EqualIgnoreCasePredicate) { 180 parent.eq(genUpperFunc(pp), ((String) value).toUpperCase()); 181 } else if (p instanceof GreaterThanOrEqualPredicate) { 182 parent.gte(pp, value); 183 } else if (p instanceof GreaterThanPredicate) { 184 parent.gt(pp, value); 185 } else if (p instanceof LessThanOrEqualPredicate) { 186 parent.lte(pp, value); 187 } else if (p instanceof LessThanPredicate) { 188 parent.lt(pp, value); 189 } else if (p instanceof LikePredicate) { 190 //no need to convert * or ? since ojb handles the conversion/escaping 191 parent.like(pp, value); 192 } else if (p instanceof NotEqualPredicate) { 193 parent.ne(pp, value); 194 } else if (p instanceof NotEqualIgnoreCasePredicate) { 195 parent.ne(genUpperFunc(pp), ((String) value).toUpperCase()); 196 } else if (p instanceof NotLikePredicate) { 197 parent.notLike(pp, value); 198 } else { 199 throw new UnsupportedPredicateException(p); 200 } 201 } 202 203 /** adds a multi valued predicate to a Criteria. */ 204 private void addMultiValuePredicate(MultiValuedPredicate p, Criteria parent) { 205 final String pp = p.getPropertyPath(); 206 if (p instanceof InPredicate) { 207 final Set<?> values = getVals(p.getValues()); 208 parent.in(pp, values); 209 } else if (p instanceof InIgnoreCasePredicate) { 210 final Set<String> values = toUpper(getValsUnsafe(((InIgnoreCasePredicate) p).getValues())); 211 parent.in(genUpperFunc(pp), values); 212 } else if (p instanceof NotInPredicate) { 213 final Set<?> values = getVals(p.getValues()); 214 parent.notIn(pp, values); 215 } else if (p instanceof NotInIgnoreCasePredicate) { 216 final Set<String> values = toUpper(getValsUnsafe(((NotInIgnoreCasePredicate) p).getValues())); 217 parent.notIn(genUpperFunc(pp), values); 218 } else { 219 throw new UnsupportedPredicateException(p); 220 } 221 } 222 223 /** adds a composite predicate to a Criteria. */ 224 private void addCompositePredicate(final CompositePredicate p, final Criteria parent, LookupCustomizer.Transform<Predicate, Predicate> transform) { 225 for (Predicate ip : p.getPredicates()) { 226 final Criteria inner = new Criteria(parent.getEntityName()); 227 addPredicate(ip, inner, transform); 228 if (p instanceof AndPredicate) { 229 parent.and(inner); 230 } else if (p instanceof OrPredicate) { 231 parent.or(inner); 232 } else { 233 throw new UnsupportedPredicateException(p); 234 } 235 } 236 } 237 238 private static <U extends CriteriaValue<?>> Object getVal(U toConv) { 239 Object o = toConv.getValue(); 240 if (o instanceof DateTime) { 241 return new Timestamp(((DateTime) o).getMillis()); 242 } 243 return o; 244 } 245 246 //this is unsafe b/c values could be converted resulting in a classcast exception 247 @SuppressWarnings("unchecked") 248 private static <T, U extends CriteriaValue<T>> Set<T> getValsUnsafe(Set<? extends U> toConv) { 249 return (Set<T>) getVals(toConv); 250 } 251 252 private static Set<?> getVals(Set<? extends CriteriaValue<?>> toConv) { 253 final Set<Object> values = new HashSet<Object>(); 254 for (CriteriaValue<?> value : toConv) { 255 values.add(getVal(value)); 256 } 257 return values; 258 } 259 260 //eliding performance for function composition.... 261 private static Set<String> toUpper(Set<String> strs) { 262 final Set<String> values = new HashSet<String>(); 263 for (String value : strs) { 264 values.add(value.toUpperCase()); 265 } 266 return values; 267 } 268 269 270 private String genUpperFunc(String pp) { 271 if (StringUtils.contains(pp, "__JPA_ALIAS[[")) { 272 pp = "UPPER(" + pp + ")"; 273 } else { 274 pp = "UPPER(__JPA_ALIAS[[0]]__." + pp + ")"; 275 } 276 return pp; 277 } 278 279 /** 280 * @param entityManager the entityManager to set 281 */ 282 public void setEntityManager(EntityManager entityManager) { 283 this.entityManager = entityManager; 284 } 285 286 /** this is a fatal error since this implementation should support all known predicates. */ 287 private static class UnsupportedPredicateException extends RuntimeException { 288 private UnsupportedPredicateException(Predicate predicate) { 289 super("Unsupported predicate [" + String.valueOf(predicate) + "]"); 290 } 291 } 292 293 /** this is a fatal error since this implementation should support all known count flags. */ 294 private static class UnsupportedCountFlagException extends RuntimeException { 295 private UnsupportedCountFlagException(CountFlag flag) { 296 super("Unsupported predicate [" + String.valueOf(flag) + "]"); 297 } 298 } 299}