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.ojb.broker.query.Criteria;
019import org.apache.ojb.broker.query.Query;
020import org.apache.ojb.broker.query.QueryFactory;
021import org.joda.time.DateTime;
022import org.kuali.rice.core.api.criteria.AndPredicate;
023import org.kuali.rice.core.api.criteria.CompositePredicate;
024import org.kuali.rice.core.api.criteria.CountFlag;
025import org.kuali.rice.core.api.criteria.CriteriaDateTimeValue;
026import org.kuali.rice.core.api.criteria.CriteriaLookupService;
027import org.kuali.rice.core.api.criteria.CriteriaValue;
028import org.kuali.rice.core.api.criteria.EqualIgnoreCasePredicate;
029import org.kuali.rice.core.api.criteria.EqualPredicate;
030import org.kuali.rice.core.api.criteria.GenericQueryResults;
031import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
032import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
033import org.kuali.rice.core.api.criteria.InIgnoreCasePredicate;
034import org.kuali.rice.core.api.criteria.InPredicate;
035import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
036import org.kuali.rice.core.api.criteria.LessThanPredicate;
037import org.kuali.rice.core.api.criteria.LikePredicate;
038import org.kuali.rice.core.api.criteria.LookupCustomizer;
039import org.kuali.rice.core.api.criteria.MultiValuedPredicate;
040import org.kuali.rice.core.api.criteria.NotEqualIgnoreCasePredicate;
041import org.kuali.rice.core.api.criteria.NotEqualPredicate;
042import org.kuali.rice.core.api.criteria.NotInIgnoreCasePredicate;
043import org.kuali.rice.core.api.criteria.NotInPredicate;
044import org.kuali.rice.core.api.criteria.NotLikePredicate;
045import org.kuali.rice.core.api.criteria.NotNullPredicate;
046import org.kuali.rice.core.api.criteria.NullPredicate;
047import org.kuali.rice.core.api.criteria.OrPredicate;
048import org.kuali.rice.core.api.criteria.Predicate;
049import org.kuali.rice.core.api.criteria.PropertyPathPredicate;
050import org.kuali.rice.core.api.criteria.QueryByCriteria;
051import org.kuali.rice.core.api.criteria.SingleValuedPredicate;
052import org.kuali.rice.core.framework.persistence.ojb.dao.PlatformAwareDaoBaseOjb;
053
054import java.sql.Timestamp;
055import java.util.ArrayList;
056import java.util.HashSet;
057import java.util.List;
058import java.util.Set;
059
060public class CriteriaLookupDaoOjb extends PlatformAwareDaoBaseOjb implements CriteriaLookupDao {
061
062    @Override
063    public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria) {
064        return lookup(queryClass, criteria, LookupCustomizer.Builder.<T>create().build());
065    }
066
067    @Override
068    public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer) {
069        if (queryClass == null) {
070            throw new IllegalArgumentException("queryClass is null");
071        }
072
073        if (criteria == null) {
074            throw new IllegalArgumentException("criteria is null");
075        }
076
077        if (customizer == null) {
078            throw new IllegalArgumentException("customizer is null");
079        }
080
081        final Criteria parent = new Criteria();
082
083        if (criteria.getPredicate() != null) {
084            addPredicate(criteria.getPredicate(), parent, customizer.getPredicateTransform());
085        }
086
087        switch (criteria.getCountFlag()) {
088            case ONLY:
089                return forCountOnly(queryClass, criteria, parent);
090            case NONE:
091                return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
092            case INCLUDE:
093                return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
094            default: throw new UnsupportedCountFlagException(criteria.getCountFlag());
095        }
096    }
097
098    /** gets results where the actual rows are requested. */
099    private <T> GenericQueryResults<T> forRowResults(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria ojbCriteria, CountFlag flag, LookupCustomizer.Transform<T, T> transform) {
100        final Query ojbQuery = QueryFactory.newQuery(queryClass, ojbCriteria);
101        final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
102
103        if (flag == CountFlag.INCLUDE) {
104            results.setTotalRowCount(getPersistenceBrokerTemplate().getCount(ojbQuery));
105        }
106
107        //ojb's is 1 based, our query api is zero based
108        final int startAtIndex = criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() + 1 : 1;
109        ojbQuery.setStartAtIndex(startAtIndex);
110
111        if (criteria.getMaxResults() != null) {
112            //not subtracting one from MaxResults in order to retrieve
113            //one extra row so that the MoreResultsAvailable field can be set
114            ojbQuery.setEndAtIndex(criteria.getMaxResults() + startAtIndex);
115        }
116
117        @SuppressWarnings("unchecked")
118        final List<T> rows = new ArrayList<T>(getPersistenceBrokerTemplate().getCollectionByQuery(ojbQuery));
119
120        if (criteria.getMaxResults() != null && rows.size() > criteria.getMaxResults()) {
121            results.setMoreResultsAvailable(true);
122            //remove the extra row that was returned
123            rows.remove(criteria.getMaxResults().intValue());
124        }
125
126        results.setResults(transformResults(rows, transform));
127        return results.build();
128    }
129
130    private static <T> List<T> transformResults(List<T> results, LookupCustomizer.Transform<T, T> transform) {
131        final List<T> list = new ArrayList<T>();
132        for (T r : results) {
133            list.add(transform.apply(r));
134        }
135        return list;
136    }
137
138    /** gets results where only the count is requested. */
139    private <T> GenericQueryResults<T> forCountOnly(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria ojbCriteria) {
140        final Query ojbQuery = QueryFactory.newQuery(queryClass, ojbCriteria);
141        final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
142        results.setTotalRowCount(getPersistenceBrokerTemplate().getCount(ojbQuery));
143
144        return results.build();
145    }
146
147    /** adds a predicate to a Criteria.*/
148    private void addPredicate(Predicate p, Criteria parent, LookupCustomizer.Transform<Predicate, Predicate> transform) {
149        p = transform.apply(p);
150
151        if (p instanceof PropertyPathPredicate) {
152            final String pp = ((PropertyPathPredicate) p).getPropertyPath();
153            if (p instanceof NotNullPredicate) {
154                parent.addNotNull(pp);
155            } else if (p instanceof NullPredicate) {
156                parent.addIsNull(pp);
157            } else if (p instanceof SingleValuedPredicate) {
158                addSingleValuePredicate((SingleValuedPredicate) p, parent);
159            } else if (p instanceof MultiValuedPredicate) {
160                addMultiValuePredicate((MultiValuedPredicate) p, parent);
161            } else {
162                throw new UnsupportedPredicateException(p);
163            }
164        } else if (p instanceof CompositePredicate) {
165            addCompositePredicate((CompositePredicate) p, parent, transform);
166        } else {
167            throw new UnsupportedPredicateException(p);
168        }
169    }
170
171    /** adds a single valued predicate to a Criteria. */
172    private void addSingleValuePredicate(SingleValuedPredicate p, Criteria parent) {
173        final Object value = getVal(p.getValue());
174        final String pp = p.getPropertyPath();
175        if (p instanceof EqualPredicate) {
176            parent.addEqualTo(pp, value);
177        } else if (p instanceof EqualIgnoreCasePredicate) {
178            parent.addEqualTo(genUpperFunc(pp), ((String) value).toUpperCase());
179        } else if (p instanceof GreaterThanOrEqualPredicate) {
180            parent.addGreaterOrEqualThan(pp, value);
181        } else if (p instanceof GreaterThanPredicate) {
182            parent.addGreaterThan(pp, value);
183        } else if (p instanceof LessThanOrEqualPredicate) {
184            parent.addLessOrEqualThan(pp, value);
185        } else if (p instanceof LessThanPredicate) {
186            parent.addLessThan(pp, value);
187        } else if (p instanceof LikePredicate) {
188            //no need to convert * or ? since ojb handles the conversion/escaping
189            parent.addLike(genUpperFunc(pp), ((String) value).toUpperCase());
190        } else if (p instanceof NotEqualPredicate) {
191            parent.addNotEqualTo(pp, value);
192        } else if (p instanceof NotEqualIgnoreCasePredicate) {
193            parent.addNotEqualTo(genUpperFunc(pp), ((String) value).toUpperCase());
194        } else if (p instanceof NotLikePredicate) {
195            parent.addNotLike(pp, value);
196        } else {
197            throw new UnsupportedPredicateException(p);
198        }
199    }
200
201    /** adds a multi valued predicate to a Criteria. */
202    private void addMultiValuePredicate(MultiValuedPredicate p, Criteria parent) {
203        final String pp = p.getPropertyPath();
204        if (p instanceof InPredicate) {
205            final Set<?> values = getVals(p.getValues());
206            parent.addIn(pp, values);
207        } else if (p instanceof InIgnoreCasePredicate) {
208            final Set<String> values = toUpper(getValsUnsafe(((InIgnoreCasePredicate) p).getValues()));
209            parent.addIn(genUpperFunc(pp), values);
210        } else if (p instanceof NotInPredicate) {
211            final Set<?> values = getVals(p.getValues());
212            parent.addNotIn(pp, values);
213        } else if (p instanceof NotInIgnoreCasePredicate) {
214            final Set<String> values = toUpper(getValsUnsafe(((NotInIgnoreCasePredicate) p).getValues()));
215            parent.addNotIn(genUpperFunc(pp), values);
216        } else {
217            throw new UnsupportedPredicateException(p);
218        }
219    }
220
221    /** adds a composite predicate to a Criteria. */
222    private void addCompositePredicate(final CompositePredicate p, final Criteria parent,  LookupCustomizer.Transform<Predicate, Predicate> transform) {
223        for (Predicate ip : p.getPredicates()) {
224            final Criteria inner = new Criteria();
225            addPredicate(ip, inner, transform);
226            if (p instanceof AndPredicate) {
227                parent.addAndCriteria(inner);
228            } else if (p instanceof OrPredicate) {
229                parent.addOrCriteria(inner);
230            } else {
231                throw new UnsupportedPredicateException(p);
232            }
233        }
234    }
235
236    private static <U extends CriteriaValue<?>> Object getVal(U toConv) {
237        Object o = toConv.getValue();
238        if (o instanceof DateTime) {
239            return new Timestamp(((DateTime) o).getMillis());
240        }
241        return o;
242    }
243
244    //this is unsafe b/c values could be converted resulting in a classcast exception
245    @SuppressWarnings("unchecked")
246    private static <T, U extends CriteriaValue<T>> Set<T> getValsUnsafe(Set<? extends U> toConv) {
247        return (Set<T>) getVals(toConv);
248    }
249
250    private static Set<?> getVals(Set<? extends CriteriaValue<?>> toConv) {
251        final Set<Object> values = new HashSet<Object>();
252        for (CriteriaValue<?> value : toConv) {
253            values.add(getVal(value));
254        }
255        return values;
256    }
257
258    //eliding performance for function composition....
259    private static Set<String> toUpper(Set<String> strs) {
260        final Set<String> values = new HashSet<String>();
261        for (String value : strs) {
262            values.add(value.toUpperCase());
263        }
264        return values;
265    }
266
267    private String getUpperFunction() {
268        return getDbPlatform().getUpperCaseFunction();
269    }
270
271    private String genUpperFunc(String pp) {
272        return new StringBuilder(getUpperFunction()).append("(").append(pp).append(")").toString();
273    }
274
275    /** this is a fatal error since this implementation should support all known predicates. */
276    private static class UnsupportedPredicateException extends RuntimeException {
277        private UnsupportedPredicateException(Predicate predicate) {
278            super("Unsupported predicate [" + String.valueOf(predicate) + "]");
279        }
280    }
281
282    /** this is a fatal error since this implementation should support all known count flags. */
283    private static class UnsupportedCountFlagException extends RuntimeException {
284        private UnsupportedCountFlagException(CountFlag flag) {
285            super("Unsupported predicate [" + String.valueOf(flag) + "]");
286        }
287    }
288}