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.service.impl;
017
018import static org.junit.Assert.assertEquals;
019import static org.junit.Assert.assertNotNull;
020import static org.junit.Assert.assertTrue;
021import static org.junit.Assert.fail;
022import static org.mockito.Matchers.any;
023import static org.mockito.Mockito.mock;
024import static org.mockito.Mockito.when;
025
026import java.sql.Date;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Set;
030
031import org.joda.time.DateTime;
032import org.joda.time.LocalDate;
033import org.joda.time.format.DateTimeFormatter;
034import org.junit.Before;
035import org.junit.Test;
036import org.junit.runner.RunWith;
037import org.kuali.rice.core.api.criteria.AndPredicate;
038import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
039import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
040import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
041import org.kuali.rice.core.api.criteria.LessThanPredicate;
042import org.kuali.rice.core.api.criteria.LikeIgnoreCasePredicate;
043import org.kuali.rice.core.api.criteria.LikePredicate;
044import org.kuali.rice.core.api.criteria.OrPredicate;
045import org.kuali.rice.core.api.criteria.Predicate;
046import org.kuali.rice.core.api.criteria.QueryByCriteria;
047import org.kuali.rice.core.api.datetime.DateTimeService;
048import org.kuali.rice.core.api.search.SearchOperator;
049import org.kuali.rice.krad.data.DataObjectService;
050import org.kuali.rice.krad.data.DataObjectWrapper;
051import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
052import org.kuali.rice.krad.data.provider.impl.DataObjectWrapperBase;
053import org.kuali.rice.krad.data.util.ReferenceLinker;
054import org.kuali.rice.krad.datadictionary.DataDictionary;
055import org.kuali.rice.krad.service.DataDictionaryService;
056import org.mockito.InjectMocks;
057import org.mockito.Mock;
058import org.mockito.invocation.InvocationOnMock;
059import org.mockito.runners.MockitoJUnitRunner;
060import org.mockito.stubbing.Answer;
061import org.springframework.format.datetime.joda.DateTimeFormatterFactory;
062
063/**
064 * Tests the functionality of the LookupCriteriaGeneratorImpl.
065 *
066 * @author Kuali Rice Team (rice.collab@kuali.org)
067 */
068@RunWith(MockitoJUnitRunner.class)
069public class LookupCriteriaGeneratorImplTest {
070
071    @Mock DataDictionary dataDictionary;
072    @Mock DataDictionaryService dataDictionaryService;
073    @Mock DataObjectService dataObjectService;
074    @Mock ReferenceLinker referenceLinker;
075    @Mock DateTimeService dateTimeService;
076
077    @InjectMocks private LookupCriteriaGeneratorImpl generator = new LookupCriteriaGeneratorImpl();
078
079    private static final DateTimeFormatter formatter = new DateTimeFormatterFactory("mm/dd/yyyy").createDateTimeFormatter();
080
081    @Before
082    public void setUp() throws Exception {
083        when(dataDictionaryService.getDataDictionary()).thenReturn(dataDictionary);
084
085        // Make this property force upper-case so that the LIKE comparison generated from it will
086        // be converted to case sensitive.
087        when(dataDictionaryService.isAttributeDefined(TestClass.class, "prop2")).thenReturn(Boolean.TRUE);
088        when(dataDictionaryService.getAttributeForceUppercase(TestClass.class, "prop2")).thenReturn(Boolean.TRUE);
089
090        when(dataObjectService.wrap(any(TestClass.class))).thenAnswer(new Answer<DataObjectWrapper<TestClass>>() {
091            @Override
092            public DataObjectWrapper<TestClass> answer(InvocationOnMock invocation) throws Throwable {
093                return new DataObjectWrapperImpl<TestClass>((TestClass)invocation.getArguments()[0],
094                        mock(DataObjectMetadata.class), dataObjectService, referenceLinker);
095            }
096        });
097        when(dateTimeService.convertToSqlDate(any(String.class))).thenAnswer(new Answer<Date>() {
098            @Override
099            public Date answer(InvocationOnMock invocation) throws Throwable {
100                String date = (String) invocation.getArguments()[0];
101                return new Date(LocalDate.parse(date, formatter).toDateTimeAtStartOfDay().getMillis());
102            }
103        });
104        when(dateTimeService.convertToSqlDateUpperBound(any(String.class))).thenAnswer(new Answer<Date>() {
105            @Override
106            public Date answer(InvocationOnMock invocation) throws Throwable {
107                String date = (String) invocation.getArguments()[0];
108                return new Date(LocalDate.parse(date, formatter).plusDays(1).toDateTimeAtStartOfDay().getMillis());
109            }
110        });
111    }
112
113    @Test
114    public void testGenerateCriteria_MultipleOr() throws Exception {
115        Map<String, String> mapCriteria = new HashMap<String, String>();
116        mapCriteria.put("prop1", "a|b");
117        mapCriteria.put("prop2", "c");
118        mapCriteria.put("prop3", "d");
119
120        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
121        assertNotNull("build should not have been null", qbcBuilder);
122        QueryByCriteria qbc = qbcBuilder.build();
123
124        // now walk the tree, it should come out as:
125        // and(
126        //   or(
127        //     likeIgnoreCase(prop1, "a"),
128        //     likeIgnoreCase(prop1, "b"),
129        //   ),
130        //   like(prop2, "c"),
131        //   likeIgnoreCase(prop3, "d")
132        // )
133
134        Predicate and = qbc.getPredicate();
135        assertTrue("top level predicate type incorrect.  Was: " + and, and instanceof AndPredicate);
136        Set<Predicate> predicates = ((AndPredicate) and).getPredicates();
137
138        assertEquals("Wrong number of top-level predicates", 3, predicates.size());
139
140        boolean foundProp1 = false;
141        boolean foundProp2 = false;
142        boolean foundProp3 = false;
143        for (Predicate predicate : predicates) {
144            if (predicate instanceof LikePredicate) {
145                LikePredicate like = (LikePredicate)predicate;
146                if (like.getPropertyPath().equals("prop2")) {
147                    assertEquals("prop2 had wrong value", "c", like.getValue().getValue());
148                    foundProp2 = true;
149                } else {
150                    fail("Invalid like predicate encountered: " + predicate);
151                }
152            } else if (predicate instanceof LikeIgnoreCasePredicate) {
153                LikeIgnoreCasePredicate like = (LikeIgnoreCasePredicate)predicate;
154                if (like.getPropertyPath().equals("prop3")) {
155                    assertEquals("prop3 had wrong value", "d", like.getValue().getValue());
156                    foundProp3 = true;
157                } else {
158                    fail("Invalid likeIgnoreCase predicate encountered: " + predicate);
159                }
160            } else if (predicate instanceof OrPredicate) {
161                foundProp1 = true;
162                // under the or predicate we should have 2 likes, one for each component of the OR
163                OrPredicate orPredicate = (OrPredicate)predicate;
164                assertEquals("wrong number of predicates in the internal OR predicate",2, orPredicate.getPredicates().size());
165                for (Predicate orSubPredicate : orPredicate.getPredicates()) {
166                    if (orSubPredicate instanceof LikeIgnoreCasePredicate) {
167                        LikeIgnoreCasePredicate likeInternal = (LikeIgnoreCasePredicate)orSubPredicate;
168                        if (likeInternal.getPropertyPath().equals("prop1")) {
169                            assertTrue("prop1 had wrong value", "a".equals(likeInternal.getValue().getValue()) ||
170                                    "b".equals(likeInternal.getValue().getValue()));
171                        } else {
172                            fail("Invalid predicate, does not have a propertypath of prop1:" + predicate);
173                        }
174                    } else {
175                        fail("Unexpected predicate: " + orSubPredicate);
176                    }
177                }
178            } else {
179                fail("Unexpected predicate: " + predicate);
180            }
181        }
182        assertTrue("prop1 predicate missing", foundProp1);
183        assertTrue("prop2 predicate missing", foundProp2);
184        assertTrue("prop3 predicate missing", foundProp3);
185    }
186
187    /**
188     * Criteria for BETWEEN dates should range from the start of the day on the lower date to the end of the day on the
189     * upper date.
190     *
191     * <p>
192     * Since the end of the day is defined as the moment before the next day, then the range that should be checked is
193     * [1/1/2010,1/2/2010), or in SQL, approximately >=2010-01-01 00:00:00 AND <=2010-01-03 00:00:00.
194     * </p>
195     */
196    @Test
197    public void testGenerateCriteria_BetweenDate() {
198        String lowerDateString = "1/1/2010";
199        DateTime lowerDate = DateTime.parse(lowerDateString, formatter).withTimeAtStartOfDay();
200        String upperDateString = "1/2/2010";
201        DateTime upperDate = DateTime.parse(upperDateString, formatter).withTimeAtStartOfDay();
202
203        Map<String, String> mapCriteria = new HashMap<String, String>();
204        mapCriteria.put("prop4", lowerDateString + SearchOperator.BETWEEN.op() + upperDateString);
205
206        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
207        assertNotNull(qbcBuilder);
208        QueryByCriteria qbc = qbcBuilder.build();
209
210        Predicate and = qbc.getPredicate();
211        assertTrue(and instanceof AndPredicate);
212        Set<Predicate> predicates = ((AndPredicate) and).getPredicates();
213
214        assertEquals(2, predicates.size());
215
216        boolean foundProp4Lower = false;
217        boolean foundProp4Upper = false;
218        for (Predicate predicate : predicates) {
219            if (predicate instanceof GreaterThanOrEqualPredicate) {
220                foundProp4Lower = true;
221                GreaterThanOrEqualPredicate greaterThanOrEqual = (GreaterThanOrEqualPredicate) predicate;
222                assertEquals(greaterThanOrEqual.getValue().getValue(), lowerDate);
223            } else if (predicate instanceof LessThanOrEqualPredicate) {
224                foundProp4Upper = true;
225                LessThanOrEqualPredicate lessThanOrEqual = (LessThanOrEqualPredicate) predicate;
226                assertEquals(lessThanOrEqual.getValue().getValue(), upperDate.plusDays(1));
227            }
228        }
229        assertTrue(foundProp4Lower);
230        assertTrue(foundProp4Upper);
231    }
232
233    /**
234     * Criteria for GREATER THAN OR EQUAL dates should be equal to or after the start of the day on the date.
235     *
236     * <p>
237     * The value that should be checked is [1/1/2010,END_OF_TIME), or in SQL, >=2010-01-01 00:00:00.
238     * </p>
239     */
240    @Test
241    public void testGenerateCriteria_GreaterThanEqualDate() {
242        String dateString = "1/1/2010";
243        DateTime date = DateTime.parse(dateString, formatter).withTimeAtStartOfDay();
244
245        Map<String, String> mapCriteria = new HashMap<String, String>();
246        mapCriteria.put("prop4", SearchOperator.GREATER_THAN_EQUAL.op() + dateString);
247
248        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
249        assertNotNull(qbcBuilder);
250        QueryByCriteria qbc = qbcBuilder.build();
251
252        Predicate greaterThanEqual = qbc.getPredicate();
253        assertTrue(greaterThanEqual instanceof GreaterThanOrEqualPredicate);
254        assertEquals(((GreaterThanOrEqualPredicate) greaterThanEqual).getValue().getValue(), date);
255    }
256
257    /**
258     * Criteria for LESS THAN OR EQUAL dates should be equal to or before the end of the day on the date.
259     *
260     * <p>
261     * Since the end of the day is defined as the moment before the next day, then the value that should be
262     * checked is (BEGINNING_OF_TIME,1/2/2010), or in SQL, approximately <=2010-01-03 00:00:00.
263     * </p>
264     */
265    @Test
266    public void testGenerateCriteria_LessThanEqualDate() {
267        String dateString = "1/2/2010";
268        DateTime date = DateTime.parse(dateString, formatter).withTimeAtStartOfDay();
269
270        Map<String, String> mapCriteria = new HashMap<String, String>();
271        mapCriteria.put("prop4", SearchOperator.LESS_THAN_EQUAL.op() + dateString);
272
273        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
274        assertNotNull(qbcBuilder);
275        QueryByCriteria qbc = qbcBuilder.build();
276
277        Predicate lessThanEqual = qbc.getPredicate();
278        assertTrue(lessThanEqual instanceof LessThanOrEqualPredicate);
279        assertEquals(((LessThanOrEqualPredicate) lessThanEqual).getValue().getValue(), date.plusDays(1));
280    }
281
282    /**
283     * Criteria for GREATER THAN dates should be after the start of the day on the date.
284     *
285     * <p>
286     * The value that should be checked is >2010-01-01 00:00:00.
287     * </p>
288     */
289    @Test
290    public void testGenerateCriteria_GreaterThanDate() {
291        String dateString = "1/1/2010";
292        DateTime date = DateTime.parse(dateString, formatter).withTimeAtStartOfDay();
293
294        Map<String, String> mapCriteria = new HashMap<String, String>();
295        mapCriteria.put("prop4", SearchOperator.GREATER_THAN.op() + dateString);
296
297        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
298        assertNotNull(qbcBuilder);
299        QueryByCriteria qbc = qbcBuilder.build();
300
301        Predicate greaterThan = qbc.getPredicate();
302        assertTrue(greaterThan instanceof GreaterThanPredicate);
303        assertEquals(((GreaterThanPredicate) greaterThan).getValue().getValue(), date);
304    }
305
306    /**
307     * Criteria for LESS THAN dates should be before the start of the day on the date.
308     *
309     * <p>
310     * The value that should be checked is <2010-02-01 00:00:00.
311     * </p>
312     */
313    @Test
314    public void testGenerateCriteria_LessThanDate() {
315        String dateString = "1/2/2010";
316        DateTime date = DateTime.parse(dateString, formatter).withTimeAtStartOfDay();
317
318        Map<String, String> mapCriteria = new HashMap<String, String>();
319        mapCriteria.put("prop4", SearchOperator.LESS_THAN.op() + dateString);
320
321        QueryByCriteria.Builder qbcBuilder = generator.generateCriteria(TestClass.class, mapCriteria, false);
322        assertNotNull(qbcBuilder);
323        QueryByCriteria qbc = qbcBuilder.build();
324
325        Predicate lessThan = qbc.getPredicate();
326        assertTrue(lessThan instanceof LessThanPredicate);
327        assertEquals(((LessThanPredicate) lessThan).getValue().getValue(), date);
328    }
329
330    public static final class TestClass {
331
332        private String prop1;
333        private String prop2;
334        private String prop3;
335        private Date prop4;
336
337        public String getProp1() {
338            return prop1;
339        }
340
341        public void setProp1(String prop1) {
342            this.prop1 = prop1;
343        }
344
345        public String getProp2() {
346            return prop2;
347        }
348
349        public void setProp2(String prop2) {
350            this.prop2 = prop2;
351        }
352
353        public String getProp3() {
354            return prop3;
355        }
356
357        public void setProp3(String prop3) {
358            this.prop3 = prop3;
359        }
360
361        public Date getProp4() {
362            return prop4;
363        }
364
365        public void setProp4(Date prop4) {
366            this.prop4 = prop4;
367        }
368
369    }
370
371    private static final class DataObjectWrapperImpl<T> extends DataObjectWrapperBase<T> {
372        private DataObjectWrapperImpl(T dataObject, DataObjectMetadata metadata, DataObjectService dataObjectService,
373                ReferenceLinker referenceLinker) {
374            super(dataObject, metadata, dataObjectService, referenceLinker);
375        }
376    }
377
378}