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     */
016    package org.kuali.rice.krad.workflow.attribute;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.log4j.Logger;
021    import org.kuali.rice.core.api.config.property.ConfigurationService;
022    import org.kuali.rice.core.api.uif.RemotableAttributeError;
023    import org.kuali.rice.core.api.uif.RemotableAttributeField;
024    import org.kuali.rice.kew.api.KewApiConstants;
025    import org.kuali.rice.kew.api.document.DocumentWithContent;
026    import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
027    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
028    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeString;
029    import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
030    import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
031    import org.kuali.rice.kew.api.exception.WorkflowException;
032    import org.kuali.rice.kew.api.extension.ExtensionDefinition;
033    import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
034    import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
035    import org.kuali.rice.kns.document.MaintenanceDocument;
036    import org.kuali.rice.kns.lookup.LookupUtils;
037    import org.kuali.rice.kns.maintenance.KualiGlobalMaintainableImpl;
038    import org.kuali.rice.kns.maintenance.Maintainable;
039    import org.kuali.rice.kns.service.DictionaryValidationService;
040    import org.kuali.rice.kns.service.KNSServiceLocator;
041    import org.kuali.rice.kns.util.FieldUtils;
042    import org.kuali.rice.kns.web.ui.Field;
043    import org.kuali.rice.kns.web.ui.Row;
044    import org.kuali.rice.krad.bo.BusinessObject;
045    import org.kuali.rice.krad.bo.DocumentHeader;
046    import org.kuali.rice.krad.bo.GlobalBusinessObject;
047    import org.kuali.rice.krad.bo.PersistableBusinessObject;
048    import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
049    import org.kuali.rice.krad.datadictionary.DocumentEntry;
050    import org.kuali.rice.krad.datadictionary.SearchingAttribute;
051    import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
052    import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
053    import org.kuali.rice.krad.document.Document;
054    import org.kuali.rice.krad.service.DataDictionaryRemoteFieldService;
055    import org.kuali.rice.krad.service.DocumentService;
056    import org.kuali.rice.krad.service.KRADServiceLocator;
057    import org.kuali.rice.krad.service.KRADServiceLocatorInternal;
058    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
059    import org.kuali.rice.krad.util.ErrorMessage;
060    import org.kuali.rice.krad.util.GlobalVariables;
061    import org.kuali.rice.krad.util.KRADPropertyConstants;
062    import org.kuali.rice.krad.util.MessageMap;
063    import org.kuali.rice.krad.util.ObjectUtils;
064    import org.kuali.rice.krad.workflow.service.WorkflowAttributePropertyResolutionService;
065    
066    import java.text.MessageFormat;
067    import java.util.ArrayList;
068    import java.util.LinkedHashMap;
069    import java.util.List;
070    import java.util.Map;
071    
072    public 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    }