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