View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.actions;
17  
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertFalse;
20  import static org.junit.Assert.assertNotNull;
21  import static org.junit.Assert.assertTrue;
22  import static org.junit.Assert.fail;
23  
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  import junit.framework.Assert;
30  
31  import org.apache.commons.lang.ArrayUtils;
32  import org.junit.Test;
33  import org.kuali.rice.core.api.util.RiceKeyConstants;
34  import org.kuali.rice.coreservice.api.parameter.Parameter;
35  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
36  import org.kuali.rice.kew.actionitem.ActionItem;
37  import org.kuali.rice.kew.api.KewApiConstants;
38  import org.kuali.rice.kew.api.KewApiServiceLocator;
39  import org.kuali.rice.kew.api.WorkflowDocument;
40  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
41  import org.kuali.rice.kew.api.action.ActionRequest;
42  import org.kuali.rice.kew.api.action.ActionRequestType;
43  import org.kuali.rice.kew.api.action.ActionType;
44  import org.kuali.rice.kew.api.action.InvalidActionTakenException;
45  import org.kuali.rice.kew.doctype.bo.DocumentType;
46  import org.kuali.rice.kew.doctype.service.impl.KimDocumentTypeAuthorizer;
47  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
48  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
49  import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
50  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
51  import org.kuali.rice.kew.service.KEWServiceLocator;
52  import org.kuali.rice.kew.test.KEWTestCase;
53  import org.kuali.rice.kim.api.KimConstants;
54  import org.kuali.rice.kim.api.common.attribute.KimAttribute;
55  import org.kuali.rice.kim.api.common.template.Template;
56  import org.kuali.rice.kim.api.permission.Permission;
57  import org.kuali.rice.kim.api.role.Role;
58  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
59  import org.kuali.rice.kim.api.type.KimType;
60  import org.kuali.rice.kim.api.type.KimTypeAttribute;
61  import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo;
62  import org.kuali.rice.kim.impl.type.KimTypeAttributeBo;
63  import org.kuali.rice.krad.data.KradDataServiceLocator;
64  import org.kuali.rice.krad.util.ErrorMessage;
65  import org.kuali.rice.krad.util.GlobalVariables;
66  import org.kuali.rice.kim.impl.type.KimTypeBo;
67  import org.kuali.rice.krad.service.KRADServiceLocator;
68  import org.kuali.rice.krad.util.KRADConstants;
69  import org.kuali.rice.test.BaselineTestCase;
70  
71  import java.util.Collection;
72  import java.util.Collections;
73  import java.util.HashMap;
74  import java.util.List;
75  import java.util.Map;
76  
77  import static org.junit.Assert.*;
78  
79  public class RecallActionTest extends KEWTestCase {
80      /**
81       * test postprocessor for testing afterActionTaken hook
82       */
83      public static class RecallTestPostProcessor extends DefaultPostProcessor {
84          public static ActionType afterActionTakenType;
85          public static ActionTakenEvent afterActionTakenEvent;
86          @Override
87          public ProcessDocReport afterActionTaken(ActionType performed, ActionTakenEvent event) throws Exception {
88              afterActionTakenType = performed;
89              afterActionTakenEvent = event;
90              return super.afterActionTaken(performed, event);
91          }
92      }
93  
94      public static class RecallTestDocumentTypeAuthorizer extends KimDocumentTypeAuthorizer {
95          public static String CUSTOM_RECALL_KIM_TYPE_NAME = "Dynamic Type";
96          public static String CUSTOM_RECALL_QUALIFIER_NAME = "Dynamic Qualifier";
97          public static String CUSTOM_RECALL_QUALIFIER_VALUE = "Dynamic Qualifier Value";
98          // we have to use a detail already defined for the recall permission - app doc status seems the most application-controlled
99          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 }