View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl2.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.util;
17  
18  import java.util.Collection;
19  import java.util.HashMap;
20  import java.util.HashSet;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Properties;
25  import java.util.Set;
26  import java.util.StringTokenizer;
27  
28  import org.apache.commons.lang.StringUtils;
29  import org.kuali.rice.kew.exception.WorkflowException;
30  import org.kuali.rice.kns.bo.BusinessObject;
31  import org.kuali.rice.kns.datadictionary.AttributeSecurity;
32  import org.kuali.rice.kns.datadictionary.MaintainableCollectionDefinition;
33  import org.kuali.rice.kns.datadictionary.MaintainableFieldDefinition;
34  import org.kuali.rice.kns.datadictionary.MaintainableItemDefinition;
35  import org.kuali.rice.kns.datadictionary.MaintainableSectionDefinition;
36  import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
37  import org.kuali.rice.kns.document.MaintenanceDocument;
38  import org.kuali.rice.kns.exception.KualiExceptionIncident;
39  import org.kuali.rice.kns.exception.ValidationException;
40  import org.kuali.rice.kns.lookup.LookupUtils;
41  import org.kuali.rice.kns.lookup.SelectiveReferenceRefresher;
42  import org.kuali.rice.kns.maintenance.Maintainable;
43  import org.kuali.rice.kns.service.DataDictionaryService;
44  import org.kuali.rice.kns.service.KNSServiceLocator;
45  import org.kuali.rice.kns.service.KualiConfigurationService;
46  import org.kuali.rice.kns.service.KualiExceptionIncidentService;
47  import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
48  import org.kuali.rice.kns.service.MaintenanceDocumentService;
49  import org.kuali.rice.kns.web.ui.Field;
50  import org.kuali.rice.kns.web.ui.Row;
51  import org.kuali.rice.kns.web.ui.Section;
52  import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
53  import org.kuali.rice.kns.workflow.service.WorkflowDocumentService;
54  
55  public class MaintenanceUtils {
56      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(MaintenanceUtils.class);
57  
58      private static MaintenanceDocumentService maintenanceDocumentService;
59      private static WorkflowDocumentService workflowDocumentService;
60      private static KualiConfigurationService kualiConfigurationService;
61      private static KualiExceptionIncidentService kualiExceptionIncidentService; 
62      private static MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
63      private static DataDictionaryService dataDictionaryService;
64      
65      /**
66       * Returns the field templates defined in the maint dictionary xml files. Field templates are used in multiple value lookups.
67       * When doing a MV lookup on a collection, the returned BOs are not necessarily of the same type as the elements of the
68       * collection. Therefore, a means of mapping between the fields for the 2 BOs are necessary. The template attribute of
69       * <maintainableField>s contained within <maintainableCollection>s tells us this mapping. Example: a
70       * <maintainableField name="collectionAttrib" template="lookupBOAttrib"> definition means that when a list of BOs are
71       * returned, the lookupBOAttrib value of the looked up BO will be placed into the collectionAttrib value of the BO added to the
72       * collection
73       * 
74       * @param sections the sections of a document
75       * @param collectionName the name of a collection. May be a nested collection with indices (e.g. collA[1].collB)
76       * @return
77       */
78      public static Map<String, String> generateMultipleValueLookupBOTemplate(List<MaintainableSectionDefinition> sections, String collectionName) {
79          MaintainableCollectionDefinition definition = findMaintainableCollectionDefinition(sections, collectionName);
80          if (definition == null) {
81              return null;
82          }
83          Map<String, String> template = null;
84  
85          for (MaintainableFieldDefinition maintainableField : definition.getMaintainableFields()) {
86              String templateString = maintainableField.getTemplate();
87              if (StringUtils.isNotBlank(templateString)) {
88                  if (template == null) {
89                      template = new HashMap<String, String>();
90                  }
91                  template.put(maintainableField.getName(), templateString);
92              }
93          }
94          return template;
95      }
96      
97      /**
98       * Finds the MaintainableCollectionDefinition corresponding to the given collection name. For example, if the collection name is
99       * "A.B.C", it will attempt to find the MaintainableCollectionDefinition for C that is nested in B that is nested under A. This
100      * may not work correctly if there are duplicate collection definitions within the sections
101      * 
102      * @param sections the sections of a maint doc
103      * @param collectionName the name of a collection, relative to the root of the BO being maintained. This value may have index
104      *        values (e.g. [1]), but these are ignored.
105      * @return
106      */
107     public static MaintainableCollectionDefinition findMaintainableCollectionDefinition(List<MaintainableSectionDefinition> sections, String collectionName) {
108         String[] collectionNameParts = StringUtils.split(collectionName, ".");
109         for (MaintainableSectionDefinition section : sections) {
110             MaintainableCollectionDefinition collDefinition = findMaintainableCollectionDefinitionHelper(section.getMaintainableItems(), collectionNameParts, 0);
111             if (collDefinition != null) {
112                 return collDefinition;
113             }
114         }
115         return null;
116     }
117 
118     private static <E extends MaintainableItemDefinition> MaintainableCollectionDefinition findMaintainableCollectionDefinitionHelper(Collection<E> items, String[] collectionNameParts, int collectionNameIndex) {
119         if (collectionNameParts.length <= collectionNameIndex) {
120             // we've gone too far down the nesting without finding it
121             return null;
122         }
123 
124         // we only care about the coll name, and not the index, since the coll definitions do not include the indexing characters,
125         // i.e. [ and ]
126         String collectionToFind = StringUtils.substringBefore(collectionNameParts[collectionNameIndex], "[");
127         for (MaintainableItemDefinition item : items) {
128             if (item instanceof MaintainableCollectionDefinition) {
129                 MaintainableCollectionDefinition collection = (MaintainableCollectionDefinition) item;
130                 if (collection.getName().equals(collectionToFind)) {
131                     // we found an appropriate coll, now we have to see if we need to recurse even more (more nested collections),
132                     // or just return the one we found.
133                     if (collectionNameIndex == collectionNameParts.length - 1) {
134                         // we're at the last part of the name, so we return
135                         return collection;
136                     }
137                     else {
138                         // go deeper
139                         return findMaintainableCollectionDefinitionHelper(collection.getMaintainableCollections(), collectionNameParts, collectionNameIndex + 1);
140                     }
141                 }
142             }
143         }
144         return null;
145     }
146 
147     /**
148      * Checks to see if there has been an override lookup declared for the maintenance field. If so, the override will be used for
149      * the quickfinder and lookup utils will not be called. If no override was given, LookupUtils.setFieldQuickfinder will be called
150      * to set the system generated quickfinder based on the attributes relationship to the parent business object.
151      * 
152      * @return Field with quickfinder set if one was found
153      */
154     public static final Field setFieldQuickfinder(BusinessObject businessObject, String attributeName, MaintainableFieldDefinition maintainableFieldDefinition, Field field, List<String> displayedFieldNames, SelectiveReferenceRefresher srr) {
155         if (maintainableFieldDefinition.getOverrideLookupClass() != null && StringUtils.isNotBlank(maintainableFieldDefinition.getOverrideFieldConversions())) {
156             field.setQuickFinderClassNameImpl(maintainableFieldDefinition.getOverrideLookupClass().getName());
157             field.setFieldConversions(maintainableFieldDefinition.getOverrideFieldConversions());
158             field.setBaseLookupUrl(LookupUtils.getBaseLookupUrl(false));
159             field.setReferencesToRefresh(LookupUtils.convertReferencesToSelectCollectionToString(
160                     srr.getAffectedReferencesFromLookup(businessObject, attributeName, "")));
161             return field;
162         }
163         if (maintainableFieldDefinition.isNoLookup()){
164         	return field;
165         }
166         return LookupUtils.setFieldQuickfinder(businessObject, null, false, 0, attributeName, field, displayedFieldNames, maintainableFieldDefinition.isNoLookup());
167     }
168 
169     public static final Field setFieldQuickfinder(BusinessObject businessObject, String collectionName, boolean addLine, int index,
170             String attributeName, Field field, List<String> displayedFieldNames, Maintainable maintainable, MaintainableFieldDefinition maintainableFieldDefinition) {
171         if (maintainableFieldDefinition.getOverrideLookupClass() != null && StringUtils.isNotBlank(maintainableFieldDefinition.getOverrideFieldConversions())) {
172             if (maintainable != null) {
173                 String collectionPrefix = "";
174                 if ( collectionName != null ) {
175                     if (addLine) {
176                         collectionPrefix = KNSConstants.MAINTENANCE_ADD_PREFIX + collectionName + ".";
177                     }
178                     else {
179                         collectionPrefix = collectionName + "[" + index + "].";
180                     }
181                 }
182                 field.setQuickFinderClassNameImpl(maintainableFieldDefinition.getOverrideLookupClass().getName());
183                 
184                 String prefixedFieldConversions = prefixFieldConversionsDestinationsWithCollectionPrefix(maintainableFieldDefinition.getOverrideFieldConversions(), collectionPrefix);
185                 field.setFieldConversions(prefixedFieldConversions);
186                 field.setBaseLookupUrl(LookupUtils.getBaseLookupUrl(false));
187                 field.setReferencesToRefresh(LookupUtils.convertReferencesToSelectCollectionToString(
188                         maintainable.getAffectedReferencesFromLookup(businessObject, attributeName, collectionPrefix)));
189             }
190             return field;
191         }
192         if(maintainableFieldDefinition.isNoLookup()){
193         	return field;
194         }
195         return LookupUtils.setFieldQuickfinder(businessObject, collectionName, addLine, index,
196                 attributeName, field, displayedFieldNames, maintainable);
197     }
198     
199     private static String prefixFieldConversionsDestinationsWithCollectionPrefix(String originalFieldConversions, String collectionPrefix) {
200         StringBuilder buf = new StringBuilder();
201         StringTokenizer tok = new StringTokenizer(originalFieldConversions, KNSConstants.FIELD_CONVERSIONS_SEPARATOR);
202         boolean needsSeparator = false;
203         while (tok.hasMoreTokens()) {
204             String conversionPair = tok.nextToken();
205             if (StringUtils.isBlank(conversionPair)) {
206                 continue;
207             }
208             
209             String fromValue = StringUtils.substringBefore(conversionPair, KNSConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
210             String toValue = StringUtils.substringAfter(conversionPair, KNSConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
211             
212             if (needsSeparator) {
213                 buf.append(KNSConstants.FIELD_CONVERSIONS_SEPARATOR);
214             }
215             needsSeparator = true;
216             
217             buf.append(fromValue).append(KNSConstants.FIELD_CONVERSION_PAIR_SEPARATOR).append(collectionPrefix).append(toValue);
218         }
219         return buf.toString();
220     }
221     
222     public static final void setFieldDirectInquiry(BusinessObject businessObject, String attributeName, MaintainableFieldDefinition maintainableFieldDefinition, Field field, List<String> displayedFieldNames) {
223         LookupUtils.setFieldDirectInquiry(businessObject, attributeName, field);
224     }
225     
226     public static final void setFieldDirectInquiry(BusinessObject businessObject, String collectionName, boolean addLine, int index,
227             String attributeName, Field field, List<String> displayedFieldNames, Maintainable maintainable, MaintainableFieldDefinition maintainableFieldDefinition) {
228         LookupUtils.setFieldDirectInquiry(businessObject, attributeName, field);
229     }
230     /**
231      * Given a section, returns a comma delimited string of all fields, representing the error keys that exist for a section
232      * 
233      * @param section a section
234      * @return
235      */
236     public static String generateErrorKeyForSection(Section section) {
237         Set<String> fieldPropertyNames = new HashSet<String>();
238         addRowsToErrorKeySet(section.getRows(), fieldPropertyNames);
239 
240         StringBuilder buf = new StringBuilder();
241         buf.append(section.getSectionId()).append(",");
242         
243         Iterator<String> nameIter = fieldPropertyNames.iterator();
244         while (nameIter.hasNext()) {
245             buf.append(nameIter.next());
246             if (nameIter.hasNext()) {
247                 buf.append(",");
248             }
249         }
250 
251         if (section.getContainedCollectionNames() != null && section.getContainedCollectionNames().size() > 0) {
252             buf.append(",");
253             
254             Iterator<String> collectionIter = section.getContainedCollectionNames().iterator();
255             while (collectionIter.hasNext()) {
256                 buf.append(KNSConstants.MAINTENANCE_NEW_MAINTAINABLE + collectionIter.next());
257                 if (collectionIter.hasNext()) {
258                     buf.append(",");
259                 }
260             }
261         }
262 
263         return buf.toString();
264     }
265 
266     /**
267      * This method recurses through all the fields of the list of rows and adds each field's property name to the set if it starts
268      * with Constants.MAINTENANCE_NEW_MAINTAINABLE
269      * 
270      * @see KNSConstants#MAINTENANCE_NEW_MAINTAINABLE
271      * @param listOfRows
272      * @param errorKeys
273      */
274     protected static void addRowsToErrorKeySet(List<Row> listOfRows, Set<String> errorKeys) {
275         if (listOfRows == null) {
276             return;
277         }
278         for (Row row : listOfRows) {
279             List<Field> fields = row.getFields();
280             if (fields == null) {
281                 continue;
282             }
283             for (Field field : fields) {
284                 String fieldPropertyName = field.getPropertyName();
285                 if (fieldPropertyName != null && fieldPropertyName.startsWith(KNSConstants.MAINTENANCE_NEW_MAINTAINABLE)) {
286                     errorKeys.add(field.getPropertyName());
287                 }
288                 addRowsToErrorKeySet(field.getContainerRows(), errorKeys);
289             }
290         }
291     }
292     
293     public static boolean isMaintenanceDocumentCreatingNewRecord(String maintenanceAction) {
294         if (KNSConstants.MAINTENANCE_EDIT_ACTION.equalsIgnoreCase(maintenanceAction)) {
295             return false;
296         }
297         else if (KNSConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equalsIgnoreCase(maintenanceAction)) {
298             return false;
299         }
300         else if (KNSConstants.MAINTENANCE_DELETE_ACTION.equalsIgnoreCase(maintenanceAction)) {
301             return false;
302         }
303         
304         else if (KNSConstants.MAINTENANCE_NEW_ACTION.equalsIgnoreCase(maintenanceAction)) {
305             return true;
306         }
307         else if (KNSConstants.MAINTENANCE_COPY_ACTION.equalsIgnoreCase(maintenanceAction)) {
308             return true;
309         }
310         else {
311             return true;
312         }
313     }
314 
315     /**
316      * This method will throw a {@link ValidationException} if there is a valid locking document in existence and throwExceptionIfLocked is true.
317      */
318     public static void checkForLockingDocument(MaintenanceDocument document, boolean throwExceptionIfLocked) {
319         LOG.info("starting checkForLockingDocument (by MaintenanceDocument)");
320 
321         // get the docHeaderId of the blocking docs, if any are locked and blocking
322         //String blockingDocId = getMaintenanceDocumentService().getLockingDocumentId(document);
323         String blockingDocId = document.getNewMaintainableObject().getLockingDocumentId();
324         checkDocumentBlockingDocumentId(blockingDocId, throwExceptionIfLocked);
325     }
326 
327     /**
328      * This method will throw a {@link ValidationException} if there is a valid locking document in existence and throwExceptionIfLocked is true.
329      */
330     public static void checkForLockingDocument(Maintainable maintainable, boolean throwExceptionIfLocked) {
331         LOG.info("starting checkForLockingDocument (by Maintainable)");
332 
333         // get the docHeaderId of the blocking docs, if any are locked and blocking
334         //String blockingDocId = getMaintenanceDocumentService().getLockingDocumentId(maintainable, null);
335         String blockingDocId = maintainable.getLockingDocumentId();
336         checkDocumentBlockingDocumentId(blockingDocId, throwExceptionIfLocked);
337     }
338 
339     private static void checkDocumentBlockingDocumentId(String blockingDocId, boolean throwExceptionIfLocked) {
340         // if we got nothing, then no docs are blocking, and we're done
341         if (StringUtils.isBlank(blockingDocId)) {
342             return;
343         }
344 
345         if ( LOG.isInfoEnabled() ) {
346             LOG.info("Locking document found:  docId = " + blockingDocId + ".");
347         }
348 
349         // load the blocking locked document
350         KualiWorkflowDocument lockedDocument = null;
351         try {
352         	// need to perform this check to prevent an exception from being thrown by the
353         	// createWorkflowDocument call - the throw itself causes transaction rollback problems to
354         	// occur, even though the exception would be caught here
355         	if ( getWorkflowDocumentService().workflowDocumentExists(blockingDocId) ) {
356         		lockedDocument = getWorkflowDocumentService().createWorkflowDocument(Long.valueOf(blockingDocId), GlobalVariables.getUserSession().getPerson() );
357         	}
358         } catch (Exception ex) {
359         	// clean up the lock and notify the admins
360         	LOG.error("Unable to retrieve locking document specified in the maintenance lock table: " + blockingDocId, ex);
361 
362         	cleanOrphanLocks(blockingDocId, ex);
363         	return;
364         }
365         if ( lockedDocument == null ) {
366         	LOG.warn( "Locking document header for " + blockingDocId + "came back null." );
367         	cleanOrphanLocks(blockingDocId, null);
368         }
369 
370         // if we can ignore the lock (see method notes), then exit cause we're done
371         if (lockCanBeIgnored(lockedDocument)) {
372             return;
373         }
374 
375         // build the link URL for the blocking document
376         Properties parameters = new Properties();
377         parameters.put(KNSConstants.PARAMETER_DOC_ID, blockingDocId);
378         parameters.put(KNSConstants.PARAMETER_COMMAND, KNSConstants.METHOD_DISPLAY_DOC_SEARCH_VIEW);
379         String blockingUrl = UrlFactory.parameterizeUrl(getKualiConfigurationService().getPropertyString(KNSConstants.WORKFLOW_URL_KEY) + "/" + KNSConstants.DOC_HANDLER_ACTION, parameters);
380         if ( LOG.isDebugEnabled() ) {
381             LOG.debug("blockingUrl = '" + blockingUrl + "'");
382             LOG.debug("Maintenance record: " + lockedDocument.getAppDocId() + "is locked.");
383         }
384         String[] errorParameters = { blockingUrl, blockingDocId };
385         
386         // If specified, add an error to the ErrorMap and throw an exception; otherwise, just add a warning to the ErrorMap instead.
387         if (throwExceptionIfLocked) {
388         	// post an error about the locked document
389             GlobalVariables.getMessageMap().putError(KNSConstants.GLOBAL_ERRORS, RiceKeyConstants.ERROR_MAINTENANCE_LOCKED, errorParameters);
390             throw new ValidationException("Maintenance Record is locked by another document.");
391         }
392         else {
393         	// Post a warning about the locked document.
394         	GlobalVariables.getMessageMap().putWarning(KNSConstants.GLOBAL_MESSAGES, RiceKeyConstants.WARNING_MAINTENANCE_LOCKED, errorParameters);
395         }
396     }
397 
398     /**
399      * This method guesses whether the current user should be allowed to change a document even though it is locked. It probably
400      * should use Authorization instead? See KULNRVSYS-948
401      * 
402      * @param lockedDocument
403      * @return
404      * @throws WorkflowException
405      */
406     private static boolean lockCanBeIgnored(KualiWorkflowDocument lockedDocument) {
407         // TODO: implement real authorization for Maintenance Document Save/Route - KULNRVSYS-948
408     	if ( lockedDocument == null ) {
409     		return true;
410 		}
411 
412         // get the user-id. if no user-id, then we can do this test, so exit
413         String userId = GlobalVariables.getUserSession().getPrincipalId().trim();
414         if (StringUtils.isBlank(userId)) {
415             return false; // dont bypass locking
416         }
417 
418         // if the current user is not the initiator of the blocking document
419         if (!userId.equalsIgnoreCase(lockedDocument.getRouteHeader().getInitiatorPrincipalId().trim())) {
420             return false;
421         }
422 
423         // if the blocking document hasn't been routed, we can ignore it
424         return lockedDocument.stateIsInitiated();
425     }
426 
427     private static void cleanOrphanLocks( String lockingDocumentNumber, Exception workflowException ) {
428     	// put a try/catch around the whole thing - the whole reason we are doing this is to prevent data errors
429     	// from stopping a document
430     	try {
431     		// delete the locks for this document since it does not seem to exist
432     		getMaintenanceDocumentService().deleteLocks(lockingDocumentNumber);
433     		// notify the incident list
434             Map<String, String> parameters = new HashMap<String, String>(1);
435             parameters.put(KNSConstants.PARAMETER_DOC_ID, lockingDocumentNumber);
436         	KualiExceptionIncident kei = getKualiExceptionIncidentService().getExceptionIncident(workflowException, parameters);
437     		getKualiExceptionIncidentService().report(kei);
438     	} catch ( Exception ex ) {
439     		LOG.error("Unable to delete and notify upon locking document retrieval failure.", ex);
440     	}
441     }
442 
443 	private static MaintenanceDocumentService getMaintenanceDocumentService() {
444 		if ( maintenanceDocumentService == null ) {
445 			maintenanceDocumentService = KNSServiceLocator.getMaintenanceDocumentService();
446 		}
447 		return maintenanceDocumentService;
448 	}
449 
450 	private static WorkflowDocumentService getWorkflowDocumentService() {
451 		if ( workflowDocumentService == null ) {
452 			workflowDocumentService = KNSServiceLocator.getWorkflowDocumentService();
453 		}
454 		return workflowDocumentService;
455 	}
456 
457 	private static KualiConfigurationService getKualiConfigurationService() {
458 		if ( kualiConfigurationService == null ) {
459 			kualiConfigurationService = KNSServiceLocator.getKualiConfigurationService();
460 		}
461 		return kualiConfigurationService;
462 	}
463 
464 	private static KualiExceptionIncidentService getKualiExceptionIncidentService() {
465 		if ( kualiExceptionIncidentService == null ) {
466 			kualiExceptionIncidentService = KNSServiceLocator.getKualiExceptionIncidentService();
467 		}
468 		return kualiExceptionIncidentService;
469 	}
470     
471 	private static MaintenanceDocumentDictionaryService getMaintenanceDocumentDictionaryService() {
472 		if ( maintenanceDocumentDictionaryService == null ) {
473 			maintenanceDocumentDictionaryService = KNSServiceLocator.getMaintenanceDocumentDictionaryService();
474 		}
475 		return maintenanceDocumentDictionaryService;
476 	}
477 	
478 	private static DataDictionaryService getDataDictionaryService() {
479 		if ( dataDictionaryService == null ) {
480 			dataDictionaryService = KNSServiceLocator.getDataDictionaryService();
481 		}
482 		return dataDictionaryService;
483 	}
484 	public static Map<String, AttributeSecurity> retrievePropertyPathToAttributeSecurityMappings(String docTypeName) {
485 		Map<String, AttributeSecurity> results = new HashMap<String, AttributeSecurity>();
486 		MaintenanceDocumentEntry entry = getMaintenanceDocumentDictionaryService().getMaintenanceDocumentEntry(docTypeName);
487 		String className = entry.getBusinessObjectClass().getName();
488 		
489 		for (MaintainableSectionDefinition section : entry.getMaintainableSections()) {
490 			for (MaintainableItemDefinition item : section.getMaintainableItems()) {
491 				if (item instanceof MaintainableFieldDefinition) {
492 					MaintainableFieldDefinition field = (MaintainableFieldDefinition) item;
493 					AttributeSecurity attributeSecurity = getDataDictionaryService().getAttributeSecurity(className, field.getName());
494 					if (attributeSecurity != null) {
495 						results.put(field.getName(), attributeSecurity);
496 					}
497 				}
498 				else if (item instanceof MaintainableCollectionDefinition) {
499 					addMaintenanceDocumentCollectionPathToSecurityMappings(results, "", (MaintainableCollectionDefinition) item);
500 				}
501 			}
502 		}
503 		return results;
504 	}
505 	
506 	private static void addMaintenanceDocumentCollectionPathToSecurityMappings(Map<String, AttributeSecurity> mappings, String propertyPathPrefix, MaintainableCollectionDefinition collectionDefinition) {
507 		propertyPathPrefix = propertyPathPrefix + collectionDefinition.getName() + ".";
508 		String boClassName = collectionDefinition.getBusinessObjectClass().getName();
509 		for (MaintainableFieldDefinition field : collectionDefinition.getMaintainableFields()) {
510 			AttributeSecurity attributeSecurity = getDataDictionaryService().getAttributeSecurity(boClassName, field.getName());
511 			if (attributeSecurity != null) {
512 				mappings.put(propertyPathPrefix + field.getName(), attributeSecurity);
513 			}
514 		}
515 		for (MaintainableCollectionDefinition nestedCollection : collectionDefinition.getMaintainableCollections()) {
516 			addMaintenanceDocumentCollectionPathToSecurityMappings(mappings, propertyPathPrefix, nestedCollection);
517 		}
518 	}
519 }