View Javadoc

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