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.krad.workflow.attribute;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.log4j.Logger;
021import org.kuali.rice.core.api.config.property.ConfigurationService;
022import org.kuali.rice.core.api.uif.RemotableAttributeError;
023import org.kuali.rice.core.api.uif.RemotableAttributeField;
024import org.kuali.rice.kew.api.KewApiConstants;
025import org.kuali.rice.kew.api.document.DocumentWithContent;
026import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
027import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
028import org.kuali.rice.kew.api.document.attribute.DocumentAttributeString;
029import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
030import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
031import org.kuali.rice.kew.api.exception.WorkflowException;
032import org.kuali.rice.kew.api.extension.ExtensionDefinition;
033import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
034import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
035import org.kuali.rice.kns.document.MaintenanceDocument;
036import org.kuali.rice.kns.lookup.LookupUtils;
037import org.kuali.rice.kns.maintenance.KualiGlobalMaintainableImpl;
038import org.kuali.rice.kns.maintenance.Maintainable;
039import org.kuali.rice.kns.service.DictionaryValidationService;
040import org.kuali.rice.kns.service.KNSServiceLocator;
041import org.kuali.rice.kns.util.FieldUtils;
042import org.kuali.rice.kns.web.ui.Field;
043import org.kuali.rice.kns.web.ui.Row;
044import org.kuali.rice.krad.bo.BusinessObject;
045import org.kuali.rice.krad.bo.DocumentHeader;
046import org.kuali.rice.krad.bo.GlobalBusinessObject;
047import org.kuali.rice.krad.bo.PersistableBusinessObject;
048import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
049import org.kuali.rice.krad.datadictionary.DocumentEntry;
050import org.kuali.rice.krad.datadictionary.SearchingAttribute;
051import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
052import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
053import org.kuali.rice.krad.document.Document;
054import org.kuali.rice.krad.service.DataDictionaryRemoteFieldService;
055import org.kuali.rice.krad.service.DocumentService;
056import org.kuali.rice.krad.service.KRADServiceLocator;
057import org.kuali.rice.krad.service.KRADServiceLocatorInternal;
058import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
059import org.kuali.rice.krad.util.ErrorMessage;
060import org.kuali.rice.krad.util.GlobalVariables;
061import org.kuali.rice.krad.util.KRADPropertyConstants;
062import org.kuali.rice.krad.util.MessageMap;
063import org.kuali.rice.krad.util.ObjectUtils;
064import org.kuali.rice.krad.workflow.service.WorkflowAttributePropertyResolutionService;
065
066import java.text.MessageFormat;
067import java.util.ArrayList;
068import java.util.LinkedHashMap;
069import java.util.List;
070import java.util.Map;
071
072public class DataDictionarySearchableAttribute implements SearchableAttribute {
073
074    private static final long serialVersionUID = 173059488280366451L;
075        private static final Logger LOG = Logger.getLogger(DataDictionarySearchableAttribute.class);
076    public static final String DATA_TYPE_BOOLEAN = "boolean";
077
078    @Override
079    public String generateSearchContent(ExtensionDefinition extensionDefinition,
080            String documentTypeName,
081            WorkflowAttributeDefinition attributeDefinition) {
082        return "";
083    }
084
085    @Override
086    public List<DocumentAttribute> extractDocumentAttributes(ExtensionDefinition extensionDefinition,
087            DocumentWithContent documentWithContent) {
088        List<DocumentAttribute> attributes = new ArrayList<DocumentAttribute>();
089
090        String docId = documentWithContent.getDocument().getDocumentId();
091
092        DocumentService docService = KRADServiceLocatorWeb.getDocumentService();
093        Document doc = null;
094        try  {
095            doc = docService.getByDocumentHeaderIdSessionless(docId);
096        } catch (WorkflowException we) {
097                LOG.error( "Unable to retrieve document " + docId + " in getSearchStorageValues()", we);
098        }
099
100        String attributeValue = "";
101        if ( doc != null ) {
102                if ( doc.getDocumentHeader() != null ) {
103                attributeValue = doc.getDocumentHeader().getDocumentDescription();
104                } else {
105                        attributeValue = "null document header";
106                }
107        } else {
108                attributeValue = "null document";
109        }
110        DocumentAttributeString attribute = DocumentAttributeFactory.createStringAttribute("documentDescription", attributeValue);
111        attributes.add(attribute);
112
113        attributeValue = "";
114        if ( doc != null ) {
115                if ( doc.getDocumentHeader() != null ) {
116                attributeValue = doc.getDocumentHeader().getOrganizationDocumentNumber();
117                } else {
118                        attributeValue = "null document header";
119                }
120        } else {
121                attributeValue = "null document";
122        }
123        attribute = DocumentAttributeFactory.createStringAttribute("organizationDocumentNumber", attributeValue);
124        attributes.add(attribute);
125
126        if ( doc != null && doc instanceof MaintenanceDocument) {
127            final Class<? extends BusinessObject> businessObjectClass = getBusinessObjectClass(
128                    documentWithContent.getDocument().getDocumentTypeName());
129            if (businessObjectClass != null) {
130                if (GlobalBusinessObject.class.isAssignableFrom(businessObjectClass)) {
131                    final GlobalBusinessObject globalBO = retrieveGlobalBusinessObject(docId, businessObjectClass);
132
133                    if (globalBO != null) {
134                        attributes.addAll(findAllDocumentAttributesForGlobalBusinessObject(globalBO));
135                    }
136                } else {
137                    attributes.addAll(parsePrimaryKeyValuesFromDocument(businessObjectClass, (MaintenanceDocument)doc));
138                }
139
140            }
141        }
142        if ( doc != null ) {
143            DocumentEntry docEntry = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDocumentEntry(
144                    documentWithContent.getDocument().getDocumentTypeName());
145            if ( docEntry != null ) {
146                        WorkflowAttributes workflowAttributes = docEntry.getWorkflowAttributes();
147                        WorkflowAttributePropertyResolutionService waprs = KRADServiceLocatorInternal
148                        .getWorkflowAttributePropertyResolutionService();
149                        attributes.addAll(waprs.resolveSearchableAttributeValues(doc, workflowAttributes));
150            } else {
151                LOG.error("Unable to find DD document entry for document type: " + documentWithContent.getDocument()
152                        .getDocumentTypeName());
153            }
154        }
155        return attributes;
156    }
157
158    @Override
159    public List<RemotableAttributeField> getSearchFields(ExtensionDefinition extensionDefinition, String documentTypeName) {
160        List<Row> searchRows = getSearchingRows(documentTypeName);
161        return FieldUtils.convertRowsToAttributeFields(searchRows);
162    }
163
164    /**
165     * Produces legacy KNS rows to use for search attributes.  This method was left intact to help ease conversion
166     * until KNS is replaced with KRAD.
167     */
168    protected List<Row> getSearchingRows(String documentTypeName) {
169
170        List<Row> docSearchRows = new ArrayList<Row>();
171
172        Class boClass = DocumentHeader.class;
173
174        Field descriptionField = FieldUtils.getPropertyField(boClass, "documentDescription", true);
175        descriptionField.setFieldDataType(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING);
176
177        Field orgDocNumberField = FieldUtils.getPropertyField(boClass, "organizationDocumentNumber", true);
178        orgDocNumberField.setFieldDataType(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING);
179
180        List<Field> fieldList = new ArrayList<Field>();
181        fieldList.add(descriptionField);
182        docSearchRows.add(new Row(fieldList));
183
184        fieldList = new ArrayList<Field>();
185        fieldList.add(orgDocNumberField);
186        docSearchRows.add(new Row(fieldList));
187
188
189        DocumentEntry entry =
190                KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDocumentEntry(documentTypeName);
191        if (entry  == null)
192            return docSearchRows;
193        if (entry instanceof MaintenanceDocumentEntry) {
194            Class<? extends BusinessObject> businessObjectClass = getBusinessObjectClass(documentTypeName);
195            Class<? extends Maintainable> maintainableClass = getMaintainableClass(documentTypeName);
196
197            KualiGlobalMaintainableImpl globalMaintainable = null;
198            try {
199                globalMaintainable = (KualiGlobalMaintainableImpl)maintainableClass.newInstance();
200                businessObjectClass = globalMaintainable.getPrimaryEditedBusinessObjectClass();
201            } catch (Exception ie) {
202                //was not a globalMaintainable.
203            }
204
205            if (businessObjectClass != null)
206                docSearchRows.addAll(createFieldRowsForBusinessObject(businessObjectClass));
207        }
208
209        WorkflowAttributes workflowAttributes = entry.getWorkflowAttributes();
210        if (workflowAttributes != null)
211            docSearchRows.addAll(createFieldRowsForWorkflowAttributes(workflowAttributes));
212
213        return docSearchRows;
214    }
215
216    @Override
217    public List<RemotableAttributeError> validateDocumentAttributeCriteria(ExtensionDefinition extensionDefinition,
218            DocumentSearchCriteria documentSearchCriteria) {
219        List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
220        DictionaryValidationService validationService = KNSServiceLocator.getKNSDictionaryValidationService();
221
222        // validate the document attribute values
223        Map<String, List<String>> documentAttributeValues = documentSearchCriteria.getDocumentAttributeValues();
224        for (String key : documentAttributeValues.keySet()) {
225            List<String> values = documentAttributeValues.get(key);
226            if (CollectionUtils.isNotEmpty(values)) {
227                for (String value : values) {
228                    if (StringUtils.isNotBlank(value)) {
229                        validationService.validateAttributeFormat(documentSearchCriteria.getDocumentTypeName(), key, value, key);
230                    }
231                }
232            }
233        }
234
235        retrieveValidationErrorsFromGlobalVariables(validationErrors);
236
237        return validationErrors;
238    }
239
240    /**
241     * Retrieves validation errors from GlobalVariables MessageMap and appends to the given list of RemotableAttributeError
242     * @param validationErrors list to append validation errors
243     */
244    protected void retrieveValidationErrorsFromGlobalVariables(List<RemotableAttributeError> validationErrors) {
245        // can we use KualiConfigurationService?  It seemed to be used elsewhere...
246        ConfigurationService configurationService = KRADServiceLocator.getKualiConfigurationService();
247
248        if(GlobalVariables.getMessageMap().hasErrors()){
249            MessageMap deepCopy = (MessageMap)ObjectUtils.deepCopy(GlobalVariables.getMessageMap());
250            for (String errorKey : deepCopy.getErrorMessages().keySet()) {
251                List<ErrorMessage> errorMessages = deepCopy.getErrorMessages().get(errorKey);
252                if (CollectionUtils.isNotEmpty(errorMessages)) {
253                    List<String> errors = new ArrayList<String>();
254                    for (ErrorMessage errorMessage : errorMessages) {
255                        // need to materialize the message from it's parameters so we can send it back to the framework
256                        String error = MessageFormat.format(configurationService.getPropertyValueAsString(errorMessage.getErrorKey()), errorMessage.getMessageParameters());
257                        errors.add(error);
258                    }
259                    RemotableAttributeError remotableAttributeError = RemotableAttributeError.Builder.create(errorKey, errors).build();
260                    validationErrors.add(remotableAttributeError);
261                }
262            }
263            // we should now strip the error messages from the map because they have moved to validationErrors
264            GlobalVariables.getMessageMap().clearErrorMessages();
265        }
266    }
267
268    protected List<Row> createFieldRowsForWorkflowAttributes(WorkflowAttributes attrs) {
269        List<Row> searchFields = new ArrayList<Row>();
270
271        List<SearchingTypeDefinition> searchingTypeDefinitions = attrs.getSearchingTypeDefinitions();
272        final WorkflowAttributePropertyResolutionService propertyResolutionService = KRADServiceLocatorInternal.getWorkflowAttributePropertyResolutionService();
273        for (SearchingTypeDefinition definition: searchingTypeDefinitions) {
274            SearchingAttribute attr = definition.getSearchingAttribute();
275
276            final String attributeName = attr.getAttributeName();
277            final String businessObjectClassName = attr.getBusinessObjectClassName();
278            Class boClass = null;
279            BusinessObject businessObject  = null;
280            try {
281                boClass = Class.forName(businessObjectClassName);
282                businessObject = (BusinessObject)boClass.newInstance();
283            } catch (Exception e) {
284                throw new RuntimeException(e);
285            }
286
287            Field searchField = FieldUtils.getPropertyField(boClass, attributeName, false);
288            // prepend all document attribute field names with "documentAttribute."
289            //searchField.setPropertyName(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + searchField.getPropertyName());
290            searchField.setColumnVisible(attr.isShowAttributeInResultSet());
291
292            //TODO this is a workaround to hide the Field from the search criteria.
293            //This should be removed once hiding the entire Row is working
294            if (!attr.isShowAttributeInSearchCriteria()){
295                searchField.setFieldType(Field.HIDDEN);
296            }
297            String fieldDataType = propertyResolutionService.determineFieldDataType(boClass, attributeName);
298            if (fieldDataType.equals(DataDictionarySearchableAttribute.DATA_TYPE_BOOLEAN)) {
299                fieldDataType = KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING;
300            }
301
302            // Allow inline range searching on dates and numbers
303            if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT) ||
304                fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG) ||
305                fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE)) {
306
307                searchField.setAllowInlineRange(true);
308            }
309            searchField.setFieldDataType(fieldDataType);
310            List displayedFieldNames = new ArrayList();
311            displayedFieldNames.add(attributeName);
312            LookupUtils.setFieldQuickfinder(businessObject, attributeName, searchField, displayedFieldNames);
313
314            List<Field> fieldList = new ArrayList<Field>();
315            fieldList.add(searchField);
316
317            Row row = new Row(fieldList);
318            if (!attr.isShowAttributeInSearchCriteria()) {
319                row.setHidden(true);
320            }
321            searchFields.add(row);
322        }
323
324        return searchFields;
325    }
326
327    protected List<DocumentAttribute> parsePrimaryKeyValuesFromDocument(Class<? extends BusinessObject> businessObjectClass, MaintenanceDocument document) {
328        List<DocumentAttribute> values = new ArrayList<DocumentAttribute>();
329
330        final List primaryKeyNames = KNSServiceLocator.getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(businessObjectClass);
331
332        for (Object primaryKeyNameAsObj : primaryKeyNames) {
333            final String primaryKeyName = (String)primaryKeyNameAsObj;
334            final DocumentAttribute searchableValue = parseSearchableAttributeValueForPrimaryKey(primaryKeyName, businessObjectClass, document);
335            if (searchableValue != null) {
336                values.add(searchableValue);
337            }
338        }
339        return values;
340    }
341
342    /**
343     * Creates a searchable attribute value for the given property name out of the document XML
344     * @param propertyName the name of the property to return
345     * @param businessObjectClass the class of the business object maintained
346     * @param document the document XML
347     * @return a generated SearchableAttributeValue, or null if a value could not be created
348     */
349    protected DocumentAttribute parseSearchableAttributeValueForPrimaryKey(String propertyName, Class<? extends BusinessObject> businessObjectClass, MaintenanceDocument document) {
350
351        Maintainable maintainable  = document.getNewMaintainableObject();
352        PersistableBusinessObject bo = maintainable.getBusinessObject();
353
354        final Object propertyValue = ObjectUtils.getPropertyValue(bo, propertyName);
355        if (propertyValue == null) return null;
356
357        final WorkflowAttributePropertyResolutionService propertyResolutionService = KRADServiceLocatorInternal.getWorkflowAttributePropertyResolutionService();
358        DocumentAttribute value = propertyResolutionService.buildSearchableAttribute(businessObjectClass, propertyName, propertyValue);
359        return value;
360    }
361
362    /**
363     * Returns the class of the object being maintained by the given maintenance document type name
364     * @param documentTypeName the name of the document type to look up the maintained business object for
365     * @return the class of the maintained business object
366     */
367    protected Class<? extends BusinessObject> getBusinessObjectClass(String documentTypeName) {
368        MaintenanceDocumentEntry entry = retrieveMaintenanceDocumentEntry(documentTypeName);
369        return (entry == null ? null : (Class<? extends BusinessObject>) entry.getDataObjectClass());
370    }
371
372    /**
373     * Returns the maintainable of the object being maintained by the given maintenance document type name
374     * @param documentTypeName the name of the document type to look up the maintained business object for
375     * @return the Maintainable of the maintained business object
376     */
377    protected Class<? extends Maintainable> getMaintainableClass(String documentTypeName) {
378        MaintenanceDocumentEntry entry = retrieveMaintenanceDocumentEntry(documentTypeName);
379        return (entry == null ? null : entry.getMaintainableClass());
380    }
381
382
383    /**
384     * Retrieves the maintenance document entry for the given document type name
385     * @param documentTypeName the document type name to look up the data dictionary document entry for
386     * @return the corresponding data dictionary entry for a maintenance document
387     */
388    protected MaintenanceDocumentEntry retrieveMaintenanceDocumentEntry(String documentTypeName) {
389        return (MaintenanceDocumentEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDocumentEntry(documentTypeName);
390    }
391
392    protected GlobalBusinessObject retrieveGlobalBusinessObject(String documentNumber, Class<? extends BusinessObject> businessObjectClass) {
393        GlobalBusinessObject globalBO = null;
394
395        Map pkMap = new LinkedHashMap();
396        pkMap.put(KRADPropertyConstants.DOCUMENT_NUMBER, documentNumber);
397
398        List returnedBOs = (List) KRADServiceLocator.getBusinessObjectService().findMatching(businessObjectClass, pkMap);
399        if (returnedBOs.size() > 0) {
400            globalBO = (GlobalBusinessObject)returnedBOs.get(0);
401        }
402
403        return globalBO;
404    }
405
406    protected List<DocumentAttribute> findAllDocumentAttributesForGlobalBusinessObject(GlobalBusinessObject globalBO) {
407        List<DocumentAttribute> searchValues = new ArrayList<DocumentAttribute>();
408
409        for (PersistableBusinessObject bo : globalBO.generateGlobalChangesToPersist()) {
410            DocumentAttribute value = generateSearchableAttributeFromChange(bo);
411            if (value != null) {
412                searchValues.add(value);
413            }
414        }
415
416        return searchValues;
417    }
418
419    protected DocumentAttribute generateSearchableAttributeFromChange(PersistableBusinessObject changeToPersist) {
420        List<String> primaryKeyNames = KNSServiceLocator.getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(changeToPersist.getClass());
421
422        for (Object primaryKeyNameAsObject : primaryKeyNames) {
423            String primaryKeyName = (String)primaryKeyNameAsObject;
424            Object value = ObjectUtils.getPropertyValue(changeToPersist, primaryKeyName);
425
426            if (value != null) {
427                final WorkflowAttributePropertyResolutionService propertyResolutionService = KRADServiceLocatorInternal.getWorkflowAttributePropertyResolutionService();
428                DocumentAttribute saValue = propertyResolutionService.buildSearchableAttribute(changeToPersist.getClass(), primaryKeyName, value);
429                return saValue;
430            }
431        }
432        return null;
433    }
434
435    /**
436     * Creates a list of search fields, one for each primary key of the maintained business object
437     * @param businessObjectClass the class of the maintained business object
438     * @return a List of KEW search fields
439     */
440    protected List<Row> createFieldRowsForBusinessObject(Class<? extends BusinessObject> businessObjectClass) {
441        List<Row> searchFields = new ArrayList<Row>();
442
443        final List primaryKeyNamesAsObjects = KNSServiceLocator.getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(businessObjectClass);
444        final BusinessObjectEntry boEntry = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(businessObjectClass.getName());
445        final WorkflowAttributePropertyResolutionService propertyResolutionService = KRADServiceLocatorInternal.getWorkflowAttributePropertyResolutionService();
446        for (Object primaryKeyNameAsObject : primaryKeyNamesAsObjects) {
447
448            String attributeName =  (String)primaryKeyNameAsObject;
449            BusinessObject businessObject = null;
450            try {
451                businessObject = businessObjectClass.newInstance();
452            } catch (Exception e) {
453                throw new RuntimeException(e);
454            }
455
456            Field searchField = FieldUtils.getPropertyField(businessObjectClass, attributeName, false);
457            String dataType = propertyResolutionService.determineFieldDataType(businessObjectClass, attributeName);
458            searchField.setFieldDataType(dataType);
459            List<Field> fieldList = new ArrayList<Field>();
460
461            List displayedFieldNames = new ArrayList();
462            displayedFieldNames.add(attributeName);
463            LookupUtils.setFieldQuickfinder(businessObject, attributeName, searchField, displayedFieldNames);
464
465            fieldList.add(searchField);
466            searchFields.add(new Row(fieldList));
467        }
468
469        return searchFields;
470    }
471
472}