View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.service.impl;
17  
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertNotNull;
20  import static org.junit.Assert.assertTrue;
21  import static org.junit.Assert.fail;
22  import static org.mockito.Matchers.any;
23  import static org.mockito.Mockito.mock;
24  import static org.mockito.Mockito.when;
25  
26  import java.sql.Date;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.joda.time.DateTime;
32  import org.joda.time.LocalDate;
33  import org.joda.time.format.DateTimeFormatter;
34  import org.junit.Before;
35  import org.junit.Test;
36  import org.junit.runner.RunWith;
37  import org.kuali.rice.core.api.criteria.AndPredicate;
38  import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
39  import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
40  import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
41  import org.kuali.rice.core.api.criteria.LessThanPredicate;
42  import org.kuali.rice.core.api.criteria.LikeIgnoreCasePredicate;
43  import org.kuali.rice.core.api.criteria.LikePredicate;
44  import org.kuali.rice.core.api.criteria.OrPredicate;
45  import org.kuali.rice.core.api.criteria.Predicate;
46  import org.kuali.rice.core.api.criteria.QueryByCriteria;
47  import org.kuali.rice.core.api.datetime.DateTimeService;
48  import org.kuali.rice.core.api.search.SearchOperator;
49  import org.kuali.rice.krad.data.DataObjectService;
50  import org.kuali.rice.krad.data.DataObjectWrapper;
51  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
52  import org.kuali.rice.krad.data.provider.impl.DataObjectWrapperBase;
53  import org.kuali.rice.krad.data.util.ReferenceLinker;
54  import org.kuali.rice.krad.datadictionary.DataDictionary;
55  import org.kuali.rice.krad.service.DataDictionaryService;
56  import org.mockito.InjectMocks;
57  import org.mockito.Mock;
58  import org.mockito.invocation.InvocationOnMock;
59  import org.mockito.runners.MockitoJUnitRunner;
60  import org.mockito.stubbing.Answer;
61  import org.springframework.format.datetime.joda.DateTimeFormatterFactory;
62  
63  /**
64   * Tests the functionality of the LookupCriteriaGeneratorImpl.
65   *
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   */
68  @RunWith(MockitoJUnitRunner.class)
69  public class LookupCriteriaGeneratorImplTest {
70  
71      @Mock DataDictionary dataDictionary;
72      @Mock DataDictionaryService dataDictionaryService;
73      @Mock DataObjectService dataObjectService;
74      @Mock ReferenceLinker referenceLinker;
75      @Mock DateTimeService dateTimeService;
76  
77      @InjectMocks private LookupCriteriaGeneratorImpl generator = new LookupCriteriaGeneratorImpl();
78  
79      private static final DateTimeFormatter formatter = new DateTimeFormatterFactory("mm/dd/yyyy").createDateTimeFormatter();
80  
81      @Before
82      public void setUp() throws Exception {
83          when(dataDictionaryService.getDataDictionary()).thenReturn(dataDictionary);
84  
85          // Make this property force upper-case so that the LIKE comparison generated from it will
86          // be converted to case sensitive.
87          when(dataDictionaryService.isAttributeDefined(TestClass.class, "prop2")).thenReturn(Boolean.TRUE);
88          when(dataDictionaryService.getAttributeForceUppercase(TestClass.class, "prop2")).thenReturn(Boolean.TRUE);
89  
90          when(dataObjectService.wrap(any(TestClass.class))).thenAnswer(new Answer<DataObjectWrapper<TestClass>>() {
91              @Override
92              public DataObjectWrapper<TestClass> answer(InvocationOnMock invocation) throws Throwable {
93                  return new DataObjectWrapperImpl<TestClass>((TestClass)invocation.getArguments()[0],
94                          mock(DataObjectMetadata.class), dataObjectService, referenceLinker);
95              }
96          });
97          when(dateTimeService.convertToSqlDate(any(String.class))).thenAnswer(new Answer<Date>() {
98              @Override
99              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 }