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