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}