001/** 002 * Copyright 2005-2014 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.impl.document.search; 017 018import org.apache.commons.beanutils.PropertyUtils; 019import org.apache.commons.lang.ObjectUtils; 020import org.apache.commons.lang.StringUtils; 021import org.apache.log4j.Logger; 022import org.joda.time.DateTime; 023import org.kuali.rice.core.api.search.Range; 024import org.kuali.rice.core.api.search.SearchExpressionUtils; 025import org.kuali.rice.core.api.uif.AttributeLookupSettings; 026import org.kuali.rice.core.api.uif.RemotableAttributeField; 027import org.kuali.rice.kew.api.KEWPropertyConstants; 028import org.kuali.rice.kew.api.KewApiConstants; 029import org.kuali.rice.kew.api.document.DocumentStatus; 030import org.kuali.rice.kew.api.document.DocumentStatusCategory; 031import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria; 032import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract; 033import org.kuali.rice.kew.api.document.search.RouteNodeLookupLogic; 034import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils; 035import org.kuali.rice.kew.doctype.bo.DocumentType; 036import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration; 037import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils; 038import org.kuali.rice.kew.service.KEWServiceLocator; 039import org.kuali.rice.krad.util.KRADConstants; 040 041import java.lang.reflect.InvocationTargetException; 042import java.util.ArrayList; 043import java.util.Arrays; 044import java.util.Collection; 045import java.util.HashMap; 046import java.util.HashSet; 047import java.util.LinkedHashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.Set; 051 052/** 053 * Reference implementation of {@code DocumentSearchCriteriaTranslator}. 054 * 055 * @author Kuali Rice Team (rice.collab@kuali.org) 056 */ 057public class DocumentSearchCriteriaTranslatorImpl implements DocumentSearchCriteriaTranslator { 058 059 private static final Logger LOG = Logger.getLogger(DocumentSearchCriteriaTranslatorImpl.class); 060 061 private static final String DOCUMENT_STATUSES = "documentStatuses"; 062 private static final String ROUTE_NODE_LOOKUP_LOGIC = "routeNodeLookupLogic"; 063 064 /** 065 * Fields which translate directory from criteria strings to properties on the DocumentSearchCriteria. 066 */ 067 private static final String[] DIRECT_TRANSLATE_FIELD_NAMES = { 068 "documentId", 069 "applicationDocumentId", 070 "applicationDocumentStatus", 071 "initiatorPrincipalName", 072 "initiatorPrincipalId", 073 "viewerPrincipalName", 074 "viewerPrincipalId", 075 "groupViewerId", 076 "approverPrincipalName", 077 "approverPrincipalId", 078 "routeNodeName", 079 "documentTypeName", 080 "saveName", 081 "title", 082 "isAdvancedSearch" 083 }; 084 private static final Set<String> DIRECT_TRANSLATE_FIELD_NAMES_SET = 085 new HashSet<String>(Arrays.asList(DIRECT_TRANSLATE_FIELD_NAMES)); 086 087 private static final String[] DATE_RANGE_TRANSLATE_FIELD_NAMES = { 088 "dateCreated", 089 "dateLastModified", 090 "dateApproved", 091 "dateFinalized" 092 }; 093 private static final Set<String> DATE_RANGE_TRANSLATE_FIELD_NAMES_SET = 094 new HashSet<String>(Arrays.asList(DATE_RANGE_TRANSLATE_FIELD_NAMES)); 095 096 @Override 097 public DocumentSearchCriteria translateFieldsToCriteria(Map<String, String> fieldValues) { 098 099 DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create(); 100 List<String> documentAttributeFields = new ArrayList<String>(); 101 for (Map.Entry<String, String> field : fieldValues.entrySet()) { 102 try { 103 if (StringUtils.isNotBlank(field.getValue())) { 104 if (DIRECT_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) { 105 PropertyUtils.setNestedProperty(criteria, field.getKey(), field.getValue()); 106 } else if (DATE_RANGE_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) { 107 applyDateRangeField(criteria, field.getKey(), field.getValue()); 108 } else if (field.getKey().startsWith(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) { 109 documentAttributeFields.add(field.getKey()); 110 } 111 112 } 113 } catch (Exception e) { 114 throw new IllegalStateException("Failed to set document search criteria field: " + field.getKey(), e); 115 } 116 } 117 118 if (!documentAttributeFields.isEmpty()) { 119 translateDocumentAttributeFieldsToCriteria(fieldValues, documentAttributeFields, criteria); 120 } 121 122 String routeNodeLookupLogic = fieldValues.get(ROUTE_NODE_LOOKUP_LOGIC); 123 if (StringUtils.isNotBlank(routeNodeLookupLogic)) { 124 criteria.setRouteNodeLookupLogic(RouteNodeLookupLogic.valueOf(routeNodeLookupLogic)); 125 } 126 127 String documentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE); 128 if (StringUtils.isNotBlank(documentStatusesValue)) { 129 String[] documentStatuses = documentStatusesValue.split(","); 130 for (String documentStatus : documentStatuses) { 131 if (documentStatus.startsWith("category:")) { 132 String categoryCode = StringUtils.remove(documentStatus, "category:"); 133 criteria.getDocumentStatusCategories().add(DocumentStatusCategory.fromCode(categoryCode)); 134 } else { 135 criteria.getDocumentStatuses().add(DocumentStatus.fromCode(documentStatus)); 136 } 137 } 138 } 139 140 LinkedHashMap<String, List<String>> applicationDocumentStatusGroupings = 141 ApplicationDocumentStatusUtils.getApplicationDocumentStatusCategories(criteria.getDocumentTypeName()); 142 143 String applicationDocumentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS); 144 if (StringUtils.isNotBlank(applicationDocumentStatusesValue)) { 145 String[] applicationDocumentStatuses = applicationDocumentStatusesValue.split(","); 146 for (String applicationDocumentStatus : applicationDocumentStatuses) { 147 // KULRICE-7786: support for groups (categories) of application document statuses 148 if (applicationDocumentStatus.startsWith("category:")) { 149 String categoryCode = StringUtils.remove(applicationDocumentStatus, "category:"); 150 if (applicationDocumentStatusGroupings.containsKey(categoryCode)) { 151 criteria.getApplicationDocumentStatuses().addAll(applicationDocumentStatusGroupings.get(categoryCode)); 152 } 153 } else { 154 criteria.getApplicationDocumentStatuses().add(applicationDocumentStatus); 155 } 156 } 157 } 158 159 // blank the deprecated field out, it's not needed. 160 criteria.setApplicationDocumentStatus(null); 161 162 return criteria.build(); 163 } 164 165 /** 166 * Converts the DocumentSearchCriteria to a Map of values that can be applied to the Lookup form fields. 167 * @param criteria the criteria to translate 168 * @return a Map of values that can be applied to the Lookup form fields. 169 */ 170 public Map<String, String[]> translateCriteriaToFields(DocumentSearchCriteria criteria) { 171 Map<String, String[]> values = new HashMap<String, String[]>(); 172 173 for (String property: DIRECT_TRANSLATE_FIELD_NAMES) { 174 convertCriteriaPropertyToField(criteria, property, values); 175 } 176 177 for (String property: DATE_RANGE_TRANSLATE_FIELD_NAMES) { 178 convertCriteriaRangeField(criteria, property, values); 179 } 180 181 Map<String, List<String>> docAttrValues = criteria.getDocumentAttributeValues(); 182 if (!docAttrValues.isEmpty()) { 183 Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria); 184 for (Map.Entry<String, List<String>> entry: docAttrValues.entrySet()) { 185 AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(entry.getKey()); 186 if (lookupSettings != null && lookupSettings.isRanged()) { 187 convertAttributeRangeField(entry.getKey(), entry.getValue(), values); 188 } else { 189 values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + entry.getKey(), entry.getValue().toArray(new String[0])); 190 } 191 } 192 } 193 194 RouteNodeLookupLogic lookupLogic = criteria.getRouteNodeLookupLogic(); 195 if (lookupLogic != null) { 196 values.put(ROUTE_NODE_LOOKUP_LOGIC, new String[]{lookupLogic.name()}); 197 } 198 199 Collection<String> statuses = new ArrayList<String>(); 200 for (DocumentStatus status: criteria.getDocumentStatuses()) { 201 statuses.add(status.getCode()); 202 } 203 for (DocumentStatusCategory category: criteria.getDocumentStatusCategories()) { 204 statuses.add("category:" + category.getCode()); 205 } 206 values.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE, statuses.toArray(new String[0])); 207 208 return values; 209 } 210 211 /** 212 * Convert a ranged document search attribute field into a form field. 213 * This means: 214 * 0) the attribute field has been identified as a ranged attribute 215 * 1) we need to parse the attribute search expression to find upper and lower bounds 216 * 2) set upper and lower bounds in distinct form fields 217 * @param attrKey the attribute key 218 * @param attrValues the attribute value 219 */ 220 protected static void convertAttributeRangeField(String attrKey, List<String> attrValues, Map<String, String[]> values) { 221 String value = ""; 222 if (attrValues != null && !attrValues.isEmpty()) { 223 value = attrValues.get(0); 224 // can ranged attributes be multi-valued? 225 if (attrValues.size() > 1) { 226 LOG.warn("Encountered multi-valued ranged document search attribute '" + attrKey + "': " + attrValues); 227 } 228 } 229 Range range = SearchExpressionUtils.parseRange(value); 230 String lower; 231 String upper; 232 if (range != null) { 233 lower = range.getLowerBoundValue(); 234 upper = range.getUpperBoundValue(); 235 } else { 236 lower = null; 237 upper = value; 238 } 239 values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attrKey, new String[] { lower }); 240 values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + attrKey, new String[] { upper }); 241 } 242 243 /** 244 * Convenience method for converting a set of doc search criteria range fields into form fields 245 * @param criteria the dsc 246 * @param property the abstract property name 247 * @param values the form field values 248 */ 249 protected static void convertCriteriaRangeField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) { 250 convertCriteriaPropertyToField(criteria, property + "From", KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property, values); 251 convertCriteriaPropertyToField(criteria, property + "To", property, values); 252 } 253 254 /** 255 * Looks up a property on the criteria object and sets it as a key/value pair in the values Map 256 * @param criteria the DocumentSearchCriteria 257 * @param property the DocumentSearchCriteria property name and destination field name 258 * @param values the map of values to update 259 */ 260 protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) { 261 convertCriteriaPropertyToField(criteria, property, property, values); 262 } 263 264 /** 265 * Looks up a property on the criteria object and sets it as a key/value pair in the values Map 266 * @param criteria the DocumentSearchCriteria 267 * @param property the DocumentSearchCriteria property name 268 * @param fieldName the destination field name 269 * @param values the map of values to update 270 */ 271 protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, String fieldName, Map<String, String[]> values) { 272 try { 273 Object val = PropertyUtils.getProperty(criteria, property); 274 if (val != null) { 275 values.put(fieldName, new String[] { ObjectUtils.toString(val) }); 276 } 277 } catch (NoSuchMethodException nsme) { 278 LOG.error("Error reading property '" + property + "' of criteria", nsme); 279 } catch (InvocationTargetException ite) { 280 LOG.error("Error reading property '" + property + "' of criteria", ite); 281 } catch (IllegalAccessException iae) { 282 LOG.error("Error reading property '" + property + "' of criteria", iae); 283 284 } 285 } 286 287 protected void applyDateRangeField(DocumentSearchCriteria.Builder criteria, String fieldName, String fieldValue) throws Exception { 288 DateTime lowerDateTime = DocumentSearchInternalUtils.getLowerDateTimeBound(fieldValue); 289 DateTime upperDateTime = DocumentSearchInternalUtils.getUpperDateTimeBound(fieldValue); 290 if (lowerDateTime != null) { 291 PropertyUtils.setNestedProperty(criteria, fieldName + "From", lowerDateTime); 292 } 293 if (upperDateTime != null) { 294 PropertyUtils.setNestedProperty(criteria, fieldName + "To", upperDateTime); 295 } 296 } 297 298 /** 299 * Returns a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria 300 * @param criteria the doc search criteria 301 * @return a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria, empty otherwise 302 */ 303 protected Map<String, AttributeLookupSettings> getAttributeLookupSettings(DocumentSearchCriteriaContract criteria) { 304 String documentTypeName = criteria.getDocumentTypeName(); 305 Map<String, AttributeLookupSettings> attributeLookupSettingsMap = new HashMap<java.lang.String, AttributeLookupSettings>(); 306 307 if (StringUtils.isNotEmpty(documentTypeName)) { 308 DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName); 309 if (documentType != null) { 310 DocumentSearchCriteriaConfiguration configuration = KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration( 311 documentType); 312 if (configuration != null) { 313 List<RemotableAttributeField> remotableAttributeFields = configuration.getFlattenedSearchAttributeFields(); 314 for (RemotableAttributeField raf: remotableAttributeFields) { 315 attributeLookupSettingsMap.put(raf.getName(), raf.getAttributeLookupSettings()); 316 } 317 } 318 } else { 319 LOG.error("Searching against unknown document type '" + documentTypeName + "'; searchable attribute ranges will not work."); 320 } 321 } 322 323 return attributeLookupSettingsMap; 324 } 325 326 protected String translateRangePropertyToExpression(Map<String, String> fieldValues, String property, String prefix, AttributeLookupSettings settings) { 327 String lowerBoundValue = fieldValues.get(prefix + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property); 328 String upperBoundValue = fieldValues.get(prefix + property); 329 330 Range range = new Range(); 331 // defaults for general lookup/search 332 range.setLowerBoundInclusive(settings.isLowerBoundInclusive()); 333 range.setUpperBoundInclusive(settings.isUpperBoundInclusive()); 334 range.setLowerBoundValue(lowerBoundValue); 335 range.setUpperBoundValue(upperBoundValue); 336 337 String expr = range.toString(); 338 if (StringUtils.isEmpty(expr)) { 339 expr = upperBoundValue; 340 } 341 return expr; 342 } 343 344 protected void translateDocumentAttributeFieldsToCriteria(Map<String, String> fieldValues, List<String> fields, DocumentSearchCriteria.Builder criteria) { 345 Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria); 346 for (String field: fields) { 347 String documentAttributeName = field.substring(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX.length()); 348 // omit the synthetic lower bound field is there is an upper bound field, don't set back into doc attrib values 349 if (documentAttributeName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) { 350 String tempDocumentAttributeName = StringUtils.substringAfter(documentAttributeName,KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX) ; 351 String tempField = fieldValues.get(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + tempDocumentAttributeName); 352 if (StringUtils.isEmpty(tempField)) { 353 documentAttributeName = tempDocumentAttributeName; 354 } else { 355 continue; 356 } 357 } 358 String value = fieldValues.get(field); 359 AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(documentAttributeName); 360 if (lookupSettings != null && lookupSettings.isRanged()) { 361 value = translateRangePropertyToExpression(fieldValues, documentAttributeName, KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX, lookupSettings); 362 } 363 applyDocumentAttribute(criteria, documentAttributeName, value); 364 } 365 } 366 367 protected void applyDocumentAttribute(DocumentSearchCriteria.Builder criteria, String documentAttributeName, String attributeValue) { 368 criteria.addDocumentAttributeValue(documentAttributeName, attributeValue); 369 } 370 371}