View Javadoc
1   /**
2    * Copyright 2005-2014 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.kew.docsearch.xml;
17  
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertFalse;
20  import static org.junit.Assert.assertTrue;
21  import static org.junit.Assert.fail;
22  
23  import java.math.BigDecimal;
24  import java.util.Calendar;
25  import java.util.List;
26  
27  import javax.annotation.Nullable;
28  
29  import org.apache.commons.lang.StringUtils;
30  import org.junit.Before;
31  import org.junit.Test;
32  import org.kuali.rice.core.api.data.DataType;
33  import org.kuali.rice.core.api.uif.RemotableAttributeError;
34  import org.kuali.rice.core.api.uif.RemotableAttributeField;
35  import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
36  import org.kuali.rice.kew.api.WorkflowDocument;
37  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
38  import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
39  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
40  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
41  import org.kuali.rice.kew.api.extension.ExtensionDefinition;
42  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
43  import org.kuali.rice.kew.docsearch.DocumentSearchTestBase;
44  import org.kuali.rice.kew.docsearch.SearchableAttributeDateTimeValue;
45  import org.kuali.rice.kew.docsearch.SearchableAttributeFloatValue;
46  import org.kuali.rice.kew.docsearch.SearchableAttributeLongValue;
47  import org.kuali.rice.kew.docsearch.SearchableAttributeStringValue;
48  import org.kuali.rice.kew.docsearch.TestXMLSearchableAttributeDateTime;
49  import org.kuali.rice.kew.docsearch.TestXMLSearchableAttributeFloat;
50  import org.kuali.rice.kew.docsearch.TestXMLSearchableAttributeLong;
51  import org.kuali.rice.kew.docsearch.TestXMLSearchableAttributeString;
52  import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
53  import org.kuali.rice.kew.doctype.bo.DocumentType;
54  import org.kuali.rice.kew.doctype.service.DocumentTypeService;
55  import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
56  import org.kuali.rice.kew.service.KEWServiceLocator;
57  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
58  import org.kuali.rice.kns.util.FieldUtils;
59  import org.kuali.rice.kns.web.ui.Field;
60  import org.kuali.rice.kns.web.ui.Row;
61  import org.kuali.rice.krad.util.KRADConstants;
62  
63  import com.google.common.base.Function;
64  
65  /**
66   * Tests the StandardGenericXMLSearchableAttribute.
67   *
68   * KULWF-654: Tests the resolution to this issue by configuring a CustomActionListAttribute as well as a
69   * searchable attribute.
70   */
71  public class StandardGenericXMLSearchableAttributeRangesTest extends DocumentSearchTestBase {
72      private DocumentSearchService docSearchService;
73  
74      protected void loadTestData() throws Exception {
75          loadXmlFile("XmlConfig.xml");
76      }
77  
78      @Before
79      public void retrieveDocSearchSvc() {
80          docSearchService = (DocumentSearchService) KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_SEARCH_SERVICE);
81      }
82  
83      /*
84       * Test method for 'org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute.getSearchingRows()'
85       * @see StandardGenericXMLSearchableAttributeTest#testGetSearchFields
86       * @see StandardGenericXMLSearchableAttributeTest#tetBlankValidValuesOnKeyValues
87       */
88      @Test public void testGetSearchingRowsUsingRangeSearches() {
89          StandardGenericXMLSearchableAttribute searchAttribute = getAttribute("XMLSearchableAttributeStringRange");
90          ExtensionDefinition ed = createExtensionDefinition("XMLSearchableAttributeStringRange");
91  
92          String documentTypeName = "SearchDocType";
93          List<RemotableAttributeField> remotableAttributeFields = searchAttribute.getSearchFields(ed, documentTypeName);
94          List<Row> rows = FieldUtils.convertRemotableAttributeFields(remotableAttributeFields);
95  
96          if ((new SearchableAttributeStringValue()).allowsRangeSearches()) {
97              assertEquals("Invalid number of search rows", 2, rows.size());
98              for (int i = 1; i <= rows.size(); i++) {
99                  Row row = rows.get(i - 1);
100                 assertEquals("Invalid number of fields for search row " + i, 1, row.getFields().size());
101                 Field field = (Field)(row.getField(0));
102                 assertTrue("Field should be the member of a range",field.isMemberOfRange());
103                 if (i == 1) { // lower is inclusive
104                     assertTrue("Field should be inclusive", field.isInclusive());
105                 } else {
106                     assertFalse("Field should not be inclusive", field.isInclusive());
107                 }
108                 assertFalse("Field should not be using datepicker", field.isDatePicker());
109             }
110         } else {
111             assertEquals("Invalid number of search rows", 1, remotableAttributeFields.size());
112             Row row = rows.get(0);
113             assertEquals("Invalid number of fields for search row", 1, row.getFields().size());
114             assertFalse("Field is the member of a range when ranges are not allowed",((Field)row.getField(0)).isMemberOfRange());
115         }
116 
117         searchAttribute = getAttribute("XMLSearchableAttributeStdLongRange");
118         ed = createExtensionDefinition("XMLSearchableAttributeStdLongRange");
119         // search def :  rangeSearch=true
120         // range def  :
121         // upper def  :
122         // lower def  :
123         remotableAttributeFields = searchAttribute.getSearchFields(ed, documentTypeName);
124         rows = FieldUtils.convertRemotableAttributeFields(remotableAttributeFields);
125         if ((new SearchableAttributeLongValue()).allowsRangeSearches()) {
126             assertEquals("Invalid number of search rows", 2, rows.size());
127             for (int i = 1; i <= rows.size(); i++) {
128                 Row row = rows.get(i - 1);
129 	            assertEquals("Invalid number of fields for search row " + i, 1, row.getFields().size());
130 	            Field field = (Field)(row.getField(0));
131 	            assertTrue("Field should be the member of a range",field.isMemberOfRange());
132                 if (i == 1) { // lower is inclusive
133                     assertTrue("Field should be inclusive", field.isInclusive());
134                 } else {
135                     assertFalse("Field should not be inclusive", field.isInclusive());
136                 }
137 	            assertFalse("Field should not be using datepicker", field.isDatePicker());
138 			}
139         } else {
140             assertEquals("Invalid number of search rows", 1, rows.size());
141             Row row = rows.get(0);
142             assertEquals("Invalid number of fields for search row", 1, row.getFields().size());
143             Field field = (Field)(row.getField(0));
144             assertFalse("Field is the member of a range when ranges are not allowed",field.isMemberOfRange());
145             assertFalse("Field is inclusive when ranges are not allowed",field.isInclusive());
146             assertFalse("Field should not be using datepicker", field.isDatePicker());
147         }
148 
149         searchAttribute = getAttribute("XMLSearchableAttributeStdFloatRange");
150         ed = createExtensionDefinition("XMLSearchableAttributeStdFloatRange");
151         // search def :
152         // range def  :  inclusive=false
153         // upper def  :  label=ending
154         // lower def  :  label=starting
155         remotableAttributeFields = searchAttribute.getSearchFields(ed, documentTypeName);
156         rows = FieldUtils.convertRemotableAttributeFields(remotableAttributeFields);
157         if ((new SearchableAttributeFloatValue()).allowsRangeSearches()) {
158             assertEquals("Invalid number of search rows", 2, rows.size());
159             for (int i = 1; i <= rows.size(); i++) {
160                 Row row = rows.get(i - 1);
161 	            assertEquals("Invalid number of fields for search row " + i, 1, row.getFields().size());
162 	            Field field = (Field)(row.getField(0));
163 	            assertTrue("Upper and Lower Fields should be members of a range",field.isMemberOfRange());
164 	            assertFalse("Upper and Lower Fields should not be inclusive",field.isInclusive());
165 	            String labelValue = null;
166 	            if (field.getPropertyName().startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
167 	            	labelValue = "starting";
168 	            } else {
169 	            	labelValue = "ending";
170 	            }
171 	            assertEquals("Field label is incorrect.", labelValue, field.getFieldLabel());
172 	            assertFalse("Field should not be using datepicker", field.isDatePicker());
173 			}
174         } else {
175             assertEquals("Invalid number of search rows", 1, rows.size());
176             Row row = rows.get(0);
177             assertEquals("Invalid number of fields for search row", 1, row.getFields().size());
178             Field field = (Field)(row.getField(0));
179             assertFalse("Field is the member of a range when ranges are not allowed",field.isMemberOfRange());
180             assertFalse("Field should not be using datepicker", field.isDatePicker());
181         }
182 
183         searchAttribute = getAttribute("XMLSearchableAttributeStdDateTimeRange");
184         ed = createExtensionDefinition("XMLSearchableAttributeStdDateTimeRange");
185         // search def :  datePicker=false
186         // range def  :  inclusive=false
187         // upper def  :  inclusvie=true - datePicker=true
188         // lower def  :
189         remotableAttributeFields = searchAttribute.getSearchFields(ed, documentTypeName);
190         assertFalse(remotableAttributeFields.get(0).getAttributeLookupSettings().isLowerDatePicker());
191         assertTrue(remotableAttributeFields.get(0).getAttributeLookupSettings().isUpperDatePicker());
192         rows = FieldUtils.convertRemotableAttributeFields(remotableAttributeFields);
193         if ((new SearchableAttributeDateTimeValue()).allowsRangeSearches()) {
194             assertEquals("Invalid number of search rows", 2, rows.size());
195             for (int i = 0; i < rows.size(); i++) {
196                 Row row = rows.get(i);
197 	            assertTrue("Invalid number of fields for search row", row.getFields().size() == 1);
198 	            Field field = (Field)(row.getField(0));
199 	            assertTrue("Field should be the member of a range search", field.isRanged());
200 
201                 assertTrue("Field should be using datepicker field", field.isDatePicker());
202 
203                 if (field.getPropertyName().startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
204                     // this is the lower bound row
205                     assertFalse("Lower Field should not be inclusive", field.isInclusive());
206                 } else {
207                     // this is the upper bound row
208                     assertTrue("Upper Field should be inclusive", field.isInclusive());
209                     assertEquals("Field in row should be of data type date", DataType.DATE.toString().toLowerCase(), field.getFieldDataType());
210                 }
211 			}
212         } else {
213             assertEquals("Invalid number of search rows", 1, rows.size());
214             Row row = rows.get(0);
215             // check to make sure our datepicker field didn't make it to the search rows
216             assertEquals("Invalid number of fields", 1, row.getFields().size());
217             assertFalse("Field is the member of a range when ranges are not allowed",((Field)(row.getField(0))).isMemberOfRange());
218         }
219     }
220 
221     /*
222      * Test method for 'org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute.validateUserSearchInputs(Map)'
223      * This tests search value validation as well as bounds inclusivity; if lower/upper bound is specified, inclusivity of range vs. attribute definition is tested.
224      */
225     @Test  public void testValidateUserSearchRangeInputs() {
226         // <searchDefinition rangeSearch="true"/>
227         StandardGenericXMLSearchableAttribute searchAttribute = getAttribute("XMLSearchableAttributeStringRange");
228         ExtensionDefinition ed = createExtensionDefinition("XMLSearchableAttributeStringRange");
229 
230         RemotableAttributeError error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, ">= jack", null);
231         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, "<= jack.jack", "differ on upper bound inclusivity");
232         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, "< jack.jack", "Invalid first name");
233         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, ">= jack*jack", null);
234          // TODO: * gets stripped from value
235 
236         // <searchDefinition dataType="long" rangeSearch="true"/>
237         searchAttribute = getAttribute("XMLSearchableAttributeStdLongRange");
238         ed = createExtensionDefinition("XMLSearchableAttributeStdLongRange");
239 
240         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "<= " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString(), "differ on upper bound inclusivity");
241         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "< " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString(), null);
242         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString() + ".33","does not conform to standard validation for field type.");
243         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "<= jack*jack", "differ on upper bound inclusivity");
244         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "< jack*jack", "does not conform to standard validation for field type.");
245 
246         // <searchDefinition dataType="float">
247         //   <rangeDefinition inclusive="false">
248         //     <lower label="starting"/>
249         //     <upper label="ending"/>
250         //   </rangeDefinition>
251         // </searchDefinition>
252         searchAttribute = getAttribute("XMLSearchableAttributeStdFloatRange");
253         ed = createExtensionDefinition("XMLSearchableAttributeStdFloatRange");
254         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString(), "differ on lower bound inclusivity");
255         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "> " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString(), null);
256         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "<= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "a", "differ on upper bound inclusivity");
257         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "< " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "a", "does not conform to standard validation for field type.");
258         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "*", "differ on lower bound inclusivity");
259         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "> " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "*", "does not conform to standard validation for field type.");
260 
261         // <searchDefinition dataType="datetime" datePicker="false">
262         //   <rangeDefinition inclusive="false">
263         //     <lower/>
264         //     <upper inclusive="true" datePicker="true"/>
265         //   </rangeDefinition>
266         // </searchDefinition>
267         searchAttribute = getAttribute("XMLSearchableAttributeStdDateTimeRange");
268         ed = createExtensionDefinition("XMLSearchableAttributeStdDateTimeRange");
269         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "<= " + DocumentSearchInternalUtils.getDisplayValueWithDateOnly(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE), null);
270         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, ">= 001/5/08", "differ on lower bound inclusivity");
271         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "> 001/5/08", null);
272         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, ">= 41/5/08", "differ on lower bound inclusivity");
273         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "> 41/5/08", "does not conform to standard validation for field type.");
274         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "<= 01/02/20*", "does not conform to standard validation for field type.");
275     }
276 
277     /**
278      * Helper to assert document search criteria validation
279      */
280     protected RemotableAttributeError assertDocumentSearchCriteriaValidation(StandardGenericXMLSearchableAttribute attribute, ExtensionDefinition ed, String attrkey, String attrvalue, String expectedErrorMessage) {
281         DocumentSearchCriteria.Builder dscb = DocumentSearchCriteria.Builder.create();
282         dscb.addDocumentAttributeValue(attrkey, attrvalue);
283 
284         List<RemotableAttributeError> errors = attribute.validateDocumentAttributeCriteria(ed, dscb.build());
285 
286         if (expectedErrorMessage != null) {
287             assertEquals("Validation should return a single error message.", 1, errors.size());
288             if (StringUtils.isNotEmpty(expectedErrorMessage)) {
289                 assertTrue("Validation error is incorrect", errors.get(0).getMessage().contains(expectedErrorMessage));
290             }
291             return errors.get(0);
292         } else {
293             assertEquals("Validation should not have returned an error.", 0, errors.size());
294             return null;
295         }
296     }
297 
298     /**
299      * Sets up a doc for searching with ranged queries
300      */
301     protected WorkflowDocument setUpSearchableDoc() {
302         String documentTypeName = "SearchDocTypeRangeSearchDataType";
303     	DocumentType docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);
304         String userNetworkId = "rkirkend";
305         WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalIdForName(userNetworkId), documentTypeName);
306 
307         /*
308          *   Below we are using the keys and values from the custom searchable attribute classes' static constants but
309          *   this is only for convenience as those should always be valid values to test for.
310          */
311         // adding string searchable attribute
312         WorkflowAttributeDefinition.Builder stringXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStringRange");
313         stringXMLDef.addPropertyDefinition(TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeString.SEARCH_STORAGE_VALUE);
314         workflowDocument.addSearchableDefinition(stringXMLDef.build());
315         // adding long searchable attribute
316         WorkflowAttributeDefinition.Builder longXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdLongRangeInclusive");
317         longXMLDef.addPropertyDefinition(TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString());
318         workflowDocument.addSearchableDefinition(longXMLDef.build());
319         // adding float searchable attribute
320         WorkflowAttributeDefinition.Builder floatXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdFloatRangeInclusive");
321         floatXMLDef.addPropertyDefinition(TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString());
322         workflowDocument.addSearchableDefinition(floatXMLDef.build());
323         // adding string searchable attribute
324         WorkflowAttributeDefinition.Builder dateXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdDateTimeRangeInclusive");
325         dateXMLDef.addPropertyDefinition(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, DocumentSearchInternalUtils
326                 .getDisplayValueWithDateOnly(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE));
327         workflowDocument.addSearchableDefinition(dateXMLDef.build());
328 
329         workflowDocument.setTitle("Routing style");
330         workflowDocument.route("routing this document.");
331 
332         return WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(userNetworkId), workflowDocument.getDocumentId());
333     }
334 
335     /**
336      * Special result which indicates a range validation error is expected
337      */
338     private static final int EXPECT_EXCEPTION = -1;
339 
340     /**
341      * Helper that asserts range search results
342      */
343     protected void assertRangeSearchResults(String docType, String userId, String attrKey, String lowerBound, String upperBound, boolean upperBoundInclusive, int expected) throws WorkflowServiceErrorException {
344         DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
345         criteria.setDocumentTypeName(docType);
346 
347         addSearchableAttribute(criteria, attrKey, createSearchableAttributeRange(lowerBound, upperBound, upperBoundInclusive));
348 
349         DocumentSearchResults results;
350         try {
351             results = docSearchService.lookupDocuments(userId, criteria.build());
352             if (expected == EXPECT_EXCEPTION) fail("Error should have been thrown for invalid range");
353         } catch (WorkflowServiceErrorException e) {
354             if (expected == EXPECT_EXCEPTION) {
355                 return;
356             } else {
357                 throw e;
358             }
359         }
360 
361         assertEquals("Search results should have " + expected + " document(s).", expected,
362                 results.getSearchResults().size());
363     }
364 
365     @Test public void testStringRanges() throws Exception {
366         WorkflowDocument doc = setUpSearchableDoc();
367         String userId = doc.getInitiatorPrincipalId();
368         String docType = doc.getDocumentTypeName();
369 
370         assertRangeSearchResults(docType, userId, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeString.SEARCH_STORAGE_VALUE, null, false, 1);
371     }
372 
373     @Test public void testLongRanges() throws Exception {
374         WorkflowDocument doc = setUpSearchableDoc();
375         String userId = doc.getInitiatorPrincipalId();
376         String docType = doc.getDocumentTypeName();
377 
378         String searchAttributeLongKey = TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY;
379         Long searchAttributeLongValue = TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.longValue();
380         Long longValueToUse = null;
381 
382         // test lower bound only
383         longValueToUse = searchAttributeLongValue; // lowerbound == value
384         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 1);
385 
386         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() - 1); // lowerbound below value
387         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 1);
388 
389         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() + 1); // lowerbound is above value
390         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 0);
391 
392         // test upper bound only
393         longValueToUse = searchAttributeLongValue; // upper bound == value
394         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 1);
395 
396         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() - 1); // upper bound < value
397         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 0);
398 
399         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() + 1); // upper bound > value
400         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 1);
401 
402         // test both bounds
403         // lowerbound == upperbound == value
404         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
405                                  Long.valueOf(searchAttributeLongValue.longValue()).toString(),
406                                  Long.valueOf(searchAttributeLongValue.longValue()).toString(), true, 1);
407 
408         // lower and upper bound > value
409         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
410                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(),
411                                  Long.valueOf(searchAttributeLongValue.longValue() + 4).toString(), true, 0);
412 
413         // lower and upper bound < value, but lower > upper
414         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
415                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(),
416                                  Long.valueOf(searchAttributeLongValue.longValue() - 4).toString(), true, EXPECT_EXCEPTION);
417         // lower and upper bound < value, lower < upper
418         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
419                                  Long.valueOf(searchAttributeLongValue.longValue() - 4).toString(),
420                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(), true, 0);
421 
422         // lower bound < value, upper bound > value
423         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
424                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(),
425                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(), true, 1);
426 
427         // upper < lower
428         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
429                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(),
430                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(), true, EXPECT_EXCEPTION);
431     }
432 
433     @Test public void testFloatRanges() throws Exception {
434         WorkflowDocument doc = setUpSearchableDoc();
435         String userId = doc.getInitiatorPrincipalId();
436         String docType = doc.getDocumentTypeName();
437 
438         String searchAttributeFloatKey = TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY;
439         BigDecimal searchAttributeFloatValue = TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE;
440 
441         BigDecimal floatValueToUse = null;
442         // test lower bound only
443         floatValueToUse = searchAttributeFloatValue; // lower bound == value
444         // NOTE: original test asserted 0 results, mysql actually does match the value
445         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 1);
446 
447         floatValueToUse = searchAttributeFloatValue.subtract(BigDecimal.ONE); // lowerbound < value
448         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 1);
449 
450         floatValueToUse = searchAttributeFloatValue.add(BigDecimal.ONE); // lowerbound > value
451         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 0);
452 
453         // test upper bound only
454         floatValueToUse = searchAttributeFloatValue; // upperbound == value (does not match float)
455         // NOTE: another case where original test had 0 results, but in fact we see a float match
456         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 1);
457 
458         floatValueToUse = searchAttributeFloatValue.subtract(BigDecimal.ONE); // upperbound < value
459         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 0);
460 
461         floatValueToUse = searchAttributeFloatValue.add(BigDecimal.ONE); // upperbound > value
462         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 1);
463 
464         // test both bounds
465         // upper == lower == value
466         // NOTE: original case had 0 results, now seeing 1 result
467         // search generator invokes criteria which calls addNumericRangeCriteria when produces: (EXT1.VAL BETWEEN 123456.3456 AND 123456.3456)
468         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, searchAttributeFloatValue.toString(), searchAttributeFloatValue.toString(), true, 1);
469 
470         // upper and lower > value
471         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
472                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(),
473                                  searchAttributeFloatValue.add(new BigDecimal(4)).toString(), true, 0);
474 
475         // upper and lower < value
476         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
477                                  searchAttributeFloatValue.subtract(new BigDecimal(4)).toString(),
478                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(), true, 0);
479 
480         // lower < value, upper > value
481         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
482                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(),
483                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(), true, 1);
484 
485         // upper < lower
486         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
487                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(),
488                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(), true, EXPECT_EXCEPTION);
489     }
490 
491     @Test public void testDateRanges() throws Exception {
492         WorkflowDocument doc = setUpSearchableDoc();
493         String userId = doc.getInitiatorPrincipalId();
494         String docType = doc.getDocumentTypeName();
495 
496         // begin datetime attribute value testing
497         // inclusive = ?
498         String searchAttributeDateTimeKey = TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY;
499         Calendar searchAttributeDateTimeValue = TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE.toGregorianCalendar();
500 
501         Calendar calendarValueToUse = null;
502         // test lower bound only
503         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone(); // lower == value
504         String valueToSearch = DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse));
505         // NOTE: matches now
506         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey, valueToSearch, null, false, 1);
507 
508         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
509         calendarValueToUse.add(Calendar.DATE, -1); // lower < value
510         valueToSearch = DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse));
511         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey, valueToSearch, null, false, 1);
512 
513         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
514         calendarValueToUse.add(Calendar.DATE, 1); // lower > value
515         valueToSearch = DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse));
516         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey, valueToSearch, null, false, 0);
517 
518         // test upper bound only
519         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone(); // upper == value (inclusivity true)
520         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
521                                  null,
522                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse)), true, 1);
523 
524         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
525         calendarValueToUse.add(Calendar.DATE, -1); // upper < value
526         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
527                                  null,
528                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse)), true, 0);
529 
530         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
531         calendarValueToUse.add(Calendar.DATE, 1); // upper > value
532         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
533                                  null,
534                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse)), true, 1);
535 
536         // test both bounds
537         Calendar lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
538         Calendar upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone(); // upper == lower == value (inclusivity true)
539         // NOTE: matches now
540         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
541                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
542                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 1);
543 
544         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
545         lowerBoundValue.add(Calendar.DATE, 2);
546         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
547         upperBoundValue.add(Calendar.DATE, 4);  // upper and lower > value
548         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
549                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
550                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 0);
551 
552         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
553         lowerBoundValue.add(Calendar.DATE, -4);
554         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
555         upperBoundValue.add(Calendar.DATE, -2); // upper and lower < value
556         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
557                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
558                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 0);
559 
560         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
561         lowerBoundValue.add(Calendar.DATE, -2);
562         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
563         upperBoundValue.add(Calendar.DATE, 2);  // lower < value, upper > value
564         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
565                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
566                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 1);
567 
568         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
569         lowerBoundValue.add(Calendar.DATE, 2);
570         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
571         upperBoundValue.add(Calendar.DATE, -2); // lower > upper == error
572         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
573                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
574                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, EXPECT_EXCEPTION);
575     }
576 
577     /*
578      * Tests the XML string attributes on range definitions, using a technique similar to that of the testSearchableAttributeRanges() unit test.
579      */
580     @Test public void testRangeDefinitionStringAttributes() throws Exception {
581         String documentTypeName = "RangeDefinitionTestDocType";
582     	DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
583         String principalName = "rkirkend";
584         String principalId = KimApiServiceLocator.getPersonService().getPersonByPrincipalName(principalName).getPrincipalId();
585         WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(principalId, documentTypeName);
586 
587         // adding inclusive-lower-bound searchable attribute
588         WorkflowAttributeDefinition.Builder inclusiveLowerXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithInclusiveLower");
589         inclusiveLowerXMLDef.addPropertyDefinition("textFieldWithInclusiveLower", "newvalue");
590         workflowDocument.addSearchableDefinition(inclusiveLowerXMLDef.build());
591         // adding case-sensitive searchable attribute
592         WorkflowAttributeDefinition.Builder caseSensitiveXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithCaseSensitivity");
593         caseSensitiveXMLDef.addPropertyDefinition("textFieldWithCaseSensitivity", "thevalue");
594         workflowDocument.addSearchableDefinition(caseSensitiveXMLDef.build());
595         // adding searchable attribute with overridden properties
596         WorkflowAttributeDefinition.Builder overridesXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithOverrides");
597         overridesXMLDef.addPropertyDefinition("textFieldWithOverrides", "SomeVal");
598         workflowDocument.addSearchableDefinition(overridesXMLDef.build());
599 
600         workflowDocument.setTitle("Range Def Test");
601         workflowDocument.route("routing range def doc.");
602 
603         workflowDocument = WorkflowDocumentFactory.loadDocument(principalId, workflowDocument.getDocumentId());
604 
605         // Verify that the "TextFieldWithInclusiveLower" attribute behaves as expected (lower-bound-inclusive and (by default) case-insensitive).
606         // both upper and lower bounds set to inclusive in attr definition
607         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithInclusiveLower",
608                 new String[] { "newvalue", ""        , ""        , "NEWVALUD", "newValuf", "newValuj", "newvaluf"},
609                 new String[] { ""        , "newvalue", "Newvaluf", "NEWVALUF", "newValud", "NEWVALUK", ""        },
610                 new int[]    { 1         , 1         , 1         , 1         , -1        , 0         , 0         });
611 
612         // Verify that the "TextFieldWithCaseSensitivity" attribute behaves as expected (bound-inclusive and case-sensitive).
613         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithCaseSensitivity",
614         		new String[] { "thevalue", ""        , ""        , "THEVALUD", "thevalud", "Thevalud", "THEVALUF"},
615         		new String[] { ""        , "thevalue", "Thevalue", "THEVALUF", "THEVALUF", "Thevaluf", ""        },
616         		new int[]    { 1         , 1         , 0         , 0         , -1        , 0         , 1         });
617 
618         // Verify that the "TextFieldWithOverrides" attribute behaves as expected
619         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithOverrides",
620         		new String[] { "> someval", "> SomeVal", "<= SOMEVAL", "<= SomeVal", "SOMEVAK>..SomeVam", "SomeVam>..SOMEVAK", "SOMEVAM>..SomeVak", "> somevak", "<= SomeVak" },
621               //new String[] { ""         , ""         ,    "SOMEVAL",    "SomeVal", "SomeVam"          ,           "SOMEVAK",           "SomeVak", ""         ,    "SomeVak"},
622         		new int[]    { 0          , 0          , 1           , 1           , 1                  , -1                 , -1                 , 1          , 0        });
623     }
624 
625     /*
626      * A convenience method for performing document-searching operations involving range definitions. The array parameters must all be the same length,
627      * since this method will perform tests with the values given by entries located at the same indices.
628      * @param docType The document type to search for.
629      * @param principalId The ID of the user that will perform the search.
630      * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
631      * @param lowBounds The lower bounds to use in the tests; to ignore a lower bound for a test, use an empty String.
632      * @param upBounds The upper bounds to use in the tests; to ignore an upper bound for a test, use an empty String.
633      * @param resultSizes The expected number of documents to be returned by the search; use -1 to indicate that an exception should have occurred.
634      * @throws Exception
635      */
636     private void assertSearchBehavesAsExpected(DocumentType docType, String principalId, String fieldDefKey, final String[] lowBounds, final String[] upBounds, int[] resultSizes) throws Exception {
637         assertSearchResults(KEWServiceLocator.getDocumentSearchService(), docType, principalId, fieldDefKey, resultSizes, new Function<Integer, String>() {
638             @Override public String apply(@Nullable Integer index) {
639                 return createSearchableAttributeRange(lowBounds[index], upBounds[index], true);
640             }
641         });
642     }
643 
644     /*
645     * A convenience method for performing document-searching operations involving range definitions. The array parameters must all be the same length,
646     * since this method will perform tests with the values given by entries located at the same indices.
647     * @param docType The document type to search for.
648     * @param principalId The ID of the user that will perform the search.
649     * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
650     * @param expr array of search expressions
651     * @param resultSizes The expected number of documents to be returned by the search; use -1 to indicate that an exception should have occurred.
652     * @throws Exception
653     */
654     private void assertSearchBehavesAsExpected(DocumentType docType, String principalId, String fieldDefKey, final String[] expr, int[] resultSizes) throws Exception {
655         assertSearchResults(KEWServiceLocator.getDocumentSearchService(), docType, principalId, fieldDefKey, resultSizes, new Function<Integer, String>() {
656             @Override public String apply(@Nullable Integer index) {
657                 return expr[index];
658             }
659         });
660     }
661 
662     private void assertSearchResults(DocumentSearchService docSearchService, DocumentType docType, String principalId, String fieldDefKey, int[] resultSizes, Function<Integer, String> closure) {
663         DocumentSearchCriteria.Builder criteria = null;
664         DocumentSearchResults results = null;
665         for (int i = 0; i < resultSizes.length; i++) {
666             criteria = DocumentSearchCriteria.Builder.create();
667             criteria.setDocumentTypeName(docType.getName());
668 
669             addSearchableAttribute(criteria, fieldDefKey, closure.apply(i));
670 
671             try {
672                 results = docSearchService.lookupDocuments(principalId, criteria.build());
673                 if (resultSizes[i] < 0) {
674                     fail(fieldDefKey + "'s search at loop index " + i + " should have thrown an exception");
675                 }
676                 assertEquals(fieldDefKey
677                         + "'s search results at loop index "
678                         + i
679                         + " returned the wrong number of documents.", resultSizes[i], results.getSearchResults().size());
680             } catch (Exception ex) {
681                 if (resultSizes[i] >= 0) {
682                     fail(fieldDefKey + "'s search at loop index " + i + " should not have thrown an exception");
683                 }
684             }
685         }
686     }
687 }