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", 2, 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 
198                 if (field.getPropertyName().startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
199                     // this is the lower bound row
200                     assertFalse("Lower Field should not be inclusive", field.isInclusive());
201                 } else {
202                     // this is the upper bound row
203                     assertTrue("Upper Field should be inclusive", field.isInclusive());
204                     assertEquals("Field in row should be of data type date", DataType.DATE.toString().toLowerCase(), field.getFieldDataType());
205                 }
206 			}
207         } else {
208             assertEquals("Invalid number of search rows", 1, rows.size());
209             Row row = rows.get(0);
210             // check to make sure our datepicker field didn't make it to the search rows
211             assertEquals("Invalid number of fields", 1, row.getFields().size());
212             assertFalse("Field is the member of a range when ranges are not allowed",((Field)(row.getField(0))).isMemberOfRange());
213         }
214     }
215 
216     /*
217      * Test method for 'org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute.validateUserSearchInputs(Map)'
218      * This tests search value validation as well as bounds inclusivity; if lower/upper bound is specified, inclusivity of range vs. attribute definition is tested.
219      */
220     @Test  public void testValidateUserSearchRangeInputs() {
221         // <searchDefinition rangeSearch="true"/>
222         StandardGenericXMLSearchableAttribute searchAttribute = getAttribute("XMLSearchableAttributeStringRange");
223         ExtensionDefinition ed = createExtensionDefinition("XMLSearchableAttributeStringRange");
224 
225         RemotableAttributeError error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, ">= jack", null);
226         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, "<= jack.jack", "differ on upper bound inclusivity");
227         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, "< jack.jack", "Invalid first name");
228         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, ">= jack*jack", null);
229          // TODO: * gets stripped from value
230 
231         // <searchDefinition dataType="long" rangeSearch="true"/>
232         searchAttribute = getAttribute("XMLSearchableAttributeStdLongRange");
233         ed = createExtensionDefinition("XMLSearchableAttributeStdLongRange");
234 
235         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "<= " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString(), "differ on upper bound inclusivity");
236         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "< " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString(), null);
237         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString() + ".33","does not conform to standard validation for field type.");
238         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "<= jack*jack", "differ on upper bound inclusivity");
239         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "< jack*jack", "does not conform to standard validation for field type.");
240 
241         // <searchDefinition dataType="float">
242         //   <rangeDefinition inclusive="false">
243         //     <lower label="starting"/>
244         //     <upper label="ending"/>
245         //   </rangeDefinition>
246         // </searchDefinition>
247         searchAttribute = getAttribute("XMLSearchableAttributeStdFloatRange");
248         ed = createExtensionDefinition("XMLSearchableAttributeStdFloatRange");
249         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString(), "differ on lower bound inclusivity");
250         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "> " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString(), null);
251         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "<= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "a", "differ on upper bound inclusivity");
252         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "< " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "a", "does not conform to standard validation for field type.");
253         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, ">= " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "*", "differ on lower bound inclusivity");
254         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "> " + TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString() + "*", "does not conform to standard validation for field type.");
255 
256         // <searchDefinition dataType="datetime" datePicker="false">
257         //   <rangeDefinition inclusive="false">
258         //     <lower/>
259         //     <upper inclusive="true" datePicker="true"/>
260         //   </rangeDefinition>
261         // </searchDefinition>
262         searchAttribute = getAttribute("XMLSearchableAttributeStdDateTimeRange");
263         ed = createExtensionDefinition("XMLSearchableAttributeStdDateTimeRange");
264         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "<= " + DocumentSearchInternalUtils.getDisplayValueWithDateOnly(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE), null);
265         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, ">= 001/5/08", "differ on lower bound inclusivity");
266         assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "> 001/5/08", null);
267         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, ">= 41/5/08", "differ on lower bound inclusivity");
268         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "> 41/5/08", "does not conform to standard validation for field type.");
269         error = assertDocumentSearchCriteriaValidation(searchAttribute, ed, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "<= 01/02/20*", "does not conform to standard validation for field type.");
270     }
271 
272     /**
273      * Helper to assert document search criteria validation
274      */
275     protected RemotableAttributeError assertDocumentSearchCriteriaValidation(StandardGenericXMLSearchableAttribute attribute, ExtensionDefinition ed, String attrkey, String attrvalue, String expectedErrorMessage) {
276         DocumentSearchCriteria.Builder dscb = DocumentSearchCriteria.Builder.create();
277         dscb.addDocumentAttributeValue(attrkey, attrvalue);
278 
279         List<RemotableAttributeError> errors = attribute.validateDocumentAttributeCriteria(ed, dscb.build());
280 
281         if (expectedErrorMessage != null) {
282             assertEquals("Validation should return a single error message.", 1, errors.size());
283             if (StringUtils.isNotEmpty(expectedErrorMessage)) {
284                 assertTrue("Validation error is incorrect", errors.get(0).getMessage().contains(expectedErrorMessage));
285             }
286             return errors.get(0);
287         } else {
288             assertEquals("Validation should not have returned an error.", 0, errors.size());
289             return null;
290         }
291     }
292 
293     /**
294      * Sets up a doc for searching with ranged queries
295      */
296     protected WorkflowDocument setUpSearchableDoc() {
297         String documentTypeName = "SearchDocTypeRangeSearchDataType";
298     	DocumentType docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);
299         String userNetworkId = "rkirkend";
300         WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalIdForName(userNetworkId), documentTypeName);
301 
302         /*
303          *   Below we are using the keys and values from the custom searchable attribute classes' static constants but
304          *   this is only for convenience as those should always be valid values to test for.
305          */
306         // adding string searchable attribute
307         WorkflowAttributeDefinition.Builder stringXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStringRange");
308         stringXMLDef.addPropertyDefinition(TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeString.SEARCH_STORAGE_VALUE);
309         workflowDocument.addSearchableDefinition(stringXMLDef.build());
310         // adding long searchable attribute
311         WorkflowAttributeDefinition.Builder longXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdLongRangeInclusive");
312         longXMLDef.addPropertyDefinition(TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString());
313         workflowDocument.addSearchableDefinition(longXMLDef.build());
314         // adding float searchable attribute
315         WorkflowAttributeDefinition.Builder floatXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdFloatRangeInclusive");
316         floatXMLDef.addPropertyDefinition(TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString());
317         workflowDocument.addSearchableDefinition(floatXMLDef.build());
318         // adding string searchable attribute
319         WorkflowAttributeDefinition.Builder dateXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeStdDateTimeRangeInclusive");
320         dateXMLDef.addPropertyDefinition(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, DocumentSearchInternalUtils
321                 .getDisplayValueWithDateOnly(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE));
322         workflowDocument.addSearchableDefinition(dateXMLDef.build());
323 
324         workflowDocument.setTitle("Routing style");
325         workflowDocument.route("routing this document.");
326 
327         return WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(userNetworkId), workflowDocument.getDocumentId());
328     }
329 
330     /**
331      * Special result which indicates a range validation error is expected
332      */
333     private static final int EXPECT_EXCEPTION = -1;
334 
335     /**
336      * Helper that asserts range search results
337      */
338     protected void assertRangeSearchResults(String docType, String userId, String attrKey, String lowerBound, String upperBound, boolean upperBoundInclusive, int expected) throws WorkflowServiceErrorException {
339         DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
340         criteria.setDocumentTypeName(docType);
341 
342         addSearchableAttribute(criteria, attrKey, createSearchableAttributeRange(lowerBound, upperBound, upperBoundInclusive));
343 
344         DocumentSearchResults results;
345         try {
346             results = docSearchService.lookupDocuments(userId, criteria.build());
347             if (expected == EXPECT_EXCEPTION) fail("Error should have been thrown for invalid range");
348         } catch (WorkflowServiceErrorException e) {
349             if (expected == EXPECT_EXCEPTION) {
350                 return;
351             } else {
352                 throw e;
353             }
354         }
355 
356         assertEquals("Search results should have " + expected + " document(s).", expected,
357                 results.getSearchResults().size());
358     }
359 
360     @Test public void testStringRanges() throws Exception {
361         WorkflowDocument doc = setUpSearchableDoc();
362         String userId = doc.getInitiatorPrincipalId();
363         String docType = doc.getDocumentTypeName();
364 
365         assertRangeSearchResults(docType, userId, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, TestXMLSearchableAttributeString.SEARCH_STORAGE_VALUE, null, false, 1);
366     }
367 
368     @Test public void testLongRanges() throws Exception {
369         WorkflowDocument doc = setUpSearchableDoc();
370         String userId = doc.getInitiatorPrincipalId();
371         String docType = doc.getDocumentTypeName();
372 
373         String searchAttributeLongKey = TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY;
374         Long searchAttributeLongValue = TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.longValue();
375         Long longValueToUse = null;
376 
377         // test lower bound only
378         longValueToUse = searchAttributeLongValue; // lowerbound == value
379         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 1);
380 
381         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() - 1); // lowerbound below value
382         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 1);
383 
384         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() + 1); // lowerbound is above value
385         assertRangeSearchResults(docType, userId, searchAttributeLongKey, longValueToUse.toString(), null, false, 0);
386 
387         // test upper bound only
388         longValueToUse = searchAttributeLongValue; // upper bound == value
389         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 1);
390 
391         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() - 1); // upper bound < value
392         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 0);
393 
394         longValueToUse = Long.valueOf(searchAttributeLongValue.longValue() + 1); // upper bound > value
395         assertRangeSearchResults(docType, userId, searchAttributeLongKey, null, longValueToUse.toString(), true, 1);
396 
397         // test both bounds
398         // lowerbound == upperbound == value
399         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
400                                  Long.valueOf(searchAttributeLongValue.longValue()).toString(),
401                                  Long.valueOf(searchAttributeLongValue.longValue()).toString(), true, 1);
402 
403         // lower and upper bound > value
404         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
405                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(),
406                                  Long.valueOf(searchAttributeLongValue.longValue() + 4).toString(), true, 0);
407 
408         // lower and upper bound < value, but lower > upper
409         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
410                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(),
411                                  Long.valueOf(searchAttributeLongValue.longValue() - 4).toString(), true, EXPECT_EXCEPTION);
412         // lower and upper bound < value, lower < upper
413         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
414                                  Long.valueOf(searchAttributeLongValue.longValue() - 4).toString(),
415                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(), true, 0);
416 
417         // lower bound < value, upper bound > value
418         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
419                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(),
420                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(), true, 1);
421 
422         // upper < lower
423         assertRangeSearchResults(docType, userId, searchAttributeLongKey,
424                                  Long.valueOf(searchAttributeLongValue.longValue() + 2).toString(),
425                                  Long.valueOf(searchAttributeLongValue.longValue() - 2).toString(), true, EXPECT_EXCEPTION);
426     }
427 
428     @Test public void testFloatRanges() throws Exception {
429         WorkflowDocument doc = setUpSearchableDoc();
430         String userId = doc.getInitiatorPrincipalId();
431         String docType = doc.getDocumentTypeName();
432 
433         String searchAttributeFloatKey = TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY;
434         BigDecimal searchAttributeFloatValue = TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE;
435 
436         BigDecimal floatValueToUse = null;
437         // test lower bound only
438         floatValueToUse = searchAttributeFloatValue; // lower bound == value
439         // NOTE: original test asserted 0 results, mysql actually does match the value
440         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 1);
441 
442         floatValueToUse = searchAttributeFloatValue.subtract(BigDecimal.ONE); // lowerbound < value
443         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 1);
444 
445         floatValueToUse = searchAttributeFloatValue.add(BigDecimal.ONE); // lowerbound > value
446         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, floatValueToUse.toString(), null, false, 0);
447 
448         // test upper bound only
449         floatValueToUse = searchAttributeFloatValue; // upperbound == value (does not match float)
450         // NOTE: another case where original test had 0 results, but in fact we see a float match
451         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 1);
452 
453         floatValueToUse = searchAttributeFloatValue.subtract(BigDecimal.ONE); // upperbound < value
454         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 0);
455 
456         floatValueToUse = searchAttributeFloatValue.add(BigDecimal.ONE); // upperbound > value
457         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, null, floatValueToUse.toString(), true, 1);
458 
459         // test both bounds
460         // upper == lower == value
461         // NOTE: original case had 0 results, now seeing 1 result
462         // search generator invokes criteria which calls addNumericRangeCriteria when produces: (EXT1.VAL BETWEEN 123456.3456 AND 123456.3456)
463         assertRangeSearchResults(docType, userId, searchAttributeFloatKey, searchAttributeFloatValue.toString(), searchAttributeFloatValue.toString(), true, 1);
464 
465         // upper and lower > value
466         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
467                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(),
468                                  searchAttributeFloatValue.add(new BigDecimal(4)).toString(), true, 0);
469 
470         // upper and lower < value
471         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
472                                  searchAttributeFloatValue.subtract(new BigDecimal(4)).toString(),
473                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(), true, 0);
474 
475         // lower < value, upper > value
476         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
477                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(),
478                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(), true, 1);
479 
480         // upper < lower
481         assertRangeSearchResults(docType, userId, searchAttributeFloatKey,
482                                  searchAttributeFloatValue.add(new BigDecimal(2)).toString(),
483                                  searchAttributeFloatValue.subtract(new BigDecimal(2)).toString(), true, EXPECT_EXCEPTION);
484     }
485 
486     @Test public void testDateRanges() throws Exception {
487         WorkflowDocument doc = setUpSearchableDoc();
488         String userId = doc.getInitiatorPrincipalId();
489         String docType = doc.getDocumentTypeName();
490 
491         // begin datetime attribute value testing
492         // inclusive = ?
493         String searchAttributeDateTimeKey = TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY;
494         Calendar searchAttributeDateTimeValue = TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE.toGregorianCalendar();
495 
496         Calendar calendarValueToUse = null;
497         // test lower bound only
498         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone(); // lower == value
499         String valueToSearch = DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse));
500         // NOTE: matches now
501         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey, valueToSearch, null, false, 1);
502 
503         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
504         calendarValueToUse.add(Calendar.DATE, -1); // lower < value
505         valueToSearch = DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse));
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, 0);
512 
513         // test upper bound only
514         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone(); // upper == value (inclusivity true)
515         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
516                                  null,
517                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse)), true, 1);
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, 0);
524 
525         calendarValueToUse = (Calendar) searchAttributeDateTimeValue.clone();
526         calendarValueToUse.add(Calendar.DATE, 1); // upper > value
527         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
528                                  null,
529                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(calendarValueToUse)), true, 1);
530 
531         // test both bounds
532         Calendar lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
533         Calendar upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone(); // upper == lower == value (inclusivity true)
534         // NOTE: matches now
535         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
536                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
537                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 1);
538 
539         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
540         lowerBoundValue.add(Calendar.DATE, 2);
541         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
542         upperBoundValue.add(Calendar.DATE, 4);  // upper and lower > value
543         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
544                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
545                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 0);
546 
547         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
548         lowerBoundValue.add(Calendar.DATE, -4);
549         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
550         upperBoundValue.add(Calendar.DATE, -2); // upper and lower < value
551         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
552                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
553                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 0);
554 
555         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
556         lowerBoundValue.add(Calendar.DATE, -2);
557         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
558         upperBoundValue.add(Calendar.DATE, 2);  // lower < value, upper > value
559         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
560                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
561                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, 1);
562 
563         lowerBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
564         lowerBoundValue.add(Calendar.DATE, 2);
565         upperBoundValue = (Calendar) searchAttributeDateTimeValue.clone();
566         upperBoundValue.add(Calendar.DATE, -2); // lower > upper == error
567         assertRangeSearchResults(docType, userId, searchAttributeDateTimeKey,
568                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(lowerBoundValue)),
569                                  DocumentSearchInternalUtils.getDisplayValueWithDateOnly(SQLUtils.convertCalendar(upperBoundValue)), true, EXPECT_EXCEPTION);
570     }
571 
572     /*
573      * Tests the XML string attributes on range definitions, using a technique similar to that of the testSearchableAttributeRanges() unit test.
574      */
575     @Test public void testRangeDefinitionStringAttributes() throws Exception {
576         String documentTypeName = "RangeDefinitionTestDocType";
577     	DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
578         String principalName = "rkirkend";
579         String principalId = KimApiServiceLocator.getPersonService().getPersonByPrincipalName(principalName).getPrincipalId();
580         WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(principalId, documentTypeName);
581 
582         // adding inclusive-lower-bound searchable attribute
583         WorkflowAttributeDefinition.Builder inclusiveLowerXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithInclusiveLower");
584         inclusiveLowerXMLDef.addPropertyDefinition("textFieldWithInclusiveLower", "newvalue");
585         workflowDocument.addSearchableDefinition(inclusiveLowerXMLDef.build());
586         // adding case-sensitive searchable attribute
587         WorkflowAttributeDefinition.Builder caseSensitiveXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithCaseSensitivity");
588         caseSensitiveXMLDef.addPropertyDefinition("textFieldWithCaseSensitivity", "thevalue");
589         workflowDocument.addSearchableDefinition(caseSensitiveXMLDef.build());
590         // adding searchable attribute with overridden properties
591         WorkflowAttributeDefinition.Builder overridesXMLDef = WorkflowAttributeDefinition.Builder.create("TextFieldWithOverrides");
592         overridesXMLDef.addPropertyDefinition("textFieldWithOverrides", "SomeVal");
593         workflowDocument.addSearchableDefinition(overridesXMLDef.build());
594 
595         workflowDocument.setTitle("Range Def Test");
596         workflowDocument.route("routing range def doc.");
597 
598         workflowDocument = WorkflowDocumentFactory.loadDocument(principalId, workflowDocument.getDocumentId());
599 
600         // Verify that the "TextFieldWithInclusiveLower" attribute behaves as expected (lower-bound-inclusive and (by default) case-insensitive).
601         // both upper and lower bounds set to inclusive in attr definition
602         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithInclusiveLower",
603                 new String[] { "newvalue", ""        , ""        , "NEWVALUD", "newValuf", "newValuj", "newvaluf"},
604                 new String[] { ""        , "newvalue", "Newvaluf", "NEWVALUF", "newValud", "NEWVALUK", ""        },
605                 new int[]    { 1         , 1         , 1         , 1         , -1        , 0         , 0         });
606 
607         // Verify that the "TextFieldWithCaseSensitivity" attribute behaves as expected (bound-inclusive and case-sensitive).
608         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithCaseSensitivity",
609         		new String[] { "thevalue", ""        , ""        , "THEVALUD", "thevalud", "Thevalud", "THEVALUF"},
610         		new String[] { ""        , "thevalue", "Thevalue", "THEVALUF", "THEVALUF", "Thevaluf", ""        },
611         		new int[]    { 1         , 1         , 0         , 0         , -1        , 0         , 1         });
612 
613         // Verify that the "TextFieldWithOverrides" attribute behaves as expected
614         assertSearchBehavesAsExpected(docType, principalId, "textFieldWithOverrides",
615         		new String[] { "> someval", "> SomeVal", "<= SOMEVAL", "<= SomeVal", "SOMEVAK>..SomeVam", "SomeVam>..SOMEVAK", "SOMEVAM>..SomeVak", "> somevak", "<= SomeVak" },
616               //new String[] { ""         , ""         ,    "SOMEVAL",    "SomeVal", "SomeVam"          ,           "SOMEVAK",           "SomeVak", ""         ,    "SomeVak"},
617         		new int[]    { 0          , 0          , 1           , 1           , 1                  , -1                 , -1                 , 1          , 0        });
618     }
619 
620     /*
621      * A convenience method for performing document-searching operations involving range definitions. The array parameters must all be the same length,
622      * since this method will perform tests with the values given by entries located at the same indices.
623      * @param docType The document type to search for.
624      * @param principalId The ID of the user that will perform the search.
625      * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
626      * @param lowBounds The lower bounds to use in the tests; to ignore a lower bound for a test, use an empty String.
627      * @param upBounds The upper bounds to use in the tests; to ignore an upper bound for a test, use an empty String.
628      * @param resultSizes The expected number of documents to be returned by the search; use -1 to indicate that an exception should have occurred.
629      * @throws Exception
630      */
631     private void assertSearchBehavesAsExpected(DocumentType docType, String principalId, String fieldDefKey, final String[] lowBounds, final String[] upBounds, int[] resultSizes) throws Exception {
632         assertSearchResults(KEWServiceLocator.getDocumentSearchService(), docType, principalId, fieldDefKey, resultSizes, new Function<Integer, String>() {
633             @Override public String apply(@Nullable Integer index) {
634                 return createSearchableAttributeRange(lowBounds[index], upBounds[index], true);
635             }
636         });
637     }
638 
639     /*
640     * A convenience method for performing document-searching operations involving range definitions. The array parameters must all be the same length,
641     * since this method will perform tests with the values given by entries located at the same indices.
642     * @param docType The document type to search for.
643     * @param principalId The ID of the user that will perform the search.
644     * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
645     * @param expr array of search expressions
646     * @param resultSizes The expected number of documents to be returned by the search; use -1 to indicate that an exception should have occurred.
647     * @throws Exception
648     */
649     private void assertSearchBehavesAsExpected(DocumentType docType, String principalId, String fieldDefKey, final String[] expr, int[] resultSizes) throws Exception {
650         assertSearchResults(KEWServiceLocator.getDocumentSearchService(), docType, principalId, fieldDefKey, resultSizes, new Function<Integer, String>() {
651             @Override public String apply(@Nullable Integer index) {
652                 return expr[index];
653             }
654         });
655     }
656 
657     private void assertSearchResults(DocumentSearchService docSearchService, DocumentType docType, String principalId, String fieldDefKey, int[] resultSizes, Function<Integer, String> closure) {
658         DocumentSearchCriteria.Builder criteria = null;
659         DocumentSearchResults results = null;
660         for (int i = 0; i < resultSizes.length; i++) {
661             criteria = DocumentSearchCriteria.Builder.create();
662             criteria.setDocumentTypeName(docType.getName());
663 
664             addSearchableAttribute(criteria, fieldDefKey, closure.apply(i));
665 
666             try {
667                 results = docSearchService.lookupDocuments(principalId, criteria.build());
668                 if (resultSizes[i] < 0) {
669                     fail(fieldDefKey + "'s search at loop index " + i + " should have thrown an exception");
670                 }
671                 assertEquals(fieldDefKey
672                         + "'s search results at loop index "
673                         + i
674                         + " returned the wrong number of documents.", resultSizes[i], results.getSearchResults().size());
675             } catch (Exception ex) {
676                 if (resultSizes[i] >= 0) {
677                     fail(fieldDefKey + "'s search at loop index " + i + " should not have thrown an exception");
678                 }
679             }
680         }
681     }
682 }