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