001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.docsearch.xml;
017
018import com.google.common.base.Function;
019import org.apache.commons.collections.CollectionUtils;
020import org.apache.commons.lang.StringUtils;
021import org.kuali.rice.core.api.search.Range;
022import org.kuali.rice.core.api.search.SearchExpressionUtils;
023import org.kuali.rice.core.api.uif.DataType;
024import org.kuali.rice.core.api.uif.RemotableAbstractControl;
025import org.kuali.rice.core.api.uif.RemotableAttributeError;
026import org.kuali.rice.core.api.uif.RemotableAttributeField;
027import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
028import org.kuali.rice.core.api.uif.RemotableDatepicker;
029import org.kuali.rice.core.api.uif.RemotableHiddenInput;
030import org.kuali.rice.core.api.uif.RemotableQuickFinder;
031import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
032import org.kuali.rice.core.api.uif.RemotableSelect;
033import org.kuali.rice.core.api.uif.RemotableTextInput;
034import org.kuali.rice.core.api.util.KeyValue;
035import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
036import org.kuali.rice.core.web.format.Formatter;
037import org.kuali.rice.kew.api.KewApiConstants;
038import org.kuali.rice.kew.api.WorkflowRuntimeException;
039import org.kuali.rice.kew.api.document.DocumentWithContent;
040import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
041import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
042import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
043import org.kuali.rice.kew.api.extension.ExtensionDefinition;
044import org.kuali.rice.kew.docsearch.CaseAwareSearchableAttributeValue;
045import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
046import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
047import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
048import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
049import org.kuali.rice.kim.api.group.Group;
050import org.kuali.rice.kim.api.group.GroupService;
051import org.kuali.rice.kim.api.services.KimApiServiceLocator;
052import org.kuali.rice.kns.lookup.LookupUtils;
053import org.kuali.rice.krad.UserSession;
054import org.kuali.rice.krad.util.GlobalVariables;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.NamedNodeMap;
058import org.w3c.dom.Node;
059import org.w3c.dom.NodeList;
060import org.xml.sax.InputSource;
061
062import javax.management.modelmbean.XMLParseException;
063import javax.xml.parsers.DocumentBuilderFactory;
064import javax.xml.parsers.ParserConfigurationException;
065import javax.xml.xpath.XPath;
066import javax.xml.xpath.XPathConstants;
067import javax.xml.xpath.XPathExpressionException;
068import java.io.BufferedReader;
069import java.io.StringReader;
070import java.util.ArrayList;
071import java.util.LinkedHashMap;
072import java.util.Collection;
073import java.util.Collections;
074import java.util.HashMap;
075import java.util.List;
076import java.util.Map;
077import java.util.regex.Matcher;
078import java.util.regex.Pattern;
079
080
081/**
082 * Implementation of a {@code SearchableAttribute} whose configuration is driven from XML.
083 *
084 * XML configuration must be supplied in the ExtensionDefinition configuration parameter {@link KewApiConstants#ATTRIBUTE_XML_CONFIG_DATA}.
085 * Parsing of XML search configuration and generation of XML search content proceeds in an analogous fashion to {@link org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute}.
086 * Namely, if an <pre>searchingConfig/xmlSearchContent</pre> element is provided, its content is used as a template.  Otherwise a standard XML template is used.
087 * This template is parameterized with variables of the notation <pre>%name%</pre> which are resolved by <pre>searchingConfig/fieldDef[@name]</pre> definitions.
088 *
089 * The XML content is not validated, but it must be well formed.
090 *
091 * Example 1:
092 * <pre>
093 *     <searchingConfig>
094 *         <fieldDef name="def1" ...other attrs/>
095 *             ... other config
096 *         </fieldDef>
097 *         <fieldDef name="def2" ...other attrs/>
098 *             ... other config
099 *         </fieldDef>
100 *     </searchingConfig>
101 * </pre>
102 * Produces, when supplied with the workflow definition parameters: { def1: val1, def2: val2 }:
103 * <pre>
104 *     <xmlRouting>
105 *         <field name="def1"><value>val1</value></field>
106 *         <field name="def2"><value>val2</value></field>
107 *     </xmlRouting>
108 * </pre>
109 *
110 * Example 2:
111 * <pre>
112 *     <searchingConfig>
113 *         <xmlSearchContent>
114 *             <myGeneratedContent>
115 *                 <version>whatever</version>
116 *                 <anythingIWant>Once upon a %def1%...</anythingIWant>
117 *                 <conclusion>Happily ever %def2%.</conclusion>
118 *             </myGeneratedContent>
119 *         </xmlSearchContent>
120 *         <fieldDef name="def1" ...other attrs/>
121 *             ... other config
122 *         </fieldDef>
123 *         <fieldDef name="def2" ...other attrs/>
124 *             ... other config
125 *         </fieldDef>
126 *     </searchingConfig>
127 * </pre>
128 * Produces, when supplied with the workflow definition parameters: { def1: val1, def2: val2 }:
129 * <pre>
130 *     <myGeneratedContent>
131 *         <version>whatever</version>
132 *         <anythingIWant>Once upon a val1...</anythingIWant>
133 *         <conclusion>Happily ever val2.</conclusion>
134 *     </myGeneratedContent>
135 * </pre>
136 * @author Kuali Rice Team (rice.collab@kuali.org)
137 */
138public class StandardGenericXMLSearchableAttribute implements SearchableAttribute {
139
140        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLSearchableAttribute.class);
141    private static final String FIELD_DEF_E = "fieldDef";
142    /**
143     * Compile-time option that controls whether we check and return errors for field bounds options that conflict with searchable attribute configuration.
144     */
145    private static final boolean PEDANTIC_BOUNDS_VALIDATION = true;
146
147
148    @Override
149    public String generateSearchContent(ExtensionDefinition extensionDefinition, String documentTypeName, WorkflowAttributeDefinition attributeDefinition) {
150        Map<String, String> propertyDefinitionMap = attributeDefinition.getPropertyDefinitionsAsMap();
151        try {
152            XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
153            return content.generateSearchContent(propertyDefinitionMap);
154        } catch (XPathExpressionException e) {
155            LOG.error("error in getSearchContent ", e);
156            throw new RuntimeException("Error trying to find xml content with xpath expression", e);
157        } catch (Exception e) {
158            LOG.error("error in getSearchContent attempting to find xml search content", e);
159            throw new RuntimeException("Error trying to get xml search content.", e);
160        }
161    }
162
163    @Override
164    public List<DocumentAttribute> extractDocumentAttributes(ExtensionDefinition extensionDefinition, DocumentWithContent documentWithContent) {
165        List<DocumentAttribute> searchStorageValues = new ArrayList<DocumentAttribute>();
166        String fullDocumentContent = documentWithContent.getDocumentContent().getFullContent();
167        if (StringUtils.isBlank(documentWithContent.getDocumentContent().getFullContent())) {
168            LOG.warn("Empty Document Content found for document id: " + documentWithContent.getDocument().getDocumentId());
169            return searchStorageValues;
170        }
171        Document document;
172        try {
173            document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(fullDocumentContent))));
174        } catch (Exception e){
175            LOG.error("error parsing docContent: "+documentWithContent.getDocumentContent(), e);
176            throw new RuntimeException("Error trying to parse docContent: "+documentWithContent.getDocumentContent(), e);
177        }
178        XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
179        List<XMLSearchableAttributeContent.FieldDef> fields;
180        try {
181            fields = content.getFieldDefList();
182        } catch (XPathExpressionException xpee) {
183            throw new RuntimeException("Error parsing searchable attribute content", xpee);
184        } catch (ParserConfigurationException pce) {
185            throw new RuntimeException("Error parsing searchable attribute content", pce);
186        }
187        XPath xpath = XPathHelper.newXPath(document);
188        for (XMLSearchableAttributeContent.FieldDef field: fields) {
189            if (StringUtils.isNotEmpty(field.fieldEvaluationExpr)) {
190                List<String> values = new ArrayList<String>();
191                try {
192                    LOG.debug("Trying to retrieve node set with expression: '" + field.fieldEvaluationExpr + "'.");
193                    NodeList searchValues = (NodeList) xpath.evaluate(field.fieldEvaluationExpr, document.getDocumentElement(), XPathConstants.NODESET);
194                    // being that this is the standard xml attribute we will return the key with an empty value
195                    // so we can find it from a doc search using this key
196                    for (int j = 0; j < searchValues.getLength(); j++) {
197                        Node searchValue = searchValues.item(j);
198                        if (searchValue.getFirstChild() != null && (StringUtils.isNotEmpty(searchValue.getFirstChild().getNodeValue()))) {
199                            values.add(searchValue.getFirstChild().getNodeValue());
200                        }
201                    }
202                } catch (XPathExpressionException e) {
203                    LOG.debug("Could not retrieve node set with expression: '" + field.fieldEvaluationExpr + "'. Trying string return type.");
204                    //try for a string being returned from the expression.  This
205                    //seems like a poor way to determine our expression return type but
206                    //it's all I can come up with at the moment.
207                    try {
208                        String searchValue = (String) xpath.evaluate(field.fieldEvaluationExpr, document.getDocumentElement(), XPathConstants.STRING);
209                        if (StringUtils.isNotBlank(searchValue)) {
210                            values.add(searchValue);
211                        }
212                    } catch (XPathExpressionException xpee) {
213                        LOG.error("Error retrieving string with expression: '" + field.fieldEvaluationExpr + "'", xpee);
214                        throw new RuntimeException("Error retrieving string with expression: '" + field.fieldEvaluationExpr + "'", xpee);
215                    }
216                }
217
218                // remove any nulls
219                values.removeAll(Collections.singleton(null));
220                // being that this is the standard xml attribute we will return the key with an empty value
221                // so we can find it from a doc search using this key
222                if (values.isEmpty()) {
223                    values.add(null);
224                }
225                for (String value: values) {
226                    DocumentAttribute searchableValue = this.setupSearchableAttributeValue(field.searchDefinition.dataType, field.name, value);
227                    if (searchableValue != null) {
228                        searchStorageValues.add(searchableValue);
229                    }
230                }
231            }
232        }
233        return searchStorageValues;
234    }
235
236    private DocumentAttribute setupSearchableAttributeValue(String dataType, String key, String value) {
237        SearchableAttributeValue attValue = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(dataType);
238        if (attValue == null) {
239            String errorMsg = "Cannot find a SearchableAttributeValue associated with the data type '" + dataType + "'";
240            LOG.error("setupSearchableAttributeValue() " + errorMsg);
241            throw new RuntimeException(errorMsg);
242        }
243        value = (value != null) ? value.trim() : null;
244        if ( (StringUtils.isNotBlank(value)) && (!attValue.isPassesDefaultValidation(value)) ) {
245            String errorMsg = "SearchableAttributeValue with the data type '" + dataType + "', key '" + key + "', and value '" + value + "' does not pass default validation and cannot be saved to the database";
246            LOG.error("setupSearchableAttributeValue() " + errorMsg);
247            throw new RuntimeException(errorMsg);
248        }
249        attValue.setSearchableAttributeKey(key);
250        attValue.setupAttributeValue(value);
251        return attValue.toDocumentAttribute();
252    }
253
254    @Override
255    public List<RemotableAttributeField> getSearchFields(ExtensionDefinition extensionDefinition, String documentTypeName) {
256        List<RemotableAttributeField> searchFields = new ArrayList<RemotableAttributeField>();
257        List<SearchableAttributeValue> searchableAttributeValues = DocumentSearchInternalUtils.getSearchableAttributeValueObjectTypes();
258
259        XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
260        List<XMLSearchableAttributeContent.FieldDef> fields;
261        try {
262            fields = content.getFieldDefList();
263        } catch (XPathExpressionException xpee) {
264            throw new RuntimeException("Error parsing searchable attribute configuration", xpee);
265        } catch (ParserConfigurationException pce) {
266            throw new RuntimeException("Error parsing searchable attribute configuration", pce);
267        }
268        for (XMLSearchableAttributeContent.FieldDef field: fields) {
269            searchFields.add(convertFieldDef(field, searchableAttributeValues));
270        }
271
272        return searchFields;
273    }
274
275    /**
276     * Converts a searchable attribute FieldDef to a RemotableAttributeField
277     */
278    private RemotableAttributeField convertFieldDef(XMLSearchableAttributeContent.FieldDef field, Collection<SearchableAttributeValue> searchableAttributeValues) {
279        RemotableAttributeField.Builder fieldBuilder = RemotableAttributeField.Builder.create(field.name);
280
281        fieldBuilder.setLongLabel(field.title);
282
283        RemotableAttributeLookupSettings.Builder attributeLookupSettings = RemotableAttributeLookupSettings.Builder.create();
284        fieldBuilder.setAttributeLookupSettings(attributeLookupSettings);
285
286        // value
287        if (field.defaultValue != null) {
288            fieldBuilder.setDefaultValues(Collections.singletonList(field.defaultValue));
289        }
290
291        // Visibility
292        applyVisibility(fieldBuilder, attributeLookupSettings, field);
293
294        // Display
295        RemotableAbstractControl.Builder controlBuilder = constructControl(field.display.type, field.display.options);
296        fieldBuilder.setControl(controlBuilder);
297        if ("date".equals(field.display.type)) {
298            fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
299            fieldBuilder.setDataType(DataType.DATE);
300        }
301        if (!field.display.selectedOptions.isEmpty()) {
302            fieldBuilder.setDefaultValues(field.display.selectedOptions);
303        }
304
305        // resultcolumn
306        attributeLookupSettings.setInResults(field.isDisplayedInSearchResults());
307
308        // SearchDefinition
309        // data type operations
310        DataType dataType = DocumentSearchInternalUtils.convertValueToDataType(field.searchDefinition.dataType);
311        fieldBuilder.setDataType(dataType);
312        if (DataType.DATE == fieldBuilder.getDataType()) {
313            fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
314        }
315
316        boolean isRangeSearchField = isRangeSearchField(searchableAttributeValues, fieldBuilder.getDataType(), field);
317        if (isRangeSearchField) {
318            attributeLookupSettings.setRanged(true);
319            // we've established the search is ranged, so we can inspect the bounds
320            attributeLookupSettings.setLowerBoundInclusive(field.searchDefinition.lowerBound.inclusive);
321            attributeLookupSettings.setUpperBoundInclusive(field.searchDefinition.upperBound.inclusive);
322            attributeLookupSettings.setLowerLabel(field.searchDefinition.lowerBound.label);
323            attributeLookupSettings.setUpperLabel(field.searchDefinition.upperBound.label);
324            attributeLookupSettings.setLowerDatePicker(field.searchDefinition.lowerBound.datePicker);
325            attributeLookupSettings.setUpperDatePicker(field.searchDefinition.upperBound.datePicker);
326        }
327
328        Boolean caseSensitive = field.searchDefinition.getRangeBoundOptions().caseSensitive;
329        if (caseSensitive != null) {
330            attributeLookupSettings.setCaseSensitive(caseSensitive);
331        }
332
333        /**
334
335
336
337         String formatterClass = (searchDefAttributes.getNamedItem("formatterClass") == null) ? null : searchDefAttributes.getNamedItem("formatterClass").getNodeValue();
338         if (!StringUtils.isEmpty(formatterClass)) {
339         try {
340         myField.setFormatter((Formatter)Class.forName(formatterClass).newInstance());
341         } catch (InstantiationException e) {
342         LOG.error("Unable to get new instance of formatter class: " + formatterClass);
343         throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
344         }
345         catch (IllegalAccessException e) {
346         LOG.error("Unable to get new instance of formatter class: " + formatterClass);
347         throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
348         } catch (ClassNotFoundException e) {
349         LOG.error("Unable to find formatter class: " + formatterClass);
350         throw new RuntimeException("Unable to find formatter class: " + formatterClass);
351         }
352         }
353
354         */
355
356         String formatter = field.display.formatter == null ? null : field.display.formatter;
357         fieldBuilder.setFormatterName(formatter);
358
359        try {
360        // Register this formatter so that you can use it later in FieldUtils when processing
361            if(StringUtils.isNotEmpty(formatter)){
362                Formatter.registerFormatter(Class.forName(formatter), Class.forName(formatter));
363            }
364        } catch (ClassNotFoundException e) {
365         LOG.error("Unable to find formatter class: " + formatter);
366         throw new RuntimeException("Unable to find formatter class: " + formatter);
367         }
368
369
370        // Lookup
371        // XMLAttributeUtils.establishFieldLookup(fieldBuilder, childNode); // this code can probably die now that parsing has moved out to xmlsearchableattribcontent
372        if (field.lookup.dataObjectClass != null) {
373            RemotableQuickFinder.Builder quickFinderBuilder = RemotableQuickFinder.Builder.create(LookupUtils.getBaseLookupUrl(false), field.lookup.dataObjectClass);
374            quickFinderBuilder.setFieldConversions(field.lookup.fieldConversions);
375            fieldBuilder.getWidgets().add(quickFinderBuilder);
376        }
377
378        return fieldBuilder.build();
379    }
380
381
382    /**
383     * Determines whether the searchable field definition is a ranged search
384     * @param searchableAttributeValues the possible system {@link SearchableAttributeValue}s
385     * @param dataType the UI data type
386     * @return
387     */
388    private boolean isRangeSearchField(Collection<SearchableAttributeValue> searchableAttributeValues, DataType dataType, XMLSearchableAttributeContent.FieldDef field) {
389        for (SearchableAttributeValue attValue : searchableAttributeValues)
390        {
391            DataType attributeValueDataType = DocumentSearchInternalUtils.convertValueToDataType(attValue.getAttributeDataType());
392            if (attributeValueDataType == dataType) {
393                return isRangeSearchField(attValue, field);
394            }
395        }
396        String errorMsg = "Could not find searchable attribute value for data type '" + dataType + "'";
397        LOG.error("isRangeSearchField(List, String, NamedNodeMap, Node) " + errorMsg);
398        throw new WorkflowRuntimeException(errorMsg);
399    }
400
401    private boolean isRangeSearchField(SearchableAttributeValue searchableAttributeValue, XMLSearchableAttributeContent.FieldDef field) {
402        // this is a ranged search if
403        // 1) attribute value type allows ranged search
404        boolean allowRangedSearch = searchableAttributeValue.allowsRangeSearches();
405        // AND
406        // 2) the searchDefinition specifies a ranged search
407        return allowRangedSearch && field.searchDefinition.isRangedSearch();
408    }
409
410    /**
411     * Applies visibility settings to the RemotableAttributeField
412     */
413    private void applyVisibility(RemotableAttributeField.Builder fieldBuilder, RemotableAttributeLookupSettings.Builder attributeLookupSettings, XMLSearchableAttributeContent.FieldDef field) {
414        boolean visible = true;
415        // if visibility is explicitly set, use it
416        if (field.visibility.visible != null) {
417            visible = field.visibility.visible;
418        } else {
419            if (field.visibility.groupName != null) {
420                UserSession session = GlobalVariables.getUserSession();
421                if (session == null) {
422                    throw new WorkflowRuntimeException("UserSession is null!  Attempted to render the searchable attribute outside of an established session.");
423                }
424                GroupService groupService = KimApiServiceLocator.getGroupService();
425
426                Group group = groupService.getGroupByNamespaceCodeAndName(field.visibility.groupNamespace, field.visibility.groupName);
427                visible =  group == null ? false : groupService.isMemberOfGroup(session.getPerson().getPrincipalId(), group.getId());
428            }
429        }
430        String type = field.visibility.type;
431        if ("field".equals(type) || "fieldAndColumn".equals(type)) {
432            // if it's not visible, coerce this field to a hidden type
433            if (!visible) {
434                fieldBuilder.setControl(RemotableHiddenInput.Builder.create());
435            }
436        }
437        if ("column".equals(type) || "fieldAndColumn".equals(type)) {
438            attributeLookupSettings.setInResults(visible);
439        }
440    }
441
442    private RemotableAbstractControl.Builder constructControl(String type, Collection<KeyValue> options) {
443        RemotableAbstractControl.Builder control = null;
444        Map<String, String> optionMap = new LinkedHashMap<String, String>();
445        for (KeyValue option : options) {
446            optionMap.put(option.getKey(), option.getValue());
447        }
448        if ("text".equals(type) || "date".equals(type)) {
449            control = RemotableTextInput.Builder.create();
450        } else if ("select".equals(type)) {
451            control = RemotableSelect.Builder.create(optionMap);
452        } else if ("radio".equals(type)) {
453            control = RemotableRadioButtonGroup.Builder.create(optionMap);
454        } else if ("hidden".equals(type)) {
455            control = RemotableHiddenInput.Builder.create();
456        } else if ("multibox".equals(type)) {
457            RemotableSelect.Builder builder = RemotableSelect.Builder.create(optionMap);
458            builder.setMultiple(true);
459            control = builder;
460        } else {
461            throw new IllegalArgumentException("Illegal field type found: " + type);
462        }
463        return control;
464    }
465
466    @Override
467    public List<RemotableAttributeError> validateDocumentAttributeCriteria(ExtensionDefinition extensionDefinition, DocumentSearchCriteria documentSearchCriteria) {
468                List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
469        
470        Map<String, List<String>> documentAttributeValues = documentSearchCriteria.getDocumentAttributeValues();
471        if (documentAttributeValues == null || documentAttributeValues.isEmpty()) {
472            // nothing to validate...
473            return errors;
474        }
475
476        XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
477        List<XMLSearchableAttributeContent.FieldDef> fields;
478        try {
479            fields = content.getFieldDefList();
480        } catch (XPathExpressionException xpee) {
481            throw new RuntimeException("Error parsing searchable attribute configuration", xpee);
482        } catch (ParserConfigurationException pce) {
483            throw new RuntimeException("Error parsing searchable attribute configuration", pce);
484        }
485        if (fields.isEmpty()) {
486            LOG.warn("Could not find any field definitions (<" + FIELD_DEF_E + ">) or possibly a searching configuration (<searchingConfig>) for this XMLSearchAttribute");
487            return errors;
488        }
489
490        for (XMLSearchableAttributeContent.FieldDef field: fields) {
491            String fieldDefName = field.name;
492            String fieldDefTitle = field.title == null ? "" : field.title;
493
494            List<String> testObject = documentAttributeValues.get(fieldDefName);
495
496            if (testObject == null || testObject.isEmpty()) {
497                // no value to validate
498                // not checking for 'required' here since this is *search* criteria, and required field can be omitted
499                continue;
500            }
501
502            // What type of value is this searchable attribute field?
503            // get the searchable attribute value by using the data type
504            SearchableAttributeValue attributeValue = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(field.searchDefinition.dataType);
505            if (attributeValue == null) {
506                String errorMsg = "Cannot find SearchableAttributeValue for field data type '" + field.searchDefinition.dataType + "'";
507                LOG.error("validateUserSearchInputs() " + errorMsg);
508                throw new RuntimeException(errorMsg);
509            }
510
511            // 1) parse concrete values from possible range expressions
512            // 2) validate any resulting concrete values whether they were original arguments or parsed from range expressions
513            // 3) if the expression was a range expression, validate the logical validity of the range bounds
514
515            List<String> terminalValues = new ArrayList<String>();
516            List<Range> rangeValues = new ArrayList<Range>();
517
518            // we are assuming here that the only expressions evaluated against searchable attributes are simple
519            // non-compound expressions.  parsing compound expressions would require full grammar/parsing support
520            // and would probably be pretty absurd assuming these queries are coming from UIs.
521            // If they are not coming from the UI, do we need to support compound expressions?
522            for (String value: testObject) {
523                // is this a terminal value or does it look like a range?
524                if (value == null) {
525                    // assuming null values are not an error condition
526                    continue;
527                }
528                // this is just a war of attrition, need real parsing
529                String[] clauses = SearchExpressionUtils.splitOnClauses(value);
530                for (String clause: clauses) {
531                    // if it's not empty. see if it's a range
532                    Range r = null;
533                    if (StringUtils.isNotEmpty(value)) {
534                        r = SearchExpressionUtils.parseRange(value);
535                    }
536                    if (r != null) {
537                        // hey, it looks like a range
538                        boolean errs = false;
539                        if (!field.searchDefinition.isRangedSearch()) {
540                            errs = true;
541                            errors.add(RemotableAttributeError.Builder.create(field.name, "field does not support ranged searches but range search expression detected").build());
542                        } else {
543                            // only check bounds if range search is specified
544                            // XXX: FIXME: disabling these pedantic checks as they are causing annoying test breakages
545                            if (PEDANTIC_BOUNDS_VALIDATION) {
546                                // this is not actually an error. just disregard case-sensitivity for data types that don't support it
547                                /*if (!attributeValue.allowsCaseInsensitivity() && Boolean.FALSE.equals(field.searchDefinition.getRangeBoundOptions().caseSensitive)) {
548                                    errs = true;
549                                    errors.add(RemotableAttributeError.Builder.create(field.name, "attribute data type does not support case insensitivity but case-insensitivity specified in attribute definition").build());
550                                }*/
551                                if (r.getLowerBoundValue() != null && r.isLowerBoundInclusive() != field.searchDefinition.lowerBound.inclusive) {
552                                    errs = true;
553                                    errors.add(RemotableAttributeError.Builder.create(field.name, "range expression ('" + value + "') and attribute definition differ on lower bound inclusivity.  Range is: " + r.isLowerBoundInclusive() + " Attrib is: " + field.searchDefinition.lowerBound.inclusive).build());
554                                }
555                                if (r.getUpperBoundValue() != null && r.isUpperBoundInclusive() != field.searchDefinition.upperBound.inclusive) {
556                                    errs = true;
557                                    errors.add(RemotableAttributeError.Builder.create(field.name, "range expression ('" + value + "') and attribute definition differ on upper bound inclusivity.  Range is: " + r.isUpperBoundInclusive() + " Attrib is: " + field.searchDefinition.upperBound.inclusive).build());
558                                }
559                            }
560                        }
561
562                        if (!errs) {
563                            rangeValues.add(r);
564                        }
565                    } else {
566                        terminalValues.add(value);
567                    }
568                }
569            }
570
571            List<String> parsedValues = new ArrayList<String>();
572            // validate all values
573            for (String value: terminalValues) {
574                errors.addAll(performValidation(attributeValue, field, value, fieldDefTitle, parsedValues));
575            }
576            for (Range range: rangeValues) {
577                List<String> parsedLowerValues = new ArrayList<String>();
578                List<String> parsedUpperValues = new ArrayList<String>();
579                List<RemotableAttributeError> lowerErrors = performValidation(attributeValue, field,
580                        range.getLowerBoundValue(), constructRangeFieldErrorPrefix(field.title,
581                        field.searchDefinition.lowerBound), parsedLowerValues);
582                errors.addAll(lowerErrors);
583                List<RemotableAttributeError> upperErrors = performValidation(attributeValue, field, range.getUpperBoundValue(),
584                        constructRangeFieldErrorPrefix(field.title, field.searchDefinition.upperBound), parsedUpperValues);
585                errors.addAll(upperErrors);
586
587                // if both values check out, perform logical range validation
588                if (lowerErrors.isEmpty() && upperErrors.isEmpty()) {
589                    // TODO: how to handle multiple values?? doesn't really make sense
590                    String lowerBoundValue = parsedLowerValues.isEmpty() ? null : parsedLowerValues.get(0);
591                    String upperBoundValue = parsedUpperValues.isEmpty() ? null : parsedUpperValues.get(0);
592
593                    final Boolean rangeValid;
594                    // for the sake of string searches, make sure the bounds are uppercased before comparison if the search
595                    // is case sensitive.
596                    if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING.equals(field.searchDefinition.dataType)) {
597                        boolean caseSensitive = field.searchDefinition.getRangeBoundOptions().caseSensitive == null ? true : field.searchDefinition.getRangeBoundOptions().caseSensitive;
598                        rangeValid = ((CaseAwareSearchableAttributeValue) attributeValue).isRangeValid(lowerBoundValue, upperBoundValue, caseSensitive);
599                    } else {
600                        rangeValid = attributeValue.isRangeValid(lowerBoundValue, upperBoundValue);
601                    }
602
603                    if (rangeValid != null && !rangeValid) {
604                        String errorMsg = "The " + fieldDefTitle + " range is incorrect.  The " +
605                                (StringUtils.isNotBlank(field.searchDefinition.lowerBound.label) ? field.searchDefinition.lowerBound.label : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL)
606                                + " value entered must come before the " +
607                                (StringUtils.isNotBlank(field.searchDefinition.upperBound.label) ? field.searchDefinition.upperBound.label : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL)
608                                + " value";
609                        LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
610                        errors.add(RemotableAttributeError.Builder.create(fieldDefName, errorMsg).build());
611                    }
612                }
613            }
614       }
615        return errors;
616    }
617
618    private String constructRangeFieldErrorPrefix(String fieldDefLabel, XMLSearchableAttributeContent.FieldDef.SearchDefinition.RangeBound rangeBound) {
619        if ( StringUtils.isNotBlank(rangeBound.label) && StringUtils.isNotBlank(fieldDefLabel)) {
620            return fieldDefLabel + " " + rangeBound.label + " Field";
621        } else if (StringUtils.isNotBlank(fieldDefLabel)) {
622            return fieldDefLabel + " Range Field";
623        } else if (StringUtils.isNotBlank(rangeBound.label)) {
624            return "Range Field " + rangeBound.label + " Field";
625        }
626        return null;
627    }
628
629    /**
630     * Performs validation on a single DSC attribute value, running any defined custom validation regex after basic validation
631     * @param attributeValue the searchable attribute value type
632     * @param field the XMLSearchableAttributeContent field
633     * @param enteredValue the value to validate
634     * @param errorMessagePrefix a prefix for error messages
635     * @param resultingValues optional list of accumulated parsed values
636     * @return a (possibly empty) list of errors
637     */
638    private List<RemotableAttributeError> performValidation(SearchableAttributeValue attributeValue, final XMLSearchableAttributeContent.FieldDef field, String enteredValue, String errorMessagePrefix, List<String> resultingValues) {
639        return DocumentSearchInternalUtils.validateSearchFieldValue(field.name, attributeValue, enteredValue, errorMessagePrefix, resultingValues, new Function<String, Collection<RemotableAttributeError>>() {
640            @Override
641            public Collection<RemotableAttributeError> apply(String value) {
642                if (StringUtils.isNotEmpty(field.validation.regex)) {
643                    Pattern pattern = Pattern.compile(field.validation.regex);
644                    Matcher matcher = pattern.matcher(value);
645                    if (!matcher.matches()) {
646                        return Collections.singletonList(RemotableAttributeError.Builder.create(field.name, field.validation.message).build());
647                    }
648                }
649                return Collections.emptyList();
650            }
651        });
652    }
653
654    // preserved only for subclasses
655    protected Element getConfigXML(ExtensionDefinition extensionDefinition) {
656        try {
657            String xmlConfigData = extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
658            return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(xmlConfigData)))).getDocumentElement();
659        } catch (Exception e) {
660            String ruleAttrStr = (extensionDefinition == null ? null : extensionDefinition.getName());
661            LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
662            throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
663        }
664    }
665}