001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.krad.criteria;
017    
018    import org.apache.ojb.broker.query.Criteria;
019    import org.apache.ojb.broker.query.Query;
020    import org.apache.ojb.broker.query.QueryFactory;
021    import org.joda.time.DateTime;
022    import org.kuali.rice.core.api.criteria.AndPredicate;
023    import org.kuali.rice.core.api.criteria.CompositePredicate;
024    import org.kuali.rice.core.api.criteria.CountFlag;
025    import org.kuali.rice.core.api.criteria.CriteriaDateTimeValue;
026    import org.kuali.rice.core.api.criteria.CriteriaLookupService;
027    import org.kuali.rice.core.api.criteria.CriteriaValue;
028    import org.kuali.rice.core.api.criteria.EqualIgnoreCasePredicate;
029    import org.kuali.rice.core.api.criteria.EqualPredicate;
030    import org.kuali.rice.core.api.criteria.GenericQueryResults;
031    import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
032    import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
033    import org.kuali.rice.core.api.criteria.InIgnoreCasePredicate;
034    import org.kuali.rice.core.api.criteria.InPredicate;
035    import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
036    import org.kuali.rice.core.api.criteria.LessThanPredicate;
037    import org.kuali.rice.core.api.criteria.LikePredicate;
038    import org.kuali.rice.core.api.criteria.LookupCustomizer;
039    import org.kuali.rice.core.api.criteria.MultiValuedPredicate;
040    import org.kuali.rice.core.api.criteria.NotEqualIgnoreCasePredicate;
041    import org.kuali.rice.core.api.criteria.NotEqualPredicate;
042    import org.kuali.rice.core.api.criteria.NotInIgnoreCasePredicate;
043    import org.kuali.rice.core.api.criteria.NotInPredicate;
044    import org.kuali.rice.core.api.criteria.NotLikePredicate;
045    import org.kuali.rice.core.api.criteria.NotNullPredicate;
046    import org.kuali.rice.core.api.criteria.NullPredicate;
047    import org.kuali.rice.core.api.criteria.OrPredicate;
048    import org.kuali.rice.core.api.criteria.Predicate;
049    import org.kuali.rice.core.api.criteria.PropertyPathPredicate;
050    import org.kuali.rice.core.api.criteria.QueryByCriteria;
051    import org.kuali.rice.core.api.criteria.SingleValuedPredicate;
052    import org.kuali.rice.core.framework.persistence.ojb.dao.PlatformAwareDaoBaseOjb;
053    
054    import java.sql.Timestamp;
055    import java.util.ArrayList;
056    import java.util.HashSet;
057    import java.util.List;
058    import java.util.Set;
059    
060    public 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    }