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 */ 016package org.kuali.rice.kew.actions; 017 018import static org.junit.Assert.assertEquals; 019import static org.junit.Assert.assertFalse; 020import static org.junit.Assert.assertNotNull; 021import static org.junit.Assert.assertTrue; 022import static org.junit.Assert.fail; 023 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import junit.framework.Assert; 030 031import org.apache.commons.lang.ArrayUtils; 032import org.junit.Test; 033import org.kuali.rice.core.api.util.RiceKeyConstants; 034import org.kuali.rice.coreservice.api.parameter.Parameter; 035import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 036import org.kuali.rice.kew.actionitem.ActionItem; 037import org.kuali.rice.kew.api.KewApiConstants; 038import org.kuali.rice.kew.api.KewApiServiceLocator; 039import org.kuali.rice.kew.api.WorkflowDocument; 040import org.kuali.rice.kew.api.WorkflowDocumentFactory; 041import org.kuali.rice.kew.api.action.ActionRequest; 042import org.kuali.rice.kew.api.action.ActionRequestType; 043import org.kuali.rice.kew.api.action.ActionType; 044import org.kuali.rice.kew.api.action.InvalidActionTakenException; 045import org.kuali.rice.kew.doctype.bo.DocumentType; 046import org.kuali.rice.kew.doctype.service.impl.KimDocumentTypeAuthorizer; 047import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 048import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 049import org.kuali.rice.kew.postprocessor.DefaultPostProcessor; 050import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 051import org.kuali.rice.kew.service.KEWServiceLocator; 052import org.kuali.rice.kew.test.KEWTestCase; 053import org.kuali.rice.kim.api.KimConstants; 054import org.kuali.rice.kim.api.common.attribute.KimAttribute; 055import org.kuali.rice.kim.api.common.template.Template; 056import org.kuali.rice.kim.api.permission.Permission; 057import org.kuali.rice.kim.api.role.Role; 058import org.kuali.rice.kim.api.services.KimApiServiceLocator; 059import org.kuali.rice.kim.api.type.KimType; 060import org.kuali.rice.kim.api.type.KimTypeAttribute; 061import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo; 062import org.kuali.rice.kim.impl.type.KimTypeAttributeBo; 063import org.kuali.rice.krad.data.KradDataServiceLocator; 064import org.kuali.rice.krad.util.ErrorMessage; 065import org.kuali.rice.krad.util.GlobalVariables; 066import org.kuali.rice.kim.impl.type.KimTypeBo; 067import org.kuali.rice.krad.service.KRADServiceLocator; 068import org.kuali.rice.krad.util.KRADConstants; 069import org.kuali.rice.test.BaselineTestCase; 070 071import java.util.Collection; 072import java.util.Collections; 073import java.util.HashMap; 074import java.util.List; 075import java.util.Map; 076 077import static org.junit.Assert.*; 078 079public class RecallActionTest extends KEWTestCase { 080 /** 081 * test postprocessor for testing afterActionTaken hook 082 */ 083 public static class RecallTestPostProcessor extends DefaultPostProcessor { 084 public static ActionType afterActionTakenType; 085 public static ActionTakenEvent afterActionTakenEvent; 086 @Override 087 public ProcessDocReport afterActionTaken(ActionType performed, ActionTakenEvent event) throws Exception { 088 afterActionTakenType = performed; 089 afterActionTakenEvent = event; 090 return super.afterActionTaken(performed, event); 091 } 092 } 093 094 public static class RecallTestDocumentTypeAuthorizer extends KimDocumentTypeAuthorizer { 095 public static String CUSTOM_RECALL_KIM_TYPE_NAME = "Dynamic Type"; 096 public static String CUSTOM_RECALL_QUALIFIER_NAME = "Dynamic Qualifier"; 097 public static String CUSTOM_RECALL_QUALIFIER_VALUE = "Dynamic Qualifier Value"; 098 // we have to use a detail already defined for the recall permission - app doc status seems the most application-controlled 099 public static String CUSTOM_RECALL_DETAIL_NAME = KimConstants.AttributeConstants.APP_DOC_STATUS; 100 public static String CUSTOM_RECALL_DETAIL_VALUE = "Dynamic Recall Permission Detail Value"; 101 102 public static boolean buildPermissionDetailsInvoked = false; 103 public static boolean buildRoleQualifiersInvoked= false; 104 105 @Override 106 protected Map<String, String> buildDocumentTypePermissionDetails(DocumentType documentType, String documentStatus, String actionRequestedCode, String routeNodeName) { 107 buildPermissionDetailsInvoked = true; 108 Map<String, String> details = super.buildDocumentTypePermissionDetails(documentType, documentStatus, actionRequestedCode, routeNodeName); 109 details.put(CUSTOM_RECALL_DETAIL_NAME, CUSTOM_RECALL_DETAIL_VALUE); 110 return details; 111 } 112 113 @Override 114 protected Map<String, String> buildDocumentRoleQualifiers(DocumentRouteHeaderValue document, String routeNodeName) { 115 buildRoleQualifiersInvoked = true; 116 Map<String, String> qualifiers = super.buildDocumentRoleQualifiers(document, routeNodeName); 117 qualifiers.put(CUSTOM_RECALL_QUALIFIER_NAME, CUSTOM_RECALL_QUALIFIER_VALUE); 118 return qualifiers; 119 } 120 } 121 122 private static final String RECALL_TEST_DOC = "RecallTest"; 123 private static final String RECALL_TEST_RESTRICTED_DOC = "RecallTestRestricted"; 124 private static final String RECALL_TEST_NOROUTING_DOC = "RecallTestNoRouting"; 125 private static final String RECALL_TEST_ONLYADHOC_DOC = "RecallTestOnlyAdhoc"; 126 private static final String RECALL_NOTIFY_TEST_DOC = "RecallWithPrevNotifyTest"; 127 private static final String RECALL_NO_PENDING_NOTIFY_TEST_DOC = "RecallWithoutPendingNotifyTest"; 128 private static final String RECALL_NOTIFY_THIRDPARTY_TEST_DOC = "RecallWithThirdPartyNotifyTest"; 129 130 private String EWESTFAL = null; 131 private String JHOPF = null; 132 private String RKIRKEND = null; 133 private String NATJOHNS = null; 134 private String BMCGOUGH = null; 135 136 @Override 137 protected void loadTestData() throws Exception { 138 loadXmlFile("ActionsConfig.xml"); 139 } 140 141 @Override 142 protected void setUpAfterDataLoad() throws Exception { 143 super.setUpAfterDataLoad(); 144 EWESTFAL = getPrincipalIdForName("ewestfal"); 145 JHOPF = getPrincipalIdForName("jhopf"); 146 RKIRKEND = getPrincipalIdForName("rkirkend"); 147 NATJOHNS = getPrincipalIdForName("natjohns"); 148 BMCGOUGH = getPrincipalIdForName("bmcgough"); 149 150 RecallTestPostProcessor.afterActionTakenType = null; 151 RecallTestPostProcessor.afterActionTakenEvent = null; 152 } 153 154 protected void assertAfterActionTakenCalled(ActionType performed, ActionType taken) { 155 assertEquals(performed, RecallTestPostProcessor.afterActionTakenType); 156 assertNotNull(RecallTestPostProcessor.afterActionTakenEvent); 157 assertEquals(taken, RecallTestPostProcessor.afterActionTakenEvent.getActionTaken().getActionTaken()); 158 } 159 160 @Test(expected=InvalidActionTakenException.class) public void testCantRecallUnroutedDoc() { 161 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 162 document.recall("recalling", true); 163 } 164 165 @Test public void testRecallAsInitiatorBeforeAnyApprovals() throws Exception { 166 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 167 document.route(""); 168 169 document.recall("recalling", true); 170 171 assertTrue("Document should be recalled", document.isRecalled()); 172 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL); 173 174 //verify that the document is truly dead - no more action requests or action items. 175 176 List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId()); 177 assertEquals("Should not have any active requests", 0, requests.size()); 178 179 Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId()); 180 assertEquals("Should not have any action items", 0, actionItems.size()); 181 } 182 183 @Test public void testRecallValidActionsTaken() throws Exception { 184 // just complete 185 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 186 document.route("routing"); 187 document.recall("recalling", true); 188 189 // save and complete 190 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 191 document.saveDocument("saving"); 192 document.route("routing"); 193 document.recall("recalling", true); 194 } 195 196 @Test 197 public void testRecallInvalidActionsTaken() throws Exception { 198 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 199 document.route(""); 200 201 document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()); 202 document.approve(""); 203 204 try { 205 document.recall("recalling", true); 206 fail("Recall should NOT have succeeded. Expected InvalidActionTakenException due to invalid 'APROVE' prior action taken."); 207 } catch (InvalidActionTakenException iate) { 208 assertTrue(iate.getMessage().contains("Invalid prior action taken: 'APPROVE'")); 209 } 210 } 211 212 @Test public void testRecallOnlyAdhocRouting() throws Exception { 213 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_ONLYADHOC_DOC); 214 // adhoc it to someone to prevent doc from going final - final is itself an invalid state for recall 215 document.adHocToPrincipal(ActionRequestType.APPROVE, "adhoc approve to JHOPF", JHOPF, "adhocing to prevent finalization", true); 216 document.route("routing"); 217 try { 218 document.recall("recalling", true); 219 fail("Recall should NOT have succeeded. Expected InvalidActionTakenException due to absence of non-adhoc route nodes."); 220 } catch (InvalidActionTakenException iate) { 221 assertTrue(iate.getMessage().contains("No non-adhoc route nodes defined")); 222 } 223 } 224 225 @Test public void testRecallAsInitiatorAfterSingleApproval() throws Exception { 226 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 227 document.route(""); 228 229 document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()); 230 document.approve(""); 231 232 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 233 document.recall("recalling", true); 234 235 assertTrue("Document should be recalled", document.isRecalled()); 236 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL); 237 238 //verify that the document is truly dead - no more action requests or action items. 239 240 List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId()); 241 assertEquals("Should not have any active requests", 0, requests.size()); 242 243 Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId()); 244 assertEquals("Should not have any action items", 0, actionItems.size()); 245 246 // can't recall recalled doc 247 assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL)); 248 } 249 250 @Test 251 public void testRecallDoesNotRecallDocumentWhenProcessed() throws Exception { 252 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 253 document.route(""); 254 255 for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) { 256 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 257 document.approve(""); 258 } 259 260 document.refresh(); 261 assertTrue("Document should be processed", document.isProcessed()); 262 assertTrue("Document should be approved", document.isApproved()); 263 assertFalse("Document should not be final", document.isFinal()); 264 265 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 266 document.recall("recalling when processed should not recall the document", true); 267 268 Map<String, List<ErrorMessage>> errorMessages = GlobalVariables.getMessageMap().getErrorMessages(); 269 assertTrue(errorMessages.size() == 1); 270 for (Map.Entry<String, List<ErrorMessage>> errorMessage : errorMessages.entrySet()) { 271 assertTrue(errorMessage.getValue().get(0).getErrorKey().equals(RiceKeyConstants.MESSAGE_RECALL_NOT_SUPPORTED)); 272 } 273 274 // Verify the document status is still PROCESSED 275 assertTrue("Document should be processed", document.isProcessed()); 276 assertTrue("Document should be approved", document.isApproved()); 277 assertFalse("Document should not be final", document.isFinal()); 278 279 GlobalVariables.getMessageMap().clearErrorMessages(); 280 } 281 282 @Test 283 public void testRecallDoesNotRecallDocumentWhenFinal() throws Exception { 284 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 285 document.route(""); 286 287 for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) { 288 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 289 document.approve(""); 290 } 291 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId()); 292 document.acknowledge(""); 293 294 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId()); 295 document.fyi(); 296 297 for (ActionRequest a: document.getRootActionRequests()) { 298 System.err.println(a); 299 if (a.isAcknowledgeRequest() || a.isFyiRequest()) { 300 System.err.println(a.getPrincipalId()); 301 System.err.println(KimApiServiceLocator.getIdentityService().getPrincipal(a.getPrincipalId()).getPrincipalName()); 302 } 303 } 304 305 assertFalse("Document should not be processed", document.isProcessed()); 306 assertTrue("Document should be approved", document.isApproved()); 307 assertTrue("Document should be final", document.isFinal()); 308 309 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 310 document.recall("recalling when final should not recall the document", true); 311 312 Map<String, List<ErrorMessage>> errorMessages = GlobalVariables.getMessageMap().getErrorMessages(); 313 assertTrue(errorMessages.size() == 1); 314 for (Map.Entry<String, List<ErrorMessage>> errorMessage : errorMessages.entrySet()) { 315 assertTrue(errorMessage.getValue().get(0).getErrorKey().equals(RiceKeyConstants.MESSAGE_RECALL_NOT_SUPPORTED)); 316 } 317 318 // Verify the document status is still FINAL 319 assertFalse("Document should not be processed", document.isProcessed()); 320 assertTrue("Document should be approved", document.isApproved()); 321 assertTrue("Document should be final", document.isFinal()); 322 323 GlobalVariables.getMessageMap().clearErrorMessages(); 324 } 325 326 @Test public void testRecallToActionListAsInitiatorBeforeAnyApprovals() throws Exception { 327 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 328 document.route(""); 329 330 document.recall("recalling", false); 331 332 assertTrue("Document should be saved", document.isSaved()); 333 assertEquals(1, document.getCurrentNodeNames().size()); 334 assertTrue(document.getCurrentNodeNames().contains("AdHoc")); 335 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE); 336 337 // initiator has completion request 338 assertTrue(document.isCompletionRequested()); 339 // can't recall saved doc 340 assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL)); 341 342 // first approver has FYI 343 assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested()); 344 345 document.complete("completing"); 346 347 assertTrue("Document should be enroute", document.isEnroute()); 348 349 assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isApprovalRequested()); 350 } 351 352 private static final String PERM_APP_DOC_STATUS = "recallable by admins"; 353 private static final String ROUTE_NODE = "NotifyFirst"; 354 private static final String ROUTE_STATUS = "R"; 355 356 protected Permission createRecallPermission(String docType, String appDocStatus, String routeNode, String routeStatus) { 357 return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus); 358 } 359 360 protected Permission createRouteDocumentPermission(String docType, String appDocStatus, String routeNode, String routeStatus) { 361 return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus); 362 } 363 364 protected Permission createPermissionForTemplate(String template_ns, String template_name, String permission_ns, String permission_name, String docType, String appDocStatus, String routeNode, String routeStatus) { 365 Template permTmpl = KimApiServiceLocator.getPermissionService().findPermTemplateByNamespaceCodeAndName(template_ns, template_name); 366 assertNotNull(permTmpl); 367 Permission.Builder permission = Permission.Builder.create(permission_ns, permission_name); 368 permission.setDescription(permission_name); 369 permission.setTemplate(Template.Builder.create(permTmpl)); 370 Map<String, String> attrs = new HashMap<String, String>(); 371 attrs.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, docType); 372 attrs.put(KimConstants.AttributeConstants.APP_DOC_STATUS, appDocStatus); 373 attrs.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, routeNode); 374 attrs.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, routeStatus); 375 permission.setActive(true); 376 permission.setAttributes(attrs); 377 378 // save the permission and check that's it's wired up correctly 379 Permission perm = KimApiServiceLocator.getPermissionService().createPermission(permission.build()); 380 assertEquals(perm.getTemplate().getId(), permTmpl.getId()); 381 int num = 1; 382 if (appDocStatus != null) { 383 num++; 384 } 385 if (routeNode != null) { 386 num++; 387 } 388 if (routeStatus != null) { 389 num++; 390 } 391 assertEquals(num, perm.getAttributes().size()); 392 assertEquals(docType, perm.getAttributes().get(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME)); 393 assertEquals(appDocStatus, perm.getAttributes().get(KimConstants.AttributeConstants.APP_DOC_STATUS)); 394 assertEquals(routeNode, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_NODE_NAME)); 395 assertEquals(routeStatus, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_STATUS_CODE)); 396 397 return perm; 398 } 399 400 // disable the existing Recall Permission assigned to Initiator Role for test purposes 401 protected void disableInitiatorRecallPermission() { 402 Permission p = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName("KR-WKFLW", "Recall Document"); 403 Permission.Builder pb = Permission.Builder.create(p); 404 pb.setActive(false); 405 KimApiServiceLocator.getPermissionService().updatePermission(pb.build()); 406 } 407 408 // setter for Kim Priority Parameter (used for useKimPermission method call) 409 protected void setKimPriorityOnDocumentTypeParameterValue(String parameterValue) { 410 if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) { 411 Parameter kimPriorityOverDocTypePolicyParameter = CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND); 412 Parameter.Builder b = Parameter.Builder.create(kimPriorityOverDocTypePolicyParameter); 413 b.setValue(parameterValue); 414 CoreFrameworkServiceLocator.getParameterService().updateParameter(b.build()); 415 } 416 } 417 418 protected String getKimPriorityOnDocumentTypeParameterValue() { 419 if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) { 420 return CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND).getValue(); 421 } 422 return null; 423 } 424 425 426 /** 427 * Tests that a new permission can be configured with the Recall Permission template and that matching works correctly 428 * against the new permission 429 */ 430 @Test public void testRecallPermissionMatching() { 431 disableInitiatorRecallPermission(); 432 createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS); 433 434 Map<String, String> details = new HashMap<String, String>(); 435 details.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, RECALL_TEST_DOC); 436 details.put(KimConstants.AttributeConstants.APP_DOC_STATUS, PERM_APP_DOC_STATUS); 437 details.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, ROUTE_NODE); 438 details.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, ROUTE_STATUS); 439 440 // test all single field mismatches 441 for (Map.Entry<String, String> entry: details.entrySet()) { 442 Map<String, String> testDetails = new HashMap<String, String>(details); 443 // change a single detail to a non-matching value 444 testDetails.put(entry.getKey(), entry.getValue() + " BOGUS "); 445 assertFalse("non-matching " + entry.getKey() + " detail should cause template to not match", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, testDetails)); 446 } 447 448 assertTrue("template should match details", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, details)); 449 } 450 451 @Test public void testRecallPermissionTemplate() throws Exception { 452 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 453 document.route(""); 454 455 // nope, technical admins can't recall 456 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 457 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 458 459 // create a recall permission for the RECALL_TEST_DOC doctype 460 Permission perm = createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS); 461 462 // assign the permission to Technical Administrator role 463 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 464 KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId()); 465 466 // our recall permission is assigned to the technical admin role 467 468 // but the doc will not match... 469 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_NOTIFY_TEST_DOC); 470 document.route(PERM_APP_DOC_STATUS); 471 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 472 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 473 474 // .. the app doc status will not match... 475 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 476 document.route(""); 477 // technical admins can't recall since the app doc status is not correct 478 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 479 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 480 481 // ... the node will not match ... 482 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 483 document.route(""); 484 WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); // approve past notifyfirstnode 485 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 486 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 487 488 // ... the doc status will not match (not recallable anyway) ... 489 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 490 document.route(""); 491 document.cancel("cancelled"); 492 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 493 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 494 495 // everything should match 496 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 497 document.setApplicationDocumentStatus(PERM_APP_DOC_STATUS); 498 document.route(""); 499 // now technical admins can recall by virtue of having the recall permission on this doc 500 assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 501 assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 502 } 503 504 @Test public void testRecallToActionListAsInitiatorAfterApprovals() throws Exception { 505 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_TEST_DOC); 506 } 507 508 @Test public void testRecallToActionListAsInitiatorWithNotificationAfterApprovals() throws Exception { 509 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_TEST_DOC); 510 } 511 512 @Test public void testRecallToActionListAsInitiatorWithoutPendingNotificationAfterApprovals() throws Exception { 513 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NO_PENDING_NOTIFY_TEST_DOC); 514 } 515 516 @Test public void testRecallToActionListAsInitiatorWithThirdPartyNotificationAfterApprovals() throws Exception { 517 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_THIRDPARTY_TEST_DOC); 518 } 519 520 /** 521 * Tests that the document is returned to the *recaller*'s action list, not the original initiator 522 * @throws Exception 523 */ 524 @Test public void testRecallToActionListAsThirdParty() throws Exception { 525 Permission perm = createRecallPermission(RECALL_TEST_DOC, null, null, null); 526 // assign the permission to Technical Administrator role 527 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 528 KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId()); 529 // recall as 'admin' user 530 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC); 531 } 532 533 // the three tests below test permutations of recall permission and derived role assignment 534 protected void assignRoutePermissionToTechAdmin() { 535 // assign Route Document permission to the Technical Administrator role 536 Permission routePerm = createRouteDocumentPermission(RECALL_TEST_DOC, null, null, null); 537 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 538 KimApiServiceLocator.getRoleService().assignPermissionToRole(routePerm.getId(), techadmin.getId()); 539 } 540 protected void assignRecallPermissionToDocumentRouters() { 541 // assign Recall permission to the Document Router derived role 542 Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, null, null, null); 543 Role documentRouterDerivedRole = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-WKFLW", "Document Router"); 544 KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), documentRouterDerivedRole.getId()); 545 } 546 /** 547 * Tests that simply assigning the Route Document permission to the Technical Admin role *without* assigning the 548 * Recall permission to the Document Router derived role, is NOT sufficient to enable recall. 549 */ 550 @Test public void testRoutePermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception { 551 assignRoutePermissionToTechAdmin(); 552 // recall as 'admin' (Tech Admin) user 553 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false); 554 } 555 /** 556 * Tests that simply assigning the recall permission to the Document Router derived role *without* assigning the 557 * Route Document permission to the Technical Admin role, is NOT sufficient to enable recall. 558 */ 559 @Test public void testRecallPermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception { 560 assignRecallPermissionToDocumentRouters(); 561 // recall as 'admin' (Tech Admin) user 562 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false); 563 } 564 /** 565 * Tests that we can use the Route Document derived role to assign Recall permission to document routers. 566 */ 567 @Test public void testRecallToActionListAsRouterDerivedRole() throws Exception { 568 // assign both! derived role works its magic 569 assignRoutePermissionToTechAdmin(); 570 assignRecallPermissionToDocumentRouters(); 571 // recall as 'admin' user (Tech Admin) user 572 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC); 573 } 574 575 /** 576 * Creates a new role with recall permission qualified with doc type and custom app doc status 577 * @param ns role namespace 578 * @param name role name 579 * @param recallPerm the pre-created Recall permission 580 * @return the new recall-capable Role 581 */ 582 protected Role createRoleWithRecallPermission(String ns, String name, Permission recallPerm, String kimTypeName, String roleQualifierName) { 583 // create a custom attribute for role qualification 584 KimAttribute.Builder attribute = KimAttribute.Builder.create("org.kuali.rice.kim.bo.impl.KimAttributes", roleQualifierName, "KR-SYS"); 585 attribute.setAttributeLabel(roleQualifierName); 586 attribute.setActive(true); 587 KimAttributeBo customAttribute = KRADServiceLocator.getDataObjectService().save(KimAttributeBo.from(attribute.build())); 588 589 // create a custom kim type for the custom attribute 590 KimType.Builder kimType = KimType.Builder.create(); 591 kimType.setNamespaceCode("KR-SYS"); 592 kimType.setName(kimTypeName); 593 kimType.setActive(true); 594 KimTypeBo customKimType = KRADServiceLocator.getDataObjectService().save(KimTypeBo.from(kimType.build())); 595 596 KimTypeAttribute.Builder kimTypeAttribute = KimTypeAttribute.Builder.create(); 597 kimTypeAttribute.setKimTypeId(customKimType.getId()); 598 kimTypeAttribute.setKimAttribute(KimAttribute.Builder.create(customAttribute)); 599 kimTypeAttribute.setActive(true); 600 KimTypeAttributeBo customKimTypeAttribute = KRADServiceLocator.getDataObjectService().save(KimTypeAttributeBo.from(kimTypeAttribute.build())); 601 602 kimType = KimType.Builder.create(customKimType); 603 kimTypeAttribute = KimTypeAttribute.Builder.create(customKimTypeAttribute); 604 kimType.setAttributeDefinitions(Collections.singletonList(kimTypeAttribute)); 605 customKimType = KRADServiceLocator.getDataObjectService().save(KimTypeBo.from(kimType.build())); 606 607 KRADServiceLocator.getDataObjectService().flush(KimTypeBo.class); 608 609 // create a new role 610 Role.Builder role = Role.Builder.create(); 611 role.setActive(true); 612 role.setDescription("RecallTest custom recall role"); 613 role.setName(ns); 614 role.setNamespaceCode(name); 615 role.setKimTypeId(customKimType.getId()); 616 Role customRole = KimApiServiceLocator.getRoleService().createRole(role.build()); 617 618 // create a custom attribute for role qualification 619 620 Long chartAttributeId = getNextSequenceLongValue("KRIM_ATTR_DEFN_ID_S"); 621 KimAttributeBo chartAttribute = new KimAttributeBo(); 622 chartAttribute.setId("" + chartAttributeId); 623 chartAttribute.setAttributeName(roleQualifierName); 624 chartAttribute.setComponentName("org.kuali.rice.kim.bo.impl.KimAttributes"); 625 chartAttribute.setNamespaceCode("KR-SYS"); 626 chartAttribute.setAttributeLabel(roleQualifierName); 627 chartAttribute.setActive(true); 628 chartAttribute = KradDataServiceLocator.getDataObjectService().save(chartAttribute); 629 630 KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), customRole.getId()); 631 632 List<String> recallCapableRoleIds = KimApiServiceLocator.getPermissionService().getRoleIdsForPermission(recallPerm.getNamespaceCode(), recallPerm.getName()); 633 Assert.assertFalse("No recall-capable roles found", recallCapableRoleIds.isEmpty()); 634 Assert.assertTrue("New role is not associated with Recall permission", recallCapableRoleIds.contains(customRole.getId())); 635 636 return customRole; 637 } 638 639 /** 640 * Assigns user to role with single qualification 641 * @param principalId the principal to assign to role 642 * @param role the role object 643 * @param roleQualifierName the role qualifier name 644 * @param roleQualifierValue the role qualifier value 645 */ 646 protected void assignUserQualifiedRole(String principalId, Role role, String roleQualifierName, String roleQualifierValue) { 647 // assign user to role triggered by dynamic, custom role qualifications 648 Map<String, String> qualifications = new HashMap<String, String>(); 649 qualifications.put(roleQualifierName, roleQualifierValue); 650 KimApiServiceLocator.getRoleService().assignPrincipalToRole(getPrincipalIdForName("arh14"), role.getNamespaceCode(), role.getName(), qualifications); 651 652 Collection<String> ids = KimApiServiceLocator.getRoleService().getRoleMemberPrincipalIds(role.getNamespaceCode(), role.getName(), qualifications); 653 Assert.assertTrue("Qualified role assignment failed", ids.contains(principalId)); 654 } 655 656 /** 657 * Tests that an application can customize document type routing authorization via documenttypeauthorizer 658 */ 659 @Test public void testRecallWithCustomDocumentTypeAuthorizer() throws Exception { 660 // arh14 is not associated with our doc routing, will be authorized by custom documenttypeauthorizer 661 final String ARH14 = getPrincipalIdForName("arh14"); 662 663 // remove existing initiator recall permission 664 disableInitiatorRecallPermission(); 665 666 RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked = false; 667 RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked = false; 668 669 // confirm arh14 can't recall doc 670 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("arh14"), RECALL_TEST_DOC, false); 671 672 final String RECALL_ROLE_NM = "CustomRecall"; 673 final String RECALL_ROLE_NS = "KR-SYS"; 674 675 // assign permission triggered by dynamic, custom permission details 676 Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE, null, null); 677 Role recallRole = createRoleWithRecallPermission(RECALL_ROLE_NM, RECALL_ROLE_NS, recallPerm, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_KIM_TYPE_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME); 678 assignUserQualifiedRole(ARH14, recallRole, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE); 679 680 Map<String, String> d = new HashMap<String, String>(); 681 d.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE); 682 d.put(KewApiConstants.DOCUMENT_TYPE_NAME_DETAIL, RECALL_TEST_DOC); 683 d.put(KewApiConstants.ROUTE_NODE_NAME_DETAIL, ROUTE_NODE); 684 d.put(KewApiConstants.DOCUMENT_STATUS_DETAIL, ROUTE_STATUS); 685 Map<String, String> q = new HashMap<String, String>(); 686 q.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE); 687 // test that arh14 has recall permission via new recall role with proper qualifications 688 List<Permission> permissions = KimApiServiceLocator.getPermissionService().getAuthorizedPermissionsByTemplate(ARH14, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, d, q); 689 Assert.assertEquals(1, permissions.size()); 690 Assert.assertEquals(recallPerm.getId(), permissions.get(0).getId()); 691 692 // verify that arh14 *still* can't recall doc - we have to set the custom documenttypeauthorizer first 693 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 694 695 // now update the doctype with custom documenttype authorizer 696 org.kuali.rice.kew.api.doctype.DocumentType dt = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC); 697 org.kuali.rice.kew.api.doctype.DocumentType.Builder b = org.kuali.rice.kew.api.doctype.DocumentType.Builder.create(dt); 698 b.setAuthorizer(RecallTestDocumentTypeAuthorizer.class.getName()); 699 700 KEWServiceLocator.getDocumentTypeService().save(DocumentType.from(b)); 701 702 Assert.assertEquals(RecallTestDocumentTypeAuthorizer.class.getName(), KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC).getAuthorizer()); 703 704 // custom documenttypeauthorizer has not been invoked yet 705 Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked); 706 Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked); 707 708 // arh14 should *now* be able to recall! 709 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC); 710 711 Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked); 712 Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked); 713 714 // final counter tests - change the actual dynamic values to ensure match fails 715 String orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE; 716 try { 717 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = "I will not match"; 718 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 719 } finally { 720 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = orig; 721 } 722 723 orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE; 724 try { 725 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = "I won't match either"; 726 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 727 } finally { 728 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = orig; 729 } 730 } 731 732 protected void testRecallToActionListAsInitiatorAfterApprovals(String doctype) { 733 testRecallToActionListAfterApprovals(EWESTFAL, EWESTFAL, doctype); 734 } 735 736 // Implements various permutations of recalls - with and without doctype policies/notifications of various sorts 737 // and as initiator or a third party recaller 738 protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype) { 739 testRecallToActionListAfterApprovals(initiator, recaller, doctype, true); 740 } 741 protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype, boolean expect_recall_success) { 742 boolean notifyPreviousRecipients = !RECALL_TEST_DOC.equals(doctype); 743 boolean notifyPendingRecipients = !RECALL_NO_PENDING_NOTIFY_TEST_DOC.equals(doctype); 744 String[] thirdPartiesNotified = RECALL_NOTIFY_THIRDPARTY_TEST_DOC.equals(doctype) ? new String[] { "quickstart", "admin" } : new String[] {}; 745 746 WorkflowDocument document = WorkflowDocumentFactory.createDocument(initiator, doctype); 747 document.route(""); 748 749 WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); 750 WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).approve(""); 751 WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).approve(""); 752 753 document = WorkflowDocumentFactory.loadDocument(recaller, document.getDocumentId()); 754 System.err.println(document.getValidActions().getValidActions()); 755 if (expect_recall_success) { 756 assertTrue("recaller '" + recaller + "' should be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL)); 757 } else { 758 assertFalse("recaller '" + recaller + "' should NOT be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL)); 759 return; 760 } 761 document.recall("recalling", false); 762 763 assertTrue("Document should be saved", document.isSaved()); 764 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE); 765 766 // the recaller has a completion request 767 assertTrue(document.isCompletionRequested()); 768 769 // pending approver has FYI 770 assertEquals(notifyPendingRecipients, WorkflowDocumentFactory.loadDocument(NATJOHNS, document.getDocumentId()).isFYIRequested()); 771 // third approver has FYI 772 assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).isFYIRequested()); 773 // second approver does not have FYI - approver is initiator, FYI is skipped 774 assertFalse(WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).isFYIRequested()); 775 // first approver has FYI 776 assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested()); 777 778 if (!ArrayUtils.isEmpty(thirdPartiesNotified)) { 779 for (String recipient: thirdPartiesNotified) { 780 assertTrue("Expected FYI to be sent to: " + recipient, WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(recipient), document.getDocumentId()).isFYIRequested()); 781 } 782 } 783 784 // omit JHOPF, and see if FYI is subsumed by approval request 785 for (String user: new String[] { RKIRKEND, NATJOHNS }) { 786 WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()).fyi(); 787 } 788 789 document.complete("completing"); 790 791 assertTrue("Document should be enroute", document.isEnroute()); 792 793 // generation of approval requests nullify FYIs (?) 794 // if JHOPF had an FYI, he doesn't any longer 795 for (String user: new String[] { JHOPF, RKIRKEND, NATJOHNS }) { 796 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 797 assertFalse(getPrincipalNameForId(user) + " should not have an FYI", document.isFYIRequested()); 798 } 799 800 // submit all approvals 801 for (String user: new String[] { JHOPF, initiator, RKIRKEND, NATJOHNS, BMCGOUGH }) { 802 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 803 assertTrue(getPrincipalNameForId(user) + " should have approval request", document.isApprovalRequested()); 804 document.approve("approving"); 805 } 806 807 // 2 acks outstanding, we're PROCESSED 808 assertTrue("Document should be processed", document.isProcessed()); 809 assertTrue("Document should be approved", document.isApproved()); 810 811 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId()); 812 document.acknowledge(""); 813 814 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId()); 815 document.fyi(); 816 817 assertTrue("Document should be approved", document.isApproved()); 818 assertTrue("Document should be final", document.isFinal()); 819 } 820}