001    /*
002     * Copyright 2005-2007 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.kns.maintenance.rules;
017    
018    import java.security.GeneralSecurityException;
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Properties;
025    import java.util.Set;
026    
027    import org.apache.commons.lang.StringUtils;
028    import org.kuali.rice.kim.service.KIMServiceLocator;
029    import org.kuali.rice.kim.service.RoleService;
030    import org.kuali.rice.kns.bo.GlobalBusinessObject;
031    import org.kuali.rice.kns.bo.Inactivateable;
032    import org.kuali.rice.kns.bo.PersistableBusinessObject;
033    import org.kuali.rice.kns.datadictionary.InactivationBlockingMetadata;
034    import org.kuali.rice.kns.document.Document;
035    import org.kuali.rice.kns.document.MaintenanceDocument;
036    import org.kuali.rice.kns.document.authorization.MaintenanceDocumentAuthorizer;
037    import org.kuali.rice.kns.exception.ValidationException;
038    import org.kuali.rice.kns.maintenance.Maintainable;
039    import org.kuali.rice.kns.rule.AddCollectionLineRule;
040    import org.kuali.rice.kns.rule.event.ApproveDocumentEvent;
041    import org.kuali.rice.kns.rules.DocumentRuleBase;
042    import org.kuali.rice.kns.service.BusinessObjectAuthorizationService;
043    import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
044    import org.kuali.rice.kns.service.BusinessObjectService;
045    import org.kuali.rice.kns.service.DataDictionaryService;
046    import org.kuali.rice.kns.service.DateTimeService;
047    import org.kuali.rice.kns.service.DictionaryValidationService;
048    import org.kuali.rice.kns.service.DocumentHelperService;
049    import org.kuali.rice.kns.service.InactivationBlockingDetectionService;
050    import org.kuali.rice.kns.service.KNSServiceLocator;
051    import org.kuali.rice.kns.service.KualiConfigurationService;
052    import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
053    import org.kuali.rice.kns.service.PersistenceService;
054    import org.kuali.rice.kns.service.PersistenceStructureService;
055    import org.kuali.rice.kns.util.ErrorMessage;
056    import org.kuali.rice.kns.util.ForeignKeyFieldsPopulationState;
057    import org.kuali.rice.kns.util.GlobalVariables;
058    import org.kuali.rice.kns.util.KNSConstants;
059    import org.kuali.rice.kns.util.KNSPropertyConstants;
060    import org.kuali.rice.kns.util.MessageMap;
061    import org.kuali.rice.kns.util.ObjectUtils;
062    import org.kuali.rice.kns.util.RiceKeyConstants;
063    import org.kuali.rice.kns.util.TypedArrayList;
064    import org.kuali.rice.kns.util.UrlFactory;
065    import org.kuali.rice.kns.web.format.Formatter;
066    import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
067    import org.kuali.rice.kns.workflow.service.WorkflowDocumentService;
068    
069    
070    /**
071     * This class contains all of the business rules that are common to all maintenance documents.
072     *
073     *
074     */
075    public class MaintenanceDocumentRuleBase extends DocumentRuleBase implements MaintenanceDocumentRule, AddCollectionLineRule {
076        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(MaintenanceDocumentRuleBase.class);
077    
078        // public static final String CHART_MAINTENANCE_EDOC = "ChartMaintenanceEDoc";
079    
080        // these two constants are used to correctly prefix errors added to
081        // the global errors
082        public static final String MAINTAINABLE_ERROR_PREFIX = KNSConstants.MAINTENANCE_NEW_MAINTAINABLE;
083        public static final String DOCUMENT_ERROR_PREFIX = "document.";
084        public static final String MAINTAINABLE_ERROR_PATH = DOCUMENT_ERROR_PREFIX + "newMaintainableObject";
085    
086        protected PersistenceStructureService persistenceStructureService;
087        protected PersistenceService persistenceService;
088        protected DataDictionaryService ddService;
089        protected DocumentHelperService documentHelperService;
090        protected BusinessObjectService boService;
091        protected BusinessObjectDictionaryService boDictionaryService;
092        protected DictionaryValidationService dictionaryValidationService;
093        protected KualiConfigurationService configService;
094        protected MaintenanceDocumentDictionaryService maintDocDictionaryService;
095        protected WorkflowDocumentService workflowDocumentService;
096        protected org.kuali.rice.kim.service.PersonService personService;
097        protected RoleService roleService;
098        protected BusinessObjectAuthorizationService businessObjectAuthorizationService;
099    
100        private PersistableBusinessObject oldBo;
101        private PersistableBusinessObject newBo;
102        private Class boClass;
103    
104        protected List priorErrorPath;
105    
106        /**
107         *
108         * Default constructor a MaintenanceDocumentRuleBase.java.
109         *
110         */
111        public MaintenanceDocumentRuleBase() {
112    
113            priorErrorPath = new ArrayList();
114    
115            // Pseudo-inject some services.
116            //
117            // This approach is being used to make it simpler to convert the Rule classes
118            // to spring-managed with these services injected by Spring at some later date.
119            // When this happens, just remove these calls to the setters with
120            // SpringServiceLocator, and configure the bean defs for spring.
121            try {
122                this.setPersistenceStructureService(KNSServiceLocator.getPersistenceStructureService());
123                this.setDdService(KNSServiceLocator.getDataDictionaryService());
124                this.setPersistenceService(KNSServiceLocator.getPersistenceService());
125                this.setBoService(KNSServiceLocator.getBusinessObjectService());
126                this.setBoDictionaryService(KNSServiceLocator.getBusinessObjectDictionaryService());
127                this.setDictionaryValidationService(KNSServiceLocator.getDictionaryValidationService());
128                this.setConfigService(KNSServiceLocator.getKualiConfigurationService());
129                this.setDocumentHelperService(KNSServiceLocator.getDocumentHelperService());
130                this.setMaintDocDictionaryService(KNSServiceLocator.getMaintenanceDocumentDictionaryService());
131                this.setWorkflowDocumentService(KNSServiceLocator.getWorkflowDocumentService());
132                this.setPersonService( org.kuali.rice.kim.service.KIMServiceLocator.getPersonService() );
133                this.setBusinessObjectAuthorizationService(KNSServiceLocator.getBusinessObjectAuthorizationService());
134            } catch ( Exception ex ) {
135                // do nothing, avoid blowing up if called prior to spring initialization
136            }
137        }
138    
139        /**
140         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRule#processSaveDocument(org.kuali.rice.kns.document.Document)
141         */
142        @Override
143        public boolean processSaveDocument(Document document) {
144    
145            MaintenanceDocument maintenanceDocument = (MaintenanceDocument) document;
146    
147            // remove all items from the errorPath temporarily (because it may not
148            // be what we expect, or what we need)
149            clearErrorPath();
150    
151            // setup convenience pointers to the old & new bo
152            setupBaseConvenienceObjects(maintenanceDocument);
153    
154            // the document must be in a valid state for saving. this does not include business
155            // rules, but just enough testing that the document is populated and in a valid state
156            // to not cause exceptions when saved. if this passes, then the save will always occur,
157            // regardless of business rules.
158            if (!isDocumentValidForSave(maintenanceDocument)) {
159                resumeErrorPath();
160                return false;
161            }
162    
163            // apply rules that are specific to the class of the maintenance document
164            // (if implemented). this will always succeed if not overloaded by the
165            // subclass
166            processCustomSaveDocumentBusinessRules(maintenanceDocument);
167    
168            // return the original set of items to the errorPath
169            resumeErrorPath();
170    
171            // return the original set of items to the errorPath, to ensure no impact
172            // on other upstream or downstream items that rely on the errorPath
173            return true;
174        }
175    
176        /**
177         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRule#processRouteDocument(org.kuali.rice.kns.document.Document)
178         */
179        @Override
180        public boolean processRouteDocument(Document document) {
181            LOG.info("processRouteDocument called");
182    
183            MaintenanceDocument maintenanceDocument = (MaintenanceDocument) document;
184    
185            // get the documentAuthorizer for this document
186            MaintenanceDocumentAuthorizer documentAuthorizer = (MaintenanceDocumentAuthorizer) getDocumentHelperService().getDocumentAuthorizer(document);
187    
188            // remove all items from the errorPath temporarily (because it may not
189            // be what we expect, or what we need)
190            clearErrorPath();
191    
192            // setup convenience pointers to the old & new bo
193            setupBaseConvenienceObjects(maintenanceDocument);
194    
195            // apply rules that are common across all maintenance documents, regardless of class
196            processGlobalSaveDocumentBusinessRules(maintenanceDocument);
197    
198            // from here on, it is in a default-success mode, and will route unless one of the
199            // business rules stop it.
200            boolean success = true;
201    
202            KualiWorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
203            if (workflowDocument.stateIsInitiated() || workflowDocument.stateIsSaved()){
204                    success &= documentAuthorizer.canCreateOrMaintain((MaintenanceDocument)document, GlobalVariables.getUserSession().getPerson());
205                    if (success == false) {
206                            GlobalVariables.getMessageMap().putError(KNSConstants.DOCUMENT_ERRORS, RiceKeyConstants.AUTHORIZATION_ERROR_DOCUMENT, new String[]{GlobalVariables.getUserSession().getPerson().getPrincipalName(), "Create/Maintain", this.getMaintDocDictionaryService().getDocumentTypeName(newBo.getClass())});
207                    }
208            }
209            // apply rules that are common across all maintenance documents, regardless of class
210            success &= processGlobalRouteDocumentBusinessRules(maintenanceDocument);
211    
212            // apply rules that are specific to the class of the maintenance document
213            // (if implemented). this will always succeed if not overloaded by the
214            // subclass
215            success &= processCustomRouteDocumentBusinessRules(maintenanceDocument);
216    
217            success &= processInactivationBlockChecking(maintenanceDocument);
218    
219            // return the original set of items to the errorPath, to ensure no impact
220            // on other upstream or downstream items that rely on the errorPath
221            resumeErrorPath();
222    
223            return success;
224        }
225    
226        /**
227         * Determines whether a document is inactivating the record being maintained
228         *
229         * @param maintenanceDocument
230         * @return true iff the document is inactivating the business object; false otherwise
231         */
232        protected boolean isDocumentInactivatingBusinessObject(MaintenanceDocument maintenanceDocument) {
233            if (maintenanceDocument.isEdit()) {
234                    Class boClass = maintenanceDocument.getNewMaintainableObject().getBoClass();
235                // we can only be inactivating a business object if we're editing it
236                if (boClass != null && Inactivateable.class.isAssignableFrom(boClass)) {
237                    Inactivateable oldInactivateableBO = (Inactivateable) oldBo;
238                    Inactivateable newInactivateableBO = (Inactivateable) newBo;
239    
240                    return oldInactivateableBO.isActive() && !newInactivateableBO.isActive();
241                }
242            }
243            return false;
244        }
245    
246        /**
247         * Determines whether this document has been inactivation blocked
248         *
249         * @param maintenanceDocument
250         * @return true iff there is NOTHING that blocks this record
251         */
252        protected boolean processInactivationBlockChecking(MaintenanceDocument maintenanceDocument) {
253            if (isDocumentInactivatingBusinessObject(maintenanceDocument)) {
254                    Class boClass = maintenanceDocument.getNewMaintainableObject().getBoClass();
255                Set<InactivationBlockingMetadata> inactivationBlockingMetadatas = ddService.getAllInactivationBlockingDefinitions(boClass);
256    
257                if (inactivationBlockingMetadatas != null) {
258                    for (InactivationBlockingMetadata inactivationBlockingMetadata : inactivationBlockingMetadatas) {
259                        // for the purposes of maint doc validation, we only need to look for the first blocking record
260    
261                        // we found a blocking record, so we return false
262                        if (!processInactivationBlockChecking(maintenanceDocument, inactivationBlockingMetadata)) {
263                            return false;
264                        }
265                    }
266                }
267            }
268            return true;
269        }
270    
271        /**
272         * Given a InactivationBlockingMetadata, which represents a relationship that may block inactivation of a BO, it determines whether there
273         * is a record that violates the blocking definition
274         *
275         * @param maintenanceDocument
276         * @param inactivationBlockingMetadata
277         * @return true iff, based on the InactivationBlockingMetadata, the maintenance document should be allowed to route
278         */
279        protected boolean processInactivationBlockChecking(MaintenanceDocument maintenanceDocument, InactivationBlockingMetadata inactivationBlockingMetadata) {
280    
281            String inactivationBlockingDetectionServiceBeanName = inactivationBlockingMetadata.getInactivationBlockingDetectionServiceBeanName();
282            if (StringUtils.isBlank(inactivationBlockingDetectionServiceBeanName)) {
283                inactivationBlockingDetectionServiceBeanName = KNSServiceLocator.DEFAULT_INACTIVATION_BLOCKING_DETECTION_SERVICE;
284            }
285            InactivationBlockingDetectionService inactivationBlockingDetectionService = KNSServiceLocator.getInactivationBlockingDetectionService(inactivationBlockingDetectionServiceBeanName);
286    
287            boolean foundBlockingRecord = inactivationBlockingDetectionService.hasABlockingRecord(newBo, inactivationBlockingMetadata);
288    
289            if (foundBlockingRecord) {
290                putInactivationBlockingErrorOnPage(maintenanceDocument, inactivationBlockingMetadata);
291            }
292    
293            return !foundBlockingRecord;
294        }
295    
296        /**
297         * If there is a violation of an InactivationBlockingMetadata, it prints out an appropriate error into the error map
298         *
299         * @param document
300         * @param inactivationBlockingMetadata
301         */
302        protected void putInactivationBlockingErrorOnPage(MaintenanceDocument document, InactivationBlockingMetadata inactivationBlockingMetadata) {
303            if (!persistenceStructureService.hasPrimaryKeyFieldValues(newBo)) {
304                throw new RuntimeException("Maintenance document did not have all primary key values filled in.");
305            }
306            Map fieldValues = persistenceService.getPrimaryKeyFieldValues(newBo);
307            Properties parameters = new Properties();
308            parameters.put(KNSConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, inactivationBlockingMetadata.getBlockedBusinessObjectClass().getName());
309            parameters.put(KNSConstants.DISPATCH_REQUEST_PARAMETER, KNSConstants.METHOD_DISPLAY_ALL_INACTIVATION_BLOCKERS);
310    
311            List keys = new ArrayList();
312            if (getPersistenceStructureService().isPersistable(newBo.getClass())) {
313                keys = getPersistenceStructureService().listPrimaryKeyFieldNames(newBo.getClass());
314            }
315    
316            // build key value url parameters used to retrieve the business object
317            String keyName = null;
318            for (Iterator iter = keys.iterator(); iter.hasNext();) {
319                keyName = (String) iter.next();
320    
321                Object keyValue = null;
322                if (keyName != null) {
323                    keyValue = ObjectUtils.getPropertyValue(newBo, keyName);
324                }
325    
326                if (keyValue == null) {
327                    keyValue = "";
328                } else if (keyValue instanceof java.sql.Date) { //format the date for passing in url
329                    if (Formatter.findFormatter(keyValue.getClass()) != null) {
330                        Formatter formatter = Formatter.getFormatter(keyValue.getClass());
331                        keyValue = (String) formatter.format(keyValue);
332                    }
333                } else {
334                    keyValue = keyValue.toString();
335                }
336    
337                // Encrypt value if it is a secure field
338                if (businessObjectAuthorizationService.attributeValueNeedsToBeEncryptedOnFormsAndLinks(inactivationBlockingMetadata.getBlockedBusinessObjectClass(), keyName)) {
339                    try {
340                        keyValue = KNSServiceLocator.getEncryptionService().encrypt(keyValue);
341                    }
342                    catch (GeneralSecurityException e) {
343                        LOG.error("Exception while trying to encrypted value for inquiry framework.", e);
344                        throw new RuntimeException(e);
345                    }
346                }
347    
348                parameters.put(keyName, keyValue);
349            }
350    
351            String blockingUrl = UrlFactory.parameterizeUrl(KNSConstants.DISPLAY_ALL_INACTIVATION_BLOCKERS_ACTION, parameters);
352    
353            // post an error about the locked document
354            GlobalVariables.getMessageMap().putError(KNSConstants.GLOBAL_ERRORS, RiceKeyConstants.ERROR_INACTIVATION_BLOCKED, blockingUrl);
355        }
356    
357        /**
358         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRule#processApproveDocument(ApproveDocumentEvent)
359         */
360        @Override
361        public boolean processApproveDocument(ApproveDocumentEvent approveEvent) {
362    
363            MaintenanceDocument maintenanceDocument = (MaintenanceDocument) approveEvent.getDocument();
364    
365            // remove all items from the errorPath temporarily (because it may not
366            // be what we expect, or what we need)
367            clearErrorPath();
368    
369            // setup convenience pointers to the old & new bo
370            setupBaseConvenienceObjects(maintenanceDocument);
371    
372            // apply rules that are common across all maintenance documents, regardless of class
373            processGlobalSaveDocumentBusinessRules(maintenanceDocument);
374    
375            // from here on, it is in a default-success mode, and will approve unless one of the
376            // business rules stop it.
377            boolean success = true;
378    
379            // apply rules that are common across all maintenance documents, regardless of class
380            success &= processGlobalApproveDocumentBusinessRules(maintenanceDocument);
381    
382            // apply rules that are specific to the class of the maintenance document
383            // (if implemented). this will always succeed if not overloaded by the
384            // subclass
385            success &= processCustomApproveDocumentBusinessRules(maintenanceDocument);
386    
387            // return the original set of items to the errorPath, to ensure no impact
388            // on other upstream or downstream items that rely on the errorPath
389            resumeErrorPath();
390    
391            return success;
392        }
393    
394        /**
395         *
396         * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field, but
397         * applicable to the whole document).
398         *
399         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
400         *
401         */
402        protected void putGlobalError(String errorConstant) {
403            if (!errorAlreadyExists(KNSConstants.DOCUMENT_ERRORS, errorConstant)) {
404                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KNSConstants.DOCUMENT_ERRORS, errorConstant);
405            }
406        }
407    
408        /**
409         *
410         * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field, but
411         * applicable to the whole document).
412         *
413         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
414         * @param parameter - Replacement value for part of the error message.
415         *
416         */
417        protected void putGlobalError(String errorConstant, String parameter) {
418            if (!errorAlreadyExists(KNSConstants.DOCUMENT_ERRORS, errorConstant)) {
419                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KNSConstants.DOCUMENT_ERRORS, errorConstant, parameter);
420            }
421        }
422    
423        /**
424         *
425         * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field, but
426         * applicable to the whole document).
427         *
428         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
429         * @param parameters - Array of replacement values for part of the error message.
430         *
431         */
432        protected void putGlobalError(String errorConstant, String[] parameters) {
433            if (!errorAlreadyExists(KNSConstants.DOCUMENT_ERRORS, errorConstant)) {
434                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KNSConstants.DOCUMENT_ERRORS, errorConstant, parameters);
435            }
436        }
437    
438        /**
439         *
440         * This method is a convenience method to add a property-specific error to the global errors list. This method makes sure that
441         * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
442         *
443         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
444         *        the UI.
445         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
446         *
447         */
448        protected void putFieldError(String propertyName, String errorConstant) {
449            if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
450                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant);
451            }
452        }
453    
454        /**
455         *
456         * This method is a convenience method to add a property-specific error to the global errors list. This method makes sure that
457         * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
458         *
459         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
460         *        the UI.
461         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
462         * @param parameter - Single parameter value that can be used in the message so that you can display specific values to the
463         *        user.
464         *
465         */
466        protected void putFieldError(String propertyName, String errorConstant, String parameter) {
467            if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
468                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant, parameter);
469            }
470        }
471    
472        /**
473         *
474         * This method is a convenience method to add a property-specific error to the global errors list. This method makes sure that
475         * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
476         *
477         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
478         *        the UI.
479         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
480         * @param parameters - Array of strings holding values that can be used in the message so that you can display specific values
481         *        to the user.
482         *
483         */
484        protected void putFieldError(String propertyName, String errorConstant, String[] parameters) {
485            if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
486                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant, parameters);
487            }
488        }
489    
490        /**
491         * Adds a property-specific error to the global errors list, with the DD short label as the single argument.
492         *
493         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
494         *        the UI.
495         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
496         */
497        protected void putFieldErrorWithShortLabel(String propertyName, String errorConstant) {
498            String shortLabel = ddService.getAttributeShortLabel(boClass, propertyName);
499            putFieldError(propertyName, errorConstant, shortLabel);
500        }
501    
502        /**
503         *
504         * This method is a convenience method to add a property-specific document error to the global errors list. This method makes
505         * sure that the correct prefix is added to the property name so that it will display correctly on maintenance documents.
506         *
507         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
508         *        the UI.
509         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
510         * @param parameter - Single parameter value that can be used in the message so that you can display specific values to the
511         *        user.
512         *
513         */
514        protected void putDocumentError(String propertyName, String errorConstant, String parameter) {
515            if (!errorAlreadyExists(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant)) {
516                GlobalVariables.getMessageMap().putError(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant, parameter);
517            }
518        }
519    
520        /**
521         *
522         * This method is a convenience method to add a property-specific document error to the global errors list. This method makes
523         * sure that the correct prefix is added to the property name so that it will display correctly on maintenance documents.
524         *
525         * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as errored in
526         *        the UI.
527         * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
528         * @param parameters - Array of String parameters that can be used in the message so that you can display specific values to the
529         *        user.
530         *
531         */
532        protected void putDocumentError(String propertyName, String errorConstant, String[] parameters) {
533            GlobalVariables.getMessageMap().putError(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant, parameters);
534        }
535    
536        /**
537         *
538         * Convenience method to determine whether the field already has the message indicated.
539         *
540         * This is useful if you want to suppress duplicate error messages on the same field.
541         *
542         * @param propertyName - propertyName you want to test on
543         * @param errorConstant - errorConstant you want to test
544         * @return returns True if the propertyName indicated already has the errorConstant indicated, false otherwise
545         *
546         */
547        protected boolean errorAlreadyExists(String propertyName, String errorConstant) {
548    
549            if (GlobalVariables.getMessageMap().fieldHasMessage(propertyName, errorConstant)) {
550                return true;
551            }
552            else {
553                return false;
554            }
555        }
556    
557        /**
558         *
559         * This method specifically doesn't put any prefixes before the error so that the developer can do things specific to the
560         * globals errors (like newDelegateChangeDocument errors)
561         *
562         * @param propertyName
563         * @param errorConstant
564         */
565        protected void putGlobalsError(String propertyName, String errorConstant) {
566            if (!errorAlreadyExists(propertyName, errorConstant)) {
567                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(propertyName, errorConstant);
568            }
569        }
570    
571        /**
572         *
573         * This method specifically doesn't put any prefixes before the error so that the developer can do things specific to the
574         * globals errors (like newDelegateChangeDocument errors)
575         *
576         * @param propertyName
577         * @param errorConstant
578         * @param parameter
579         */
580        protected void putGlobalsError(String propertyName, String errorConstant, String parameter) {
581            if (!errorAlreadyExists(propertyName, errorConstant)) {
582                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(propertyName, errorConstant, parameter);
583            }
584        }
585    
586        /**
587         *
588         * This method is used to deal with error paths that are not what we expect them to be. This method, along with
589         * resumeErrorPath() are used to temporarily clear the errorPath, and then return it to the original state after the rule is
590         * executed.
591         *
592         * This method is called at the very beginning of rule enforcement and pulls a copy of the contents of the errorPath ArrayList
593         * to a local arrayList for temporary storage.
594         *
595         */
596        protected void clearErrorPath() {
597    
598            // add all the items from the global list to the local list
599            priorErrorPath.addAll(GlobalVariables.getMessageMap().getErrorPath());
600    
601            // clear the global list
602            GlobalVariables.getMessageMap().getErrorPath().clear();
603    
604        }
605    
606        /**
607         *
608         * This method is used to deal with error paths that are not what we expect them to be. This method, along with clearErrorPath()
609         * are used to temporarily clear the errorPath, and then return it to the original state after the rule is executed.
610         *
611         * This method is called at the very end of the rule enforcement, and returns the temporarily stored copy of the errorPath to
612         * the global errorPath, so that no other classes are interrupted.
613         *
614         */
615        protected void resumeErrorPath() {
616            // revert the global errorPath back to what it was when we entered this
617            // class
618            GlobalVariables.getMessageMap().getErrorPath().addAll(priorErrorPath);
619        }
620    
621        /**
622         *
623         * This method executes the DataDictionary Validation against the document.
624         *
625         * @param document
626         * @return true if it passes DD validation, false otherwise
627         */
628        protected boolean dataDictionaryValidate(MaintenanceDocument document) {
629    
630            LOG.debug("MaintenanceDocument validation beginning");
631    
632            // explicitly put the errorPath that the dictionaryValidationService requires
633            GlobalVariables.getMessageMap().addToErrorPath("document.newMaintainableObject");
634    
635            // document must have a newMaintainable object
636            Maintainable newMaintainable = document.getNewMaintainableObject();
637            if (newMaintainable == null) {
638                GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject");
639                throw new ValidationException("Maintainable object from Maintenance Document '" + document.getDocumentTitle() + "' is null, unable to proceed.");
640            }
641    
642            // document's newMaintainable must contain an object (ie, not null)
643            PersistableBusinessObject businessObject = newMaintainable.getBusinessObject();
644            if (businessObject == null) {
645                GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject.");
646                throw new ValidationException("Maintainable's component business object is null.");
647            }
648    
649            // run required check from maintenance data dictionary
650            maintDocDictionaryService.validateMaintenanceRequiredFields(document);
651    
652            //check for duplicate entries in collections if necessary
653            maintDocDictionaryService.validateMaintainableCollectionsForDuplicateEntries(document);
654    
655            // run the DD DictionaryValidation (non-recursive)
656            dictionaryValidationService.validateBusinessObjectOnMaintenanceDocument(businessObject,
657                            document.getDocumentHeader().getWorkflowDocument().getDocumentType());
658    
659            // do default (ie, mandatory) existence checks
660            dictionaryValidationService.validateDefaultExistenceChecks(businessObject);
661    
662            // do apc checks
663            dictionaryValidationService.validateApcRules(businessObject);
664    
665    
666            // explicitly remove the errorPath we've added
667            GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject");
668    
669            LOG.debug("MaintenanceDocument validation ending");
670            return true;
671        }
672    
673        /**
674         *
675         * This method checks the two major cases that may violate primary key integrity.
676         *
677         * 1. Disallow changing of the primary keys on an EDIT maintenance document. Other fields can be changed, but once the primary
678         * keys have been set, they are permanent.
679         *
680         * 2. Disallow creating a new object whose primary key values are already present in the system on a CREATE NEW maintenance
681         * document.
682         *
683         * This method also will add new Errors to the Global Error Map.
684         *
685         * @param document - The Maintenance Document being tested.
686         * @return Returns false if either test failed, otherwise returns true.
687         *
688         */
689        protected boolean primaryKeyCheck(MaintenanceDocument document) {
690    
691            // default to success if no failures
692            boolean success = true;
693            Class boClass = document.getNewMaintainableObject().getBoClass();
694    
695            PersistableBusinessObject oldBo = document.getOldMaintainableObject().getBusinessObject();
696            PersistableBusinessObject newBo = document.getNewMaintainableObject().getBusinessObject();
697    
698            // We dont do primaryKeyChecks on Global Business Object maintenance documents. This is
699            // because it doesnt really make any sense to do so, given the behavior of Globals. When a
700            // Global Document completes, it will update or create a new record for each BO in the list.
701            // As a result, there's no problem with having existing BO records in the system, they will
702            // simply get updated.
703            if (newBo instanceof GlobalBusinessObject) {
704                return success;
705            }
706    
707            // fail and complain if the person has changed the primary keys on
708            // an EDIT maintenance document.
709            if (document.isEdit()) {
710                if (!ObjectUtils.equalByKeys(oldBo, newBo)) { // this is a very handy utility on our ObjectUtils
711    
712                    // add a complaint to the errors
713                    putDocumentError(KNSConstants.DOCUMENT_ERRORS, RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PRIMARY_KEYS_CHANGED_ON_EDIT, getHumanReadablePrimaryKeyFieldNames(boClass));
714                    success &= false;
715                }
716            }
717    
718            // fail and complain if the person has selected a new object with keys that already exist
719            // in the DB.
720            else if (document.isNew()) {
721    
722                // get a map of the pk field names and values
723                Map newPkFields = persistenceService.getPrimaryKeyFieldValues(newBo);
724    
725                // TODO: Good suggestion from Aaron, dont bother checking the DB, if all of the
726                // objects PK fields dont have values. If any are null or empty, then
727                // we're done. The current way wont fail, but it will make a wasteful
728                // DB call that may not be necessary, and we want to minimize these.
729    
730                // attempt to do a lookup, see if this object already exists by these Primary Keys
731                PersistableBusinessObject testBo = boService.findByPrimaryKey(boClass, newPkFields);
732    
733                // if the retrieve was successful, then this object already exists, and we need
734                // to complain
735                if (testBo != null) {
736                    putDocumentError(KNSConstants.DOCUMENT_ERRORS, RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_KEYS_ALREADY_EXIST_ON_CREATE_NEW, getHumanReadablePrimaryKeyFieldNames(boClass));
737                    success &= false;
738                }
739            }
740            return success;
741        }
742    
743        /**
744         *
745         * This method creates a human-readable string of the class' primary key field names, as designated by the DataDictionary.
746         *
747         * @param boClass
748         * @return
749         */
750        protected String getHumanReadablePrimaryKeyFieldNames(Class boClass) {
751    
752            String delim = "";
753            StringBuffer pkFieldNames = new StringBuffer();
754    
755            // get a list of all the primary key field names, walk through them
756            List pkFields = persistenceStructureService.getPrimaryKeys(boClass);
757            for (Iterator iter = pkFields.iterator(); iter.hasNext();) {
758                String pkFieldName = (String) iter.next();
759    
760                // use the DataDictionary service to translate field name into human-readable label
761                String humanReadableFieldName = ddService.getAttributeLabel(boClass, pkFieldName);
762    
763                // append the next field
764                pkFieldNames.append(delim + humanReadableFieldName);
765    
766                // separate names with commas after the first one
767                if (delim.equalsIgnoreCase("")) {
768                    delim = ", ";
769                }
770            }
771    
772            return pkFieldNames.toString();
773        }
774    
775        /**
776         * This method enforces all business rules that are common to all maintenance documents which must be tested before doing an
777         * approval.
778         *
779         * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary to what is
780         * enforced here.
781         *
782         * @param document - a populated MaintenanceDocument instance
783         * @return true if the document can be approved, false if not
784         */
785        protected boolean processGlobalApproveDocumentBusinessRules(MaintenanceDocument document) {
786            return true;
787        }
788    
789        /**
790         *
791         * This method enforces all business rules that are common to all maintenance documents which must be tested before doing a
792         * route.
793         *
794         * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary to what is
795         * enforced here.
796         *
797         * @param document - a populated MaintenanceDocument instance
798         * @return true if the document can be routed, false if not
799         */
800        protected boolean processGlobalRouteDocumentBusinessRules(MaintenanceDocument document) {
801    
802            boolean success = true;
803    
804            // require a document description field
805            success &= checkEmptyDocumentField(KNSPropertyConstants.DOCUMENT_HEADER + "." + KNSPropertyConstants.DOCUMENT_DESCRIPTION, document.getDocumentHeader().getDocumentDescription(), "Description");
806    
807            return success;
808        }
809    
810        /**
811         *
812         * This method enforces all business rules that are common to all maintenance documents which must be tested before doing a
813         * save.
814         *
815         * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary to what is
816         * enforced here.
817         *
818         * Note that although this method returns a true or false to indicate whether the save should happen or not, this result may not
819         * be followed by the calling method. In other words, the boolean result will likely be ignored, and the document saved,
820         * regardless.
821         *
822         * @param document - a populated MaintenanceDocument instance
823         * @return true if all business rules succeed, false if not
824         */
825        protected boolean processGlobalSaveDocumentBusinessRules(MaintenanceDocument document) {
826    
827            // default to success
828            boolean success = true;
829    
830            // do generic checks that impact primary key violations
831            primaryKeyCheck(document);
832    
833            // this is happening only on the processSave, since a Save happens in both the
834            // Route and Save events.
835            this.dataDictionaryValidate(document);
836    
837            return success;
838        }
839    
840        /**
841         * This method should be overridden to provide custom rules for processing document saving
842         *
843         * @param document
844         * @return boolean
845         */
846        protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
847            return true;
848        }
849    
850        /**
851         *
852         * This method should be overridden to provide custom rules for processing document routing
853         *
854         * @param document
855         * @return boolean
856         */
857        protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
858            return true;
859        }
860    
861        /**
862         * This method should be overridden to provide custom rules for processing document approval.
863         *
864         * @param document
865         * @return booelan
866         */
867        protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
868            return true;
869        }
870    
871        // Document Validation Helper Methods
872    
873        /**
874         *
875         * This method checks to see if the document is in a state that it can be saved without causing exceptions.
876         *
877         * Note that Business Rules are NOT enforced here, only validity checks.
878         *
879         * This method will only return false if the document is in such a state that routing it will cause RunTimeExceptions.
880         *
881         * @param maintenanceDocument - a populated MaintenaceDocument instance.
882         * @return boolean - returns true unless the object is in an invalid state.
883         *
884         */
885        protected boolean isDocumentValidForSave(MaintenanceDocument maintenanceDocument) {
886    
887            boolean success = true;
888    
889            success &= super.isDocumentOverviewValid(maintenanceDocument);
890            success &= validateDocumentStructure((Document) maintenanceDocument);
891            success &= validateMaintenanceDocument(maintenanceDocument);
892            success &= validateGlobalBusinessObjectPersistable(maintenanceDocument);
893            return success;
894        }
895    
896        /**
897         *
898         * This method makes sure the document itself is valid, and has the necessary fields populated to be routable.
899         *
900         * This is not a business rules test, rather its a structure test to make sure that the document will not cause exceptions
901         * before routing.
902         *
903         * @param document - document to be tested
904         * @return false if the document is missing key values, true otherwise
905         */
906        protected boolean validateDocumentStructure(Document document) {
907            boolean success = true;
908    
909            // document must have a populated documentNumber
910            String documentHeaderId = document.getDocumentNumber();
911            if (documentHeaderId == null || StringUtils.isEmpty(documentHeaderId)) {
912                throw new ValidationException("Document has no document number, unable to proceed.");
913            }
914    
915            return success;
916        }
917    
918        /**
919         *
920         * This method checks to make sure the document is a valid maintenanceDocument, and has the necessary values populated such that
921         * it will not cause exceptions in later routing or business rules testing.
922         *
923         * This is not a business rules test.
924         *
925         * @param maintenanceDocument - document to be tested
926         * @return whether maintenance doc passes
927         * @throws ValidationException
928         */
929        protected boolean validateMaintenanceDocument(MaintenanceDocument maintenanceDocument) {
930            boolean success = true;
931            Maintainable newMaintainable = maintenanceDocument.getNewMaintainableObject();
932    
933            // document must have a newMaintainable object
934            if (newMaintainable == null) {
935                throw new ValidationException("Maintainable object from Maintenance Document '" + maintenanceDocument.getDocumentTitle() + "' is null, unable to proceed.");
936            }
937    
938            // document's newMaintainable must contain an object (ie, not null)
939            if (newMaintainable.getBusinessObject() == null) {
940                throw new ValidationException("Maintainable's component business object is null.");
941            }
942    
943            // document's newMaintainable must contain a valid BusinessObject descendent
944            if (!PersistableBusinessObject.class.isAssignableFrom(newMaintainable.getBoClass())) {
945                throw new ValidationException("Maintainable's component object is not descended from BusinessObject.");
946            }
947            return success;
948        }
949    
950        /**
951         *
952         * This method checks whether this maint doc contains Global Business Objects, and if so, whether the GBOs are in a persistable
953         * state. This will return false if this method determines that the GBO will cause a SQL Exception when the document is
954         * persisted.
955         *
956         * @param document
957         * @return False when the method determines that the contained Global Business Object will cause a SQL Exception, and the
958         *         document should not be saved. It will return True otherwise.
959         *
960         */
961        protected boolean validateGlobalBusinessObjectPersistable(MaintenanceDocument document) {
962            boolean success = true;
963    
964            if (document.getNewMaintainableObject() == null) {
965                return success;
966            }
967            if (document.getNewMaintainableObject().getBusinessObject() == null) {
968                return success;
969            }
970            if (!(document.getNewMaintainableObject().getBusinessObject() instanceof GlobalBusinessObject)) {
971                return success;
972            }
973    
974            PersistableBusinessObject bo = (PersistableBusinessObject) document.getNewMaintainableObject().getBusinessObject();
975            GlobalBusinessObject gbo = (GlobalBusinessObject) bo;
976            return gbo.isPersistable();
977        }
978    
979        /**
980         *
981         * This method tests to make sure the MaintenanceDocument passed in is based on the class you are expecting.
982         *
983         * It does this based on the NewMaintainableObject of the MaintenanceDocument.
984         *
985         * @param document - MaintenanceDocument instance you want to test
986         * @param clazz - class you are expecting the MaintenanceDocument to be based on
987         * @return true if they match, false if not
988         *
989         */
990        protected boolean isCorrectMaintenanceClass(MaintenanceDocument document, Class clazz) {
991    
992            // disallow null arguments
993            if (document == null || clazz == null) {
994                throw new IllegalArgumentException("Null arguments were passed in.");
995            }
996    
997            // compare the class names
998            if (clazz.toString().equals(document.getNewMaintainableObject().getBoClass().toString())) {
999                return true;
1000            }
1001            else {
1002                return false;
1003            }
1004        }
1005    
1006        /**
1007         *
1008         * This method accepts an object, and attempts to determine whether it is empty by this method's definition.
1009         *
1010         * OBJECT RESULT null false empty-string false whitespace false otherwise true
1011         *
1012         * If the result is false, it will add an object field error to the Global Errors.
1013         *
1014         * @param valueToTest - any object to test, usually a String
1015         * @param propertyName - the name of the property being tested
1016         * @return true or false, by the description above
1017         *
1018         */
1019        protected boolean checkEmptyBOField(String propertyName, Object valueToTest, String parameter) {
1020    
1021            boolean success = true;
1022    
1023            success = checkEmptyValue(valueToTest);
1024    
1025            // if failed, then add a field error
1026            if (!success) {
1027                putFieldError(propertyName, RiceKeyConstants.ERROR_REQUIRED, parameter);
1028            }
1029    
1030            return success;
1031        }
1032    
1033        /**
1034         *
1035         * This method accepts document field (such as , and attempts to determine whether it is empty by this method's definition.
1036         *
1037         * OBJECT RESULT null false empty-string false whitespace false otherwise true
1038         *
1039         * If the result is false, it will add document field error to the Global Errors.
1040         *
1041         * @param valueToTest - any object to test, usually a String
1042         * @param propertyName - the name of the property being tested
1043         * @return true or false, by the description above
1044         *
1045         */
1046        protected boolean checkEmptyDocumentField(String propertyName, Object valueToTest, String parameter) {
1047            boolean success = true;
1048            success = checkEmptyValue(valueToTest);
1049            if (!success) {
1050                putDocumentError(propertyName, RiceKeyConstants.ERROR_REQUIRED, parameter);
1051            }
1052            return success;
1053        }
1054    
1055        /**
1056         *
1057         * This method accepts document field (such as , and attempts to determine whether it is empty by this method's definition.
1058         *
1059         * OBJECT RESULT null false empty-string false whitespace false otherwise true
1060         *
1061         * It will the result as a boolean
1062         *
1063         * @param valueToTest - any object to test, usually a String
1064         *
1065         */
1066        protected boolean checkEmptyValue(Object valueToTest) {
1067            boolean success = true;
1068    
1069            // if its not a string, only fail if its a null object
1070            if (valueToTest == null) {
1071                success = false;
1072            }
1073            else {
1074                // test for null, empty-string, or whitespace if its a string
1075                if (valueToTest instanceof String) {
1076                    if (StringUtils.isBlank((String) valueToTest)) {
1077                        success = false;
1078                    }
1079                }
1080            }
1081    
1082            return success;
1083        }
1084    
1085        /**
1086         *
1087         * This method is used during debugging to dump the contents of the error map, including the key names. It is not used by the
1088         * application in normal circumstances at all.
1089         *
1090         */
1091        protected void showErrorMap() {
1092    
1093            if (GlobalVariables.getMessageMap().hasNoErrors()) {
1094                return;
1095            }
1096    
1097            for (Iterator i = GlobalVariables.getMessageMap().getAllPropertiesAndErrors().iterator(); i.hasNext();) {
1098                Map.Entry e = (Map.Entry) i.next();
1099    
1100                TypedArrayList errorList = (TypedArrayList) e.getValue();
1101                for (Iterator j = errorList.iterator(); j.hasNext();) {
1102                    ErrorMessage em = (ErrorMessage) j.next();
1103    
1104                    if (em.getMessageParameters() == null) {
1105                        LOG.error(e.getKey().toString() + " = " + em.getErrorKey());
1106                    }
1107                    else {
1108                        LOG.error(e.getKey().toString() + " = " + em.getErrorKey() + " : " + em.getMessageParameters().toString());
1109                    }
1110                }
1111            }
1112    
1113        }
1114    
1115        /**
1116         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRule#setupBaseConvenienceObjects(MaintenanceDocument)
1117         */
1118        public void setupBaseConvenienceObjects(MaintenanceDocument document) {
1119    
1120            // setup oldAccount convenience objects, make sure all possible sub-objects are populated
1121            oldBo = (PersistableBusinessObject) document.getOldMaintainableObject().getBusinessObject();
1122            if (oldBo != null) {
1123                oldBo.refreshNonUpdateableReferences();
1124            }
1125    
1126            // setup newAccount convenience objects, make sure all possible sub-objects are populated
1127            newBo = (PersistableBusinessObject) document.getNewMaintainableObject().getBusinessObject();
1128            newBo.refreshNonUpdateableReferences();
1129    
1130            boClass = document.getNewMaintainableObject().getBoClass();
1131    
1132            // call the setupConvenienceObjects in the subclass, if a subclass exists
1133            setupConvenienceObjects();
1134        }
1135    
1136        public void setupConvenienceObjects() {
1137            // should always be overriden by subclass
1138        }
1139    
1140        /**
1141         *
1142         * This method checks to make sure that if the foreign-key fields for the given reference attributes have any fields filled out,
1143         * that all fields are filled out.
1144         *
1145         * If any are filled out, but all are not, it will return false and add a global error message about the problem.
1146         *
1147         * @param referenceName - The name of the reference object, whose foreign-key fields must be all-or-none filled out.
1148         *
1149         * @return true if this is the case, false if not
1150         *
1151         */
1152        protected boolean checkForPartiallyFilledOutReferenceForeignKeys(String referenceName) {
1153    
1154            boolean success;
1155    
1156            ForeignKeyFieldsPopulationState fkFieldsState;
1157            fkFieldsState = persistenceStructureService.getForeignKeyFieldsPopulationState(newBo, referenceName);
1158    
1159            // determine result
1160            if (fkFieldsState.isAnyFieldsPopulated() && !fkFieldsState.isAllFieldsPopulated()) {
1161                success = false;
1162            }
1163            else {
1164                success = true;
1165            }
1166    
1167            // add errors if appropriate
1168            if (!success) {
1169    
1170                // get the full set of foreign-keys
1171                List fKeys = new ArrayList(persistenceStructureService.getForeignKeysForReference(newBo.getClass(), referenceName).keySet());
1172                String fKeysReadable = consolidateFieldNames(fKeys, ", ").toString();
1173    
1174                // walk through the missing fields
1175                for (Iterator iter = fkFieldsState.getUnpopulatedFieldNames().iterator(); iter.hasNext();) {
1176                    String fieldName = (String) iter.next();
1177    
1178                    // get the human-readable name
1179                    String fieldNameReadable = ddService.getAttributeLabel(newBo.getClass(), fieldName);
1180    
1181                    // add a field error
1182                    putFieldError(fieldName, RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PARTIALLY_FILLED_OUT_REF_FKEYS, new String[] { fieldNameReadable, fKeysReadable });
1183                }
1184            }
1185    
1186            return success;
1187        }
1188    
1189        /**
1190         *
1191         * This method is a shallow inverse wrapper around applyApcRule, it simply reverses the return value, for better readability in
1192         * an if test.
1193         *
1194         * This method applies an APC rule based on the values provided.
1195         *
1196         * It will throw an ApplicationParameterException if the APC Group and Parm do not exist in the system.
1197         *
1198         * @param apcGroupName - The script or group name in the APC system. If the value is null or blank, an IllegalArgumentException
1199         *        will be thrown.
1200         * @param parameterName - The name of the parm/rule in the APC system. If the value is null or blank, an IllegalArgumentException
1201         *        will be thrown.
1202         * @param valueToTest - The String value to test against the APC rule. The value may be null or blank without throwing an error,
1203         *        but the rule will likely fail if null or blank.
1204         * @return True if the rule fails, False if the rule passes.
1205         *
1206         */
1207        protected boolean apcRuleFails(String parameterNamespace, String parameterDetailTypeCode, String parameterName, String valueToTest) {
1208            if (applyApcRule(parameterNamespace, parameterDetailTypeCode, parameterName, valueToTest) == false) {
1209                return true;
1210            }
1211            return false;
1212        }
1213    
1214        /**
1215         *
1216         * This method applies an APC rule based on the values provided.
1217         *
1218         * It will throw an ApplicationParameterException if the APC Group and Parm do not exist in the system.
1219         *
1220         * @param apcGroupName - The script or group name in the APC system. If the value is null or blank, an IllegalArgumentException
1221         *        will be thrown.
1222         * @param parameterName - The name of the parm/rule in the APC system. If the value is null or blank, an IllegalArgumentException
1223         *        will be thrown.
1224         * @param valueToTest - The String value to test against the APC rule. The value may be null or blank without throwing an error,
1225         *        but the rule will likely fail if null or blank.
1226         * @return True if the rule passes, False if the rule fails.
1227         *
1228         */
1229        protected boolean applyApcRule(String parameterNamespace, String parameterDetailTypeCode, String parameterName, String valueToTest) {
1230    
1231            // default to success
1232            boolean success = true;
1233    
1234            // apply the rule, see if it fails
1235            if (!KNSServiceLocator.getParameterService().getParameterEvaluator(parameterNamespace, parameterDetailTypeCode, parameterName, valueToTest).evaluationSucceeds()) {
1236                success = false;
1237            }
1238    
1239            return success;
1240        }
1241    
1242        /**
1243         *
1244         * This method turns a list of field property names, into a delimited string of the human-readable names.
1245         *
1246         * @param fieldNames - List of fieldNames
1247         * @return A filled StringBuffer ready to go in an error message
1248         *
1249         */
1250        protected StringBuffer consolidateFieldNames(List fieldNames, String delimiter) {
1251    
1252            StringBuffer sb = new StringBuffer();
1253    
1254            // setup some vars
1255            boolean firstPass = true;
1256            String delim = "";
1257    
1258            // walk through the list
1259            for (Iterator iter = fieldNames.iterator(); iter.hasNext();) {
1260                String fieldName = (String) iter.next();
1261    
1262                // get the human-readable name
1263                // add the new one, with the appropriate delimiter
1264                sb.append(delim + ddService.getAttributeLabel(newBo.getClass(), fieldName));
1265    
1266                // after the first item, start using a delimiter
1267                if (firstPass) {
1268                    delim = delimiter;
1269                    firstPass = false;
1270                }
1271            }
1272    
1273            return sb;
1274        }
1275    
1276        /**
1277         *
1278         * This method translates the passed in field name into a human-readable attribute label.
1279         *
1280         * It assumes the existing newBO's class as the class to examine the fieldName for.
1281         *
1282         * @param fieldName The fieldName you want a human-readable label for.
1283         * @return A human-readable label, pulled from the DataDictionary.
1284         *
1285         */
1286        protected String getFieldLabel(String fieldName) {
1287            return ddService.getAttributeLabel(newBo.getClass(), fieldName) + "(" + ddService.getAttributeShortLabel(newBo.getClass(), fieldName) + ")";
1288        }
1289    
1290        /**
1291         *
1292         * This method translates the passed in field name into a human-readable attribute label.
1293         *
1294         * It assumes the existing newBO's class as the class to examine the fieldName for.
1295         *
1296         * @param boClass The class to use in combination with the fieldName.
1297         * @param fieldName The fieldName you want a human-readable label for.
1298         * @return A human-readable label, pulled from the DataDictionary.
1299         *
1300         */
1301        protected String getFieldLabel(Class boClass, String fieldName) {
1302            return ddService.getAttributeLabel(boClass, fieldName) + "(" + ddService.getAttributeShortLabel(boClass, fieldName) + ")";
1303        }
1304    
1305        /**
1306         * Gets the boDictionaryService attribute.
1307         *
1308         * @return Returns the boDictionaryService.
1309         */
1310        protected final BusinessObjectDictionaryService getBoDictionaryService() {
1311            return boDictionaryService;
1312        }
1313    
1314        /**
1315         * Sets the boDictionaryService attribute value.
1316         *
1317         * @param boDictionaryService The boDictionaryService to set.
1318         */
1319        public final void setBoDictionaryService(BusinessObjectDictionaryService boDictionaryService) {
1320            this.boDictionaryService = boDictionaryService;
1321        }
1322    
1323        /**
1324         * Gets the boService attribute.
1325         *
1326         * @return Returns the boService.
1327         */
1328        protected final BusinessObjectService getBoService() {
1329            return boService;
1330        }
1331    
1332        /**
1333         * Sets the boService attribute value.
1334         *
1335         * @param boService The boService to set.
1336         */
1337        public final void setBoService(BusinessObjectService boService) {
1338            this.boService = boService;
1339        }
1340    
1341        /**
1342         * Gets the configService attribute.
1343         *
1344         * @return Returns the configService.
1345         */
1346        protected final KualiConfigurationService getConfigService() {
1347            return configService;
1348        }
1349    
1350        /**
1351         * Sets the configService attribute value.
1352         *
1353         * @param configService The configService to set.
1354         */
1355        public final void setConfigService(KualiConfigurationService configService) {
1356            this.configService = configService;
1357        }
1358    
1359        /**
1360         * Gets the ddService attribute.
1361         *
1362         * @return Returns the ddService.
1363         */
1364        protected final DataDictionaryService getDdService() {
1365            return ddService;
1366        }
1367    
1368        /**
1369         * Sets the ddService attribute value.
1370         *
1371         * @param ddService The ddService to set.
1372         */
1373        public final void setDdService(DataDictionaryService ddService) {
1374            this.ddService = ddService;
1375        }
1376    
1377        /**
1378         * Gets the dictionaryValidationService attribute.
1379         *
1380         * @return Returns the dictionaryValidationService.
1381         */
1382        protected final DictionaryValidationService getDictionaryValidationService() {
1383            return dictionaryValidationService;
1384        }
1385    
1386        /**
1387         * Sets the dictionaryValidationService attribute value.
1388         *
1389         * @param dictionaryValidationService The dictionaryValidationService to set.
1390         */
1391        public final void setDictionaryValidationService(DictionaryValidationService dictionaryValidationService) {
1392            this.dictionaryValidationService = dictionaryValidationService;
1393        }
1394    
1395        /**
1396         * Gets the maintDocDictionaryService attribute.
1397         *
1398         * @return Returns the maintDocDictionaryService.
1399         */
1400        protected final MaintenanceDocumentDictionaryService getMaintDocDictionaryService() {
1401            return maintDocDictionaryService;
1402        }
1403    
1404        /**
1405         * Sets the maintDocDictionaryService attribute value.
1406         *
1407         * @param maintDocDictionaryService The maintDocDictionaryService to set.
1408         */
1409        public final void setMaintDocDictionaryService(MaintenanceDocumentDictionaryService maintDocDictionaryService) {
1410            this.maintDocDictionaryService = maintDocDictionaryService;
1411        }
1412    
1413        /**
1414         * Gets the newBo attribute.
1415         *
1416         * @return Returns the newBo.
1417         */
1418        protected final PersistableBusinessObject getNewBo() {
1419            return newBo;
1420        }
1421    
1422        protected void setNewBo(PersistableBusinessObject newBo) {
1423            this.newBo = newBo;
1424        }
1425    
1426        /**
1427         * Gets the oldBo attribute.
1428         *
1429         * @return Returns the oldBo.
1430         */
1431        protected final PersistableBusinessObject getOldBo() {
1432            return oldBo;
1433        }
1434    
1435        /**
1436         * Gets the persistenceService attribute.
1437         *
1438         * @return Returns the persistenceService.
1439         */
1440        protected final PersistenceService getPersistenceService() {
1441            return persistenceService;
1442        }
1443    
1444        /**
1445         * Sets the persistenceService attribute value.
1446         *
1447         * @param persistenceService The persistenceService to set.
1448         */
1449        public final void setPersistenceService(PersistenceService persistenceService) {
1450            this.persistenceService = persistenceService;
1451        }
1452    
1453        /**
1454         * Gets the persistenceStructureService attribute.
1455         *
1456         * @return Returns the persistenceStructureService.
1457         */
1458        protected final PersistenceStructureService getPersistenceStructureService() {
1459            return persistenceStructureService;
1460        }
1461    
1462        /**
1463         * Sets the persistenceStructureService attribute value.
1464         *
1465         * @param persistenceStructureService The persistenceStructureService to set.
1466         */
1467        public final void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
1468            this.persistenceStructureService = persistenceStructureService;
1469        }
1470    
1471        /**
1472         * Gets the workflowDocumentService attribute.
1473         *
1474         * @return Returns the workflowDocumentService.
1475         */
1476        public WorkflowDocumentService getWorkflowDocumentService() {
1477            return workflowDocumentService;
1478        }
1479    
1480        /**
1481         * Sets the workflowDocumentService attribute value.
1482         *
1483         * @param workflowDocumentService The workflowDocumentService to set.
1484         */
1485        public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
1486            this.workflowDocumentService = workflowDocumentService;
1487        }
1488    
1489        public boolean processAddCollectionLineBusinessRules(MaintenanceDocument document, String collectionName, PersistableBusinessObject bo) {
1490            LOG.debug("processAddCollectionLineBusinessRules");
1491    
1492            // setup convenience pointers to the old & new bo
1493            setupBaseConvenienceObjects(document);
1494    
1495            // sanity check on the document object
1496            this.validateMaintenanceDocument( document );
1497    
1498            boolean success = true;
1499            MessageMap map = GlobalVariables.getMessageMap();
1500            int errorCount = map.getErrorCount();
1501            map.addToErrorPath( MAINTAINABLE_ERROR_PATH );
1502            if ( LOG.isDebugEnabled() ) {
1503                LOG.debug( "processAddCollectionLineBusinessRules - BO: " + bo );
1504                LOG.debug( "Before Validate: " + map );
1505            }
1506            //getBoDictionaryService().performForceUppercase(bo);
1507            getMaintDocDictionaryService().validateMaintainableCollectionsAddLineRequiredFields( document, document.getNewMaintainableObject().getBusinessObject(), collectionName );
1508            String errorPath = KNSConstants.MAINTENANCE_ADD_PREFIX + collectionName;
1509            map.addToErrorPath( errorPath );
1510          
1511            getDictionaryValidationService().validateBusinessObject( bo, false );
1512            success &= map.getErrorCount() == errorCount;
1513            success &= dictionaryValidationService.validateDefaultExistenceChecksForNewCollectionItem(document.getNewMaintainableObject().getBusinessObject(), bo, collectionName);
1514            success &= validateDuplicateIdentifierInDataDictionary(document, collectionName, bo);
1515            success &= processCustomAddCollectionLineBusinessRules( document, collectionName, bo );
1516        
1517            map.removeFromErrorPath( errorPath );
1518            map.removeFromErrorPath( MAINTAINABLE_ERROR_PATH );
1519            if ( LOG.isDebugEnabled() ) {
1520                LOG.debug( "After Validate: " + map );
1521                LOG.debug( "processAddCollectionLineBusinessRules returning: " + success );
1522            }
1523    
1524            return success;
1525        }
1526    
1527        /**
1528         * This method validates that there should only exist one entry in the collection whose
1529         * fields match the fields specified within the duplicateIdentificationFields in the
1530         * maintenance document data dictionary.
1531         * If the duplicateIdentificationFields is not specified in the DD, by default it would
1532         * allow the addition to happen and return true.
1533         * It will return false if it fails the uniqueness validation.
1534         * @param document
1535         * @param collectionName
1536         * @param bo
1537         * @return
1538         */
1539        protected boolean validateDuplicateIdentifierInDataDictionary(MaintenanceDocument document, String collectionName, PersistableBusinessObject bo) {
1540            boolean valid = true;
1541            PersistableBusinessObject maintBo = document.getNewMaintainableObject().getBusinessObject();
1542            Collection maintCollection = (Collection) ObjectUtils.getPropertyValue(maintBo, collectionName);
1543            List<String> duplicateIdentifier = document.getNewMaintainableObject().getDuplicateIdentifierFieldsFromDataDictionary(document.getDocumentHeader().getWorkflowDocument().getDocumentType(), collectionName);
1544            if (duplicateIdentifier.size()>0) {
1545                List<String> existingIdentifierString = document.getNewMaintainableObject().getMultiValueIdentifierList(maintCollection, duplicateIdentifier);
1546                if (document.getNewMaintainableObject().hasBusinessObjectExisted(bo, existingIdentifierString, duplicateIdentifier)) {
1547                        valid = false;
1548                        GlobalVariables.getMessageMap().putError(duplicateIdentifier.get(0), RiceKeyConstants.ERROR_DUPLICATE_ELEMENT, "entries in ", document.getDocumentHeader().getWorkflowDocument().getDocumentType());
1549                }
1550            }
1551            return valid;
1552        }
1553    
1554        public boolean processCustomAddCollectionLineBusinessRules(MaintenanceDocument document, String collectionName, PersistableBusinessObject line) {
1555            return true;
1556        }
1557    
1558        public org.kuali.rice.kim.service.PersonService getPersonService() {
1559            return personService;
1560        }
1561    
1562        public void setPersonService(org.kuali.rice.kim.service.PersonService personService) {
1563            this.personService = personService;
1564        }
1565    
1566        public DateTimeService getDateTimeService() {
1567            return KNSServiceLocator.getDateTimeService();
1568        }
1569    
1570        /**
1571         * @return the documentHelperService
1572         */
1573        public DocumentHelperService getDocumentHelperService() {
1574            return this.documentHelperService;
1575        }
1576    
1577        /**
1578         * @param documentHelperService the documentHelperService to set
1579         */
1580        public void setDocumentHelperService(DocumentHelperService documentHelperService) {
1581            this.documentHelperService = documentHelperService;
1582        }
1583    
1584            /**
1585             * @return the businessObjectAuthorizationService
1586             */
1587            public BusinessObjectAuthorizationService getBusinessObjectAuthorizationService() {
1588                    return this.businessObjectAuthorizationService;
1589            }
1590    
1591            /**
1592             * @param businessObjectAuthorizationService the businessObjectAuthorizationService to set
1593             */
1594            public void setBusinessObjectAuthorizationService(
1595                            BusinessObjectAuthorizationService businessObjectAuthorizationService) {
1596                    this.businessObjectAuthorizationService = businessObjectAuthorizationService;
1597            }
1598    
1599        protected RoleService getRoleService(){
1600            if(this.roleService==null){
1601                this.roleService = KIMServiceLocator.getRoleService();
1602            }
1603            return this.roleService;
1604        }
1605    
1606    }
1607