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}