001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.service.impl;
017    
018    import java.io.Serializable;
019    import java.util.ArrayList;
020    import java.util.Arrays;
021    import java.util.List;
022    import java.util.Map;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.apache.log4j.Logger;
026    import org.kuali.rice.core.api.criteria.Predicate;
027    import org.kuali.rice.core.api.criteria.PredicateFactory;
028    import org.kuali.rice.core.api.criteria.QueryByCriteria;
029    import org.kuali.rice.core.api.util.RiceKeyConstants;
030    import org.kuali.rice.core.api.util.io.SerializationUtils;
031    import org.kuali.rice.core.framework.persistence.jta.TransactionalNoValidationExceptionRollback;
032    import org.kuali.rice.kew.api.exception.WorkflowException;
033    import org.kuali.rice.krad.bo.DataObjectBase;
034    import org.kuali.rice.krad.bo.PersistableBusinessObject;
035    import org.kuali.rice.krad.data.DataObjectService;
036    import org.kuali.rice.krad.data.KradDataServiceLocator;
037    import org.kuali.rice.krad.exception.DocumentTypeAuthorizationException;
038    import org.kuali.rice.krad.maintenance.Maintainable;
039    import org.kuali.rice.krad.maintenance.MaintenanceDocument;
040    import org.kuali.rice.krad.maintenance.MaintenanceLock;
041    import org.kuali.rice.krad.service.DataObjectAuthorizationService;
042    import org.kuali.rice.krad.service.DocumentDictionaryService;
043    import org.kuali.rice.krad.service.DocumentService;
044    import org.kuali.rice.krad.service.LegacyDataAdapter;
045    import org.kuali.rice.krad.service.MaintenanceDocumentService;
046    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
047    import org.kuali.rice.krad.util.GlobalVariables;
048    import org.kuali.rice.krad.util.KRADConstants;
049    import org.kuali.rice.krad.util.KRADPropertyConstants;
050    import org.kuali.rice.krad.util.KRADUtils;
051    import org.springframework.beans.factory.annotation.Required;
052    
053    /**
054     * Service implementation for the MaintenanceDocument structure. This is the
055     * default implementation, that is delivered with Kuali
056     *
057     * @author Kuali Rice Team (rice.collab@kuali.org)
058     */
059    @TransactionalNoValidationExceptionRollback
060    public class MaintenanceDocumentServiceImpl implements MaintenanceDocumentService {
061        private static final Logger LOG = Logger.getLogger(MaintenanceDocumentServiceImpl.class);
062    
063        protected LegacyDataAdapter legacyDataAdapter;
064        protected DataObjectService dataObjectService;
065        protected DataObjectAuthorizationService dataObjectAuthorizationService;
066        protected DocumentService documentService;
067        protected DocumentDictionaryService documentDictionaryService;
068    
069        /**
070         * @see org.kuali.rice.krad.service.MaintenanceDocumentService#setupNewMaintenanceDocument(java.lang.String,
071         *      java.lang.String, java.lang.String)
072         */
073        @Override
074            @SuppressWarnings("unchecked")
075        public MaintenanceDocument setupNewMaintenanceDocument(String objectClassName, String documentTypeName,
076                String maintenanceAction) {
077            if (StringUtils.isEmpty(objectClassName) && StringUtils.isEmpty(documentTypeName)) {
078                throw new IllegalArgumentException("Document type name or bo class not given!");
079            }
080    
081            // get document type if not passed
082            if (StringUtils.isEmpty(documentTypeName)) {
083                try {
084                    documentTypeName =
085                            getDocumentDictionaryService().getMaintenanceDocumentTypeName(Class.forName(objectClassName));
086                } catch (ClassNotFoundException e) {
087                    throw new RuntimeException(e);
088                }
089    
090                if (StringUtils.isEmpty(documentTypeName)) {
091                    throw new RuntimeException(
092                            "documentTypeName is empty; does this Business Object have a maintenance document definition? " +
093                                    objectClassName);
094                }
095            }
096    
097            // check doc type allows new or copy if that action was requested
098            if (KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction) ||
099                    KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction)) {
100                Class<?> boClass =
101                        getDocumentDictionaryService().getMaintenanceDataObjectClass(documentTypeName);
102                boolean allowsNewOrCopy = getDataObjectAuthorizationService()
103                        .canCreate(boClass, GlobalVariables.getUserSession().getPerson(), documentTypeName);
104                if (!allowsNewOrCopy) {
105                    LOG.error("Document type " + documentTypeName + " does not allow new or copy actions.");
106                    throw new DocumentTypeAuthorizationException(
107                            GlobalVariables.getUserSession().getPerson().getPrincipalId(), "newOrCopy", documentTypeName);
108                }
109            }
110    
111            // get new document from service
112            try {
113                return (MaintenanceDocument) getDocumentService().getNewDocument(documentTypeName);
114            } catch (WorkflowException e) {
115                LOG.error("Cannot get new maintenance document instance for doc type: " + documentTypeName, e);
116                throw new RuntimeException("Cannot get new maintenance document instance for doc type: " + documentTypeName,
117                        e);
118            }
119        }
120    
121        /**
122         * @see org.kuali.rice.krad.service.impl.MaintenanceDocumentServiceImpl#setupMaintenanceObject
123         */
124        @Override
125        public void setupMaintenanceObject(MaintenanceDocument document, String maintenanceAction,
126                Map<String, String[]> requestParameters) {
127            document.getNewMaintainableObject().setMaintenanceAction(maintenanceAction);
128            document.getOldMaintainableObject().setMaintenanceAction(maintenanceAction);
129    
130            // if action is delete, check that object can be deleted
131            if (KRADConstants.MAINTENANCE_DELETE_ACTION.equals(maintenanceAction))
132            {
133                checkMaintenanceActionAuthorization(document, document.getOldMaintainableObject(),
134                        maintenanceAction, requestParameters);
135            }
136    
137            // if action is edit or copy first need to retrieve the old record
138            if (!KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction) &&
139                    !KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(maintenanceAction)) {
140                Object oldDataObject = retrieveObjectForMaintenance(document, requestParameters);
141    
142                Object newDataObject = null;
143    
144                // TODO should we be using ObjectUtils? also, this needs dictionary
145                // enhancement to indicate fields to/not to copy
146                if (dataObjectService.supports(oldDataObject.getClass())) {
147                    newDataObject = dataObjectService.copyInstance(oldDataObject);
148                } else {
149                    newDataObject = SerializationUtils.deepCopy((Serializable) oldDataObject);
150                }
151    
152                // set object instance for editing
153                document.getOldMaintainableObject().setDataObject(oldDataObject);
154                document.getNewMaintainableObject().setDataObject(newDataObject);
155    
156                if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction) && !document.isFieldsClearedOnCopy()) {
157                    Maintainable maintainable = document.getNewMaintainableObject();
158    
159                    // Since this will be a new object, we also need to blank out the object ID and version number fields
160                    // (if they exist).  If the object uses a different locking key or unique ID field, the blanking of
161                    // these will need to be done in the Maintainable.processAfterCopy() method.
162                    if ( maintainable.getDataObject() instanceof DataObjectBase ) {
163                        ((DataObjectBase) maintainable.getDataObject()).setObjectId(null);
164                        ((DataObjectBase) maintainable.getDataObject()).setVersionNumber(null);
165                    } else if ( maintainable.getDataObject() instanceof PersistableBusinessObject ) {
166                        // Legacy KNS Support - since they don't use DataObjectBase
167                        ((PersistableBusinessObject) maintainable.getDataObject()).setObjectId(null);
168                        ((PersistableBusinessObject) maintainable.getDataObject()).setVersionNumber(null);
169                    } else {
170                        // If neither then use reflection to see if the object has setVersionNumber and setObjectId methods
171                       if(ObjectPropertyUtils.getWriteMethod(maintainable.getDataObject().getClass(), "versionNumber") != null) {
172                            ObjectPropertyUtils.setPropertyValue(maintainable.getDataObject(), "versionNumber", null);
173                       }
174    
175                       if(ObjectPropertyUtils.getWriteMethod(maintainable.getDataObject().getClass(), "objectId") != null) {
176                            ObjectPropertyUtils.setPropertyValue(maintainable.getDataObject(), "objectId", null);
177                       }
178                    }
179    
180                    if (!getDocumentDictionaryService().getPreserveLockingKeysOnCopy(maintainable.getDataObjectClass())) {
181                        clearPrimaryKeyFields(newDataObject, maintainable.getDataObjectClass());
182                    }
183                }
184    
185                checkMaintenanceActionAuthorization(document, oldDataObject, maintenanceAction, requestParameters);
186            }
187    
188            // if new with existing we need to populate with passed in parameters
189            if (KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(maintenanceAction)) {
190                Object newBO = document.getNewMaintainableObject().getDataObject();
191                Map<String, String> parameters =
192                        buildKeyMapFromRequest(requestParameters, document.getNewMaintainableObject().getDataObjectClass());
193                ObjectPropertyUtils.copyPropertiesToObject(parameters, newBO);
194                if (newBO instanceof PersistableBusinessObject) {
195                    ((PersistableBusinessObject) newBO).refresh();
196                }
197    
198                document.getNewMaintainableObject().setupNewFromExisting(document, requestParameters);
199            } else if (KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction)) {
200                document.getNewMaintainableObject().processAfterNew(document, requestParameters);
201            }
202        }
203    
204        /**
205         * For the edit and delete maintenance actions checks with the
206         * <code>BusinessObjectAuthorizationService</code> to check whether the
207         * action is allowed for the record data. In action is allowed invokes the
208         * custom processing hook on the <code>Maintainble</code>.
209         *
210         * @param document - document instance for the maintenance object
211         * @param oldBusinessObject - the old maintenance record
212         * @param maintenanceAction - type of maintenance action requested
213         * @param requestParameters - map of parameters from the request
214         */
215        protected void checkMaintenanceActionAuthorization(MaintenanceDocument document, Object oldBusinessObject,
216                String maintenanceAction, Map<String, String[]> requestParameters) {
217            if (KRADConstants.MAINTENANCE_EDIT_ACTION.equals(maintenanceAction)) {
218                boolean allowsEdit = getDataObjectAuthorizationService()
219                        .canMaintain(oldBusinessObject, GlobalVariables.getUserSession().getPerson(),
220                                document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
221                if (!allowsEdit) {
222                    LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() +
223                            " does not allow edit actions.");
224                    throw new DocumentTypeAuthorizationException(
225                            GlobalVariables.getUserSession().getPerson().getPrincipalId(), "edit",
226                            document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
227                }
228    
229                // invoke custom processing method
230                document.getNewMaintainableObject().processAfterEdit(document, requestParameters);
231            } else if (KRADConstants.MAINTENANCE_DELETE_ACTION.equals(maintenanceAction)) {
232                boolean allowsDelete = getDataObjectAuthorizationService()
233                        .canMaintain(oldBusinessObject, GlobalVariables.getUserSession().getPerson(),
234                                document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
235    
236                if (!allowsDelete) {
237                    LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() +
238                            " does not allow delete actions.");
239                    throw new DocumentTypeAuthorizationException(
240                            GlobalVariables.getUserSession().getPerson().getPrincipalId(), "delete",
241                            document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
242                }
243    
244                boolean dataObjectAllowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion(
245                        document.getOldMaintainableObject().getDataObject().getClass());
246    
247                if (!dataObjectAllowsDelete) {
248                    LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() +
249                            " does not allow delete actions.");
250                    GlobalVariables.getMessageMap().removeAllWarningMessagesForProperty(KRADConstants.GLOBAL_MESSAGES);
251                    GlobalVariables.getMessageMap().putError(KRADConstants.DOCUMENT_ERRORS,
252                            RiceKeyConstants.MESSAGE_DELETE_ACTION_NOT_SUPPORTED);
253    
254                }
255    
256            }
257        }
258    
259        /**
260         * For the edit or copy actions retrieves the record that is to be
261         * maintained
262         *
263         * <p>
264         * Based on the persistence metadata for the maintenance object class
265         * retrieves the primary key values from the given request parameters map
266         * (if the class is persistable). With those key values attempts to find the
267         * record using the <code>LookupService</code>.
268         * </p>
269         *
270         * @param document - document instance for the maintenance object
271         * @param requestParameters - Map of parameters from the request
272         * @return Object the retrieved old object
273         */
274        protected Object retrieveObjectForMaintenance(MaintenanceDocument document,
275                Map<String, String[]> requestParameters) {
276            Map<String, String> keyMap =
277                    buildKeyMapFromRequest(requestParameters, document.getNewMaintainableObject().getDataObjectClass());
278    
279            Object oldDataObject = document.getNewMaintainableObject().retrieveObjectForEditOrCopy(document, keyMap);
280    
281            if (oldDataObject == null && !document.getOldMaintainableObject().isExternalBusinessObject()) {
282                throw new RuntimeException(
283                        "Cannot retrieve old record for maintenance document, incorrect parameters passed on maint url: " +
284                                requestParameters);
285            }
286    
287            if (document.getOldMaintainableObject().isExternalBusinessObject()) {
288                if (oldDataObject == null) {
289                    try {
290                        oldDataObject = document.getOldMaintainableObject().getDataObjectClass().newInstance();
291                    } catch (Exception ex) {
292                        throw new RuntimeException(
293                                "External BO maintainable was null and unable to instantiate for old maintainable object.",
294                                ex);
295                    }
296                }
297    
298                populateMaintenanceObjectWithCopyKeyValues(KRADUtils.translateRequestParameterMap(requestParameters),
299                        oldDataObject, document.getOldMaintainableObject());
300                document.getOldMaintainableObject().prepareExternalBusinessObject((PersistableBusinessObject) oldDataObject);
301                oldDataObject = document.getOldMaintainableObject().getDataObject();
302            }
303    
304            return oldDataObject;
305        }
306    
307        /**
308         * Clears the value of the primary key fields on the maintenance object
309         *
310         * @param maintenanceObject - document to clear the pk fields on
311         * @param dataObjectClass - class to use for retrieving primary key metadata
312         */
313        protected void clearPrimaryKeyFields(Object maintenanceObject, Class<?> dataObjectClass) {
314            List<String> keyFieldNames = legacyDataAdapter.listPrimaryKeyFieldNames(dataObjectClass);
315            for (String keyFieldName : keyFieldNames) {
316                ObjectPropertyUtils.setPropertyValue(maintenanceObject, keyFieldName, null);
317            }
318        }
319    
320        /**
321         * Based on the maintenance object class retrieves the key field names from
322         * the <code>BusinessObjectMetaDataService</code> (or alternatively from the
323         * request parameters), then retrieves any matching key value pairs from the
324         * request parameters
325         *
326         * @param requestParameters - map of parameters from the request
327         * @param dataObjectClass - class to use for checking security parameter restrictions
328         * @return Map<String, String> key value pairs
329         */
330        protected Map<String, String> buildKeyMapFromRequest(Map<String, String[]> requestParameters,
331                Class<?> dataObjectClass) {
332            List<String> keyFieldNames = null;
333    
334            // translate request parameters
335            Map<String, String> parameters = KRADUtils.translateRequestParameterMap(requestParameters);
336    
337            // are override keys listed in the request? If so, then those need to be
338            // our keys, not the primary key fields for the BO
339            if (!StringUtils.isBlank(parameters.get(KRADConstants.OVERRIDE_KEYS))) {
340                String[] overrideKeys =
341                        parameters.get(KRADConstants.OVERRIDE_KEYS).split(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
342                keyFieldNames = Arrays.asList(overrideKeys);
343            } else {
344                keyFieldNames = legacyDataAdapter.listPrimaryKeyFieldNames(dataObjectClass);
345            }
346    
347            return KRADUtils.getParametersFromRequest(keyFieldNames, dataObjectClass, parameters);
348        }
349    
350        /**
351         * Looks for a special request parameters giving the names of the keys that
352         * should be retrieved from the request parameters and copied to the
353         * maintenance object
354         *
355         * @param parameters - map of parameters from the request
356         * @param oldBusinessObject - the old maintenance object
357         * @param oldMaintainableObject - the old maintainble object (used to get object class for
358         * security checks)
359         */
360        protected void populateMaintenanceObjectWithCopyKeyValues(Map<String, String> parameters, Object oldBusinessObject,
361                Maintainable oldMaintainableObject) {
362            List<String> keyFieldNamesToCopy = null;
363            Map<String, String> parametersToCopy = null;
364    
365            if (!StringUtils.isBlank(parameters.get(KRADConstants.COPY_KEYS))) {
366                String[] copyKeys =
367                        parameters.get(KRADConstants.COPY_KEYS).split(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
368                keyFieldNamesToCopy = Arrays.asList(copyKeys);
369                parametersToCopy = KRADUtils
370                        .getParametersFromRequest(keyFieldNamesToCopy, oldMaintainableObject.getDataObjectClass(),
371                                parameters);
372            }
373    
374            if (parametersToCopy != null) {
375                // TODO: make sure we are doing formatting here eventually
376                ObjectPropertyUtils.copyPropertiesToObject(parametersToCopy, oldBusinessObject);
377            }
378        }
379    
380        /**
381         * @see org.kuali.rice.krad.service.MaintenanceDocumentService#getLockingDocumentId(org.kuali.rice.krad.maintenance.MaintenanceDocument)
382         */
383        @Override
384            public String getLockingDocumentId(MaintenanceDocument document) {
385            return getLockingDocumentId(document.getNewMaintainableObject(), document.getDocumentNumber());
386        }
387    
388        /**
389         * @see org.kuali.rice.krad.service.MaintenanceDocumentService#getLockingDocumentId(org.kuali.rice.krad.maintenance.Maintainable,
390         *      java.lang.String)
391         */
392        @Override
393            public String getLockingDocumentId(Maintainable maintainable, final String documentNumber) {
394            final List<MaintenanceLock> maintenanceLocks = maintainable.generateMaintenanceLocks();
395            String lockingDocId = null;
396            for (MaintenanceLock maintenanceLock : maintenanceLocks) {
397                lockingDocId = getLockingDocumentNumber(maintenanceLock.getLockingRepresentation(),
398                        documentNumber);
399                if (StringUtils.isNotBlank(lockingDocId)) {
400                    break;
401                }
402            }
403    
404            return lockingDocId;
405        }
406    
407        protected String getLockingDocumentNumber(String lockingRepresentation, String documentNumber) {
408            String lockingDocNumber = "";
409    
410            // build the query criteria
411            List<Predicate> predicates = new ArrayList<Predicate>();
412            predicates.add(PredicateFactory.equal("lockingRepresentation", lockingRepresentation));
413    
414            // if a docHeaderId is specified, then it will be excluded from the
415            // locking representation test.
416            if (StringUtils.isNotBlank(documentNumber)) {
417                predicates.add(PredicateFactory.notEqual(KRADPropertyConstants.DOCUMENT_NUMBER, documentNumber));
418            }
419    
420            QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create();
421            qbc.setPredicates(PredicateFactory.and(predicates.toArray(new Predicate[predicates.size()])));
422    
423            // attempt to retrieve a document based off this criteria
424            List<MaintenanceLock> results = KradDataServiceLocator.getDataObjectService().findMatching(MaintenanceLock.class, qbc.build())
425                    .getResults();
426            if (results.size() > 1) {
427                throw new IllegalStateException(
428                        "Expected single result querying for MaintenanceLock. LockRep: " + lockingRepresentation);
429            }
430    
431            // if a document was found, then there's already one out there pending,
432            // and we consider it 'locked' and we return the docnumber.
433            if (!results.isEmpty()) {
434                lockingDocNumber = results.get(0).getDocumentNumber();
435            }
436            return lockingDocNumber;
437        }
438    
439        /**
440         * @see org.kuali.rice.krad.service.MaintenanceDocumentService#deleteLocks(String)
441         */
442        @Override
443            public void deleteLocks(String documentNumber) {
444            dataObjectService.deleteMatching(MaintenanceLock.class, QueryByCriteria.Builder.forAttribute(
445                    "documentNumber", documentNumber).build());
446        }
447    
448        /**
449         * @see org.kuali.rice.krad.service.MaintenanceDocumentService#storeLocks(java.util.List)
450         */
451        @Override
452            public void storeLocks(List<MaintenanceLock> maintenanceLocks) {
453            if (maintenanceLocks == null) {
454                return;
455            }
456            for (MaintenanceLock maintenanceLock : maintenanceLocks) {
457                dataObjectService.save(maintenanceLock);
458            }
459        }
460    
461        protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
462            return dataObjectAuthorizationService;
463        }
464    
465        @Required
466        public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
467            this.dataObjectAuthorizationService = dataObjectAuthorizationService;
468        }
469    
470        protected DocumentService getDocumentService() {
471            return this.documentService;
472        }
473    
474        @Required
475        public void setDocumentService(DocumentService documentService) {
476            this.documentService = documentService;
477        }
478    
479        public DocumentDictionaryService getDocumentDictionaryService() {
480            return documentDictionaryService;
481        }
482    
483        @Required
484        public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
485            this.documentDictionaryService = documentDictionaryService;
486        }
487    
488        @Required
489        public void setDataObjectService(DataObjectService dataObjectService) {
490                    this.dataObjectService = dataObjectService;
491            }
492    
493            @Required
494        public void setLegacyDataAdapter(LegacyDataAdapter legacyDataAdapter) {
495            this.legacyDataAdapter = legacyDataAdapter;
496        }
497    
498    }