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 }