001/* 002 * (C) Copyright 2009-2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Alexandre Russel 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.platform.routing.core.impl; 021 022import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID; 023import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY; 024import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.MAX_RESULTS_PROPERTY; 025import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.PAGE_SIZE_RESULTS_KEY; 026import static org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants.DOC_ROUTING_SEARCH_ALL_ROUTE_MODELS_PROVIDER_NAME; 027import static org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants.DOC_ROUTING_SEARCH_ROUTE_MODELS_WITH_TITLE_PROVIDER_NAME; 028 029import java.io.IOException; 030import java.io.Serializable; 031import java.net.MalformedURLException; 032import java.net.URL; 033import java.util.ArrayList; 034import java.util.Collections; 035import java.util.HashMap; 036import java.util.List; 037import java.util.Map; 038import java.util.concurrent.TimeUnit; 039import java.util.stream.Collectors; 040 041import org.apache.commons.lang3.StringUtils; 042import org.apache.logging.log4j.LogManager; 043import org.apache.logging.log4j.Logger; 044import org.nuxeo.ecm.core.api.Blob; 045import org.nuxeo.ecm.core.api.CoreInstance; 046import org.nuxeo.ecm.core.api.CoreSession; 047import org.nuxeo.ecm.core.api.DocumentModel; 048import org.nuxeo.ecm.core.api.DocumentModelList; 049import org.nuxeo.ecm.core.api.DocumentNotFoundException; 050import org.nuxeo.ecm.core.api.DocumentRef; 051import org.nuxeo.ecm.core.api.IdRef; 052import org.nuxeo.ecm.core.api.IterableQueryResult; 053import org.nuxeo.ecm.core.api.LifeCycleConstants; 054import org.nuxeo.ecm.core.api.NuxeoException; 055import org.nuxeo.ecm.core.api.NuxeoGroup; 056import org.nuxeo.ecm.core.api.NuxeoPrincipal; 057import org.nuxeo.ecm.core.api.PartialList; 058import org.nuxeo.ecm.core.api.PropertyException; 059import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 060import org.nuxeo.ecm.core.api.impl.blob.URLBlob; 061import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 062import org.nuxeo.ecm.core.api.security.ACE; 063import org.nuxeo.ecm.core.api.security.ACL; 064import org.nuxeo.ecm.core.api.security.ACP; 065import org.nuxeo.ecm.core.api.security.SecurityConstants; 066import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 067import org.nuxeo.ecm.core.event.EventProducer; 068import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 069import org.nuxeo.ecm.core.query.sql.NXQL; 070import org.nuxeo.ecm.core.repository.RepositoryInitializationHandler; 071import org.nuxeo.ecm.platform.actions.ActionContext; 072import org.nuxeo.ecm.platform.actions.ELActionContext; 073import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 074import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext; 075import org.nuxeo.ecm.platform.filemanager.api.FileManager; 076import org.nuxeo.ecm.platform.query.api.PageProvider; 077import org.nuxeo.ecm.platform.query.api.PageProviderService; 078import org.nuxeo.ecm.platform.routing.api.DocumentRoute; 079import org.nuxeo.ecm.platform.routing.api.DocumentRouteElement; 080import org.nuxeo.ecm.platform.routing.api.DocumentRouteTableElement; 081import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants; 082import org.nuxeo.ecm.platform.routing.api.DocumentRoutingPersister; 083import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService; 084import org.nuxeo.ecm.platform.routing.api.LockableDocumentRoute; 085import org.nuxeo.ecm.platform.routing.api.RouteFolderElement; 086import org.nuxeo.ecm.platform.routing.api.RouteModelResourceType; 087import org.nuxeo.ecm.platform.routing.api.RouteTable; 088import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteAlredayLockedException; 089import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException; 090import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteNotLockedException; 091import org.nuxeo.ecm.platform.routing.core.api.DocumentRoutingEngineService; 092import org.nuxeo.ecm.platform.routing.core.audit.RoutingAuditHelper; 093import org.nuxeo.ecm.platform.routing.core.io.NodeAccessRunner; 094import org.nuxeo.ecm.platform.routing.core.listener.RouteModelsInitializator; 095import org.nuxeo.ecm.platform.routing.core.registries.RouteTemplateResourceRegistry; 096import org.nuxeo.ecm.platform.task.Task; 097import org.nuxeo.ecm.platform.task.TaskConstants; 098import org.nuxeo.ecm.platform.task.TaskEventNames; 099import org.nuxeo.ecm.platform.task.TaskService; 100import org.nuxeo.ecm.platform.task.core.helpers.TaskActorsHelper; 101import org.nuxeo.ecm.platform.task.core.service.TaskEventNotificationHelper; 102import org.nuxeo.ecm.platform.usermanager.UserManager; 103import org.nuxeo.runtime.api.Framework; 104import org.nuxeo.runtime.model.ComponentContext; 105import org.nuxeo.runtime.model.ComponentInstance; 106import org.nuxeo.runtime.model.DefaultComponent; 107import org.nuxeo.runtime.model.RuntimeContext; 108 109import com.google.common.cache.Cache; 110import com.google.common.cache.CacheBuilder; 111 112/** 113 * The implementation of the routing service. 114 */ 115public class DocumentRoutingServiceImpl extends DefaultComponent implements DocumentRoutingService { 116 117 private static Logger log = LogManager.getLogger(DocumentRoutingServiceImpl.class); 118 119 /** Routes in any state (model or not). */ 120 private static final String AVAILABLE_ROUTES_QUERY = String.format("SELECT * FROM %s", 121 DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE); 122 123 /** Routes Models. */ 124 private static final String AVAILABLE_ROUTES_MODEL_QUERY = String.format( 125 "SELECT * FROM %s WHERE ecm:currentLifeCycleState = '%s'", 126 DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE, 127 DocumentRoutingConstants.DOCUMENT_ROUTE_MODEL_LIFECYCLESTATE); 128 129 /** Route models that have been validated. */ 130 private static final String ROUTE_MODEL_DOC_ID_WITH_ID_QUERY = String.format("SELECT ecm:uuid FROM %s WHERE" 131 + " ecm:name = %%s AND ecm:currentLifeCycleState = 'validated' AND ecm:isVersion = 0 AND ecm:isProxy = 0 ", 132 DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE); 133 134 private static final String ORDERED_CHILDREN_QUERY = "SELECT * FROM Document WHERE" 135 + " ecm:parentId = '%s' AND ecm:isVersion = 0 AND ecm:isTrashed = 0 ORDER BY ecm:pos"; 136 137 public static final String CHAINS_TO_TYPE_XP = "chainsToType"; 138 139 public static final String PERSISTER_XP = "persister"; 140 141 /** 142 * @since 7.10 143 */ 144 public static final String ACTOR_ACE_CREATOR = "Workflow"; 145 146 // FIXME: use ContributionFragmentRegistry instances instead to handle hot 147 // reload 148 149 public static final String ROUTE_MODELS_IMPORTER_XP = "routeModelImporter"; 150 151 protected Map<String, String> typeToChain = new HashMap<>(); 152 153 protected Map<String, String> undoChainIdFromRunning = new HashMap<>(); 154 155 protected Map<String, String> undoChainIdFromDone = new HashMap<>(); 156 157 protected DocumentRoutingPersister persister; 158 159 protected RouteTemplateResourceRegistry routeResourcesRegistry = new RouteTemplateResourceRegistry(); 160 161 protected RepositoryInitializationHandler repositoryInitializationHandler; 162 163 private Cache<String, String> modelsChache; 164 165 @Override 166 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 167 if (CHAINS_TO_TYPE_XP.equals(extensionPoint)) { 168 ChainToTypeMappingDescriptor desc = (ChainToTypeMappingDescriptor) contribution; 169 typeToChain.put(desc.getDocumentType(), desc.getChainId()); 170 undoChainIdFromRunning.put(desc.getDocumentType(), desc.getUndoChainIdFromRunning()); 171 undoChainIdFromDone.put(desc.getDocumentType(), desc.getUndoChainIdFromDone()); 172 } else if (PERSISTER_XP.equals(extensionPoint)) { 173 PersisterDescriptor des = (PersisterDescriptor) contribution; 174 try { 175 persister = des.getKlass().getDeclaredConstructor().newInstance(); 176 } catch (ReflectiveOperationException e) { 177 throw new NuxeoException(e); 178 } 179 } else if (ROUTE_MODELS_IMPORTER_XP.equals(extensionPoint)) { 180 RouteModelResourceType res = (RouteModelResourceType) contribution; 181 registerRouteResource(res, contributor.getRuntimeContext()); 182 } 183 } 184 185 @Override 186 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 187 if (contribution instanceof RouteModelResourceType) { 188 routeResourcesRegistry.removeContribution((RouteModelResourceType) contribution); 189 } 190 super.unregisterContribution(contribution, extensionPoint, contributor); 191 } 192 193 protected static void fireEvent(String eventName, Map<String, Serializable> eventProperties, DocumentRoute route, 194 CoreSession session) { 195 eventProperties.put(DocumentRoutingConstants.DOCUMENT_ELEMENT_EVENT_CONTEXT_KEY, route); 196 eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, DocumentRoutingConstants.ROUTING_CATEGORY); 197 DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(), 198 route.getDocument()); 199 envContext.setProperties(eventProperties); 200 EventProducer eventProducer = Framework.getService(EventProducer.class); 201 eventProducer.fireEvent(envContext.newEvent(eventName)); 202 } 203 204 @Override 205 public String createNewInstance(final String routeModelId, final List<String> docIds, 206 final Map<String, Serializable> map, CoreSession s, final boolean startInstance) { 207 return CoreInstance.doPrivileged(s, session -> { 208 String routeDocId = getRouteModelDocIdWithId(session, routeModelId); 209 DocumentModel model = session.getDocument(new IdRef(routeDocId)); 210 DocumentModel instance = persister.createDocumentRouteInstanceFromDocumentRouteModel(model, session); 211 DocumentRoute route = instance.getAdapter(DocumentRoute.class); 212 route.setAttachedDocuments(docIds); 213 route.save(session); 214 Map<String, Serializable> props = new HashMap<>(); 215 props.put(DocumentRoutingConstants.INITIATOR_EVENT_CONTEXT_KEY, session.getPrincipal().getActingUser()); 216 fireEvent(DocumentRoutingConstants.Events.beforeRouteReady.name(), props, route, session); 217 route.setReady(session); 218 fireEvent(DocumentRoutingConstants.Events.afterRouteReady.name(), props, route, session); 219 route.save(session); 220 if (startInstance) { 221 fireEvent(DocumentRoutingConstants.Events.beforeRouteStart.name(), new HashMap<>(), route, session); 222 DocumentRoutingEngineService routingEngine = Framework.getService(DocumentRoutingEngineService.class); 223 routingEngine.start(route, map, session); 224 fireEventAfterWorkflowStarted(route, session); 225 } 226 return instance.getId(); 227 }); 228 } 229 230 @Override 231 public String createNewInstance(String routeModelId, List<String> docIds, CoreSession session, 232 boolean startInstance) { 233 return createNewInstance(routeModelId, docIds, null, session, startInstance); 234 } 235 236 @Override 237 public DocumentRoute createNewInstance(DocumentRoute model, List<String> docIds, CoreSession session, 238 boolean startInstance) { 239 String id = createNewInstance(model.getDocument().getName(), docIds, session, startInstance); 240 return session.getDocument(new IdRef(id)).getAdapter(DocumentRoute.class); 241 } 242 243 @Override 244 @Deprecated 245 public DocumentRoute createNewInstance(DocumentRoute model, String documentId, CoreSession session, 246 boolean startInstance) { 247 return createNewInstance(model, Collections.singletonList(documentId), session, startInstance); 248 } 249 250 @Override 251 @Deprecated 252 public DocumentRoute createNewInstance(DocumentRoute model, List<String> documentIds, CoreSession session) { 253 return createNewInstance(model, documentIds, session, true); 254 } 255 256 @Override 257 @Deprecated 258 public DocumentRoute createNewInstance(DocumentRoute model, String documentId, CoreSession session) { 259 return createNewInstance(model, Collections.singletonList(documentId), session, true); 260 } 261 262 @Override 263 public void startInstance(final String routeInstanceId, final List<String> docIds, 264 final Map<String, Serializable> map, CoreSession s) { 265 CoreInstance.doPrivileged(s, session -> { 266 DocumentModel instance = session.getDocument(new IdRef(routeInstanceId)); 267 DocumentRoute route = instance.getAdapter(DocumentRoute.class); 268 if (docIds != null) { 269 route.setAttachedDocuments(docIds); 270 route.save(session); 271 } 272 fireEvent(DocumentRoutingConstants.Events.beforeRouteStart.name(), new HashMap<>(), route, session); 273 DocumentRoutingEngineService routingEngine = Framework.getService(DocumentRoutingEngineService.class); 274 routingEngine.start(route, map, session); 275 fireEventAfterWorkflowStarted(route, session); 276 }); 277 } 278 279 protected void fireEventAfterWorkflowStarted(DocumentRoute route, CoreSession session) { 280 Map<String, Serializable> eventProperties = new HashMap<>(); 281 eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, route.getInitiator()); 282 eventProperties.put("modelId", route.getModelId()); 283 eventProperties.put("modelName", route.getModelName()); 284 if (route instanceof GraphRoute) { 285 eventProperties.put(RoutingAuditHelper.WORKFLOW_VARIABLES, 286 (Serializable) ((GraphRoute) route).getVariables()); 287 } 288 fireEvent(DocumentRoutingConstants.Events.afterWorkflowStarted.name(), eventProperties, route, session); 289 } 290 291 @Override 292 public void resumeInstance(String routeId, String nodeId, Map<String, Object> data, String status, 293 CoreSession session) { 294 AttachedDocumentsChecker adc = new AttachedDocumentsChecker(session, routeId); 295 adc.runUnrestricted(); 296 if (!adc.isWorkflowCanceled) { 297 completeTask(routeId, nodeId, null, data, status, session); 298 } 299 } 300 301 @Override 302 public void completeTask(String routeId, String taskId, Map<String, Object> data, String status, 303 CoreSession session) { 304 DocumentModel task = session.getDocument(new IdRef(taskId)); 305 completeTask(routeId, null, task != null ? task.getAdapter(Task.class) : null, data, status, session); 306 } 307 308 protected void completeTask(final String routeId, final String nodeId, final Task task, 309 final Map<String, Object> data, final String status, CoreSession session) { 310 if (task == null) { 311 log.debug("Resuming workflow instance: {} associated to node: {}", routeId, nodeId); 312 } else { 313 log.debug("Completing task: {} associated to node: {} for workflow instance: {}", task.getId(), nodeId, 314 routeId); 315 } 316 CompleteTaskRunner runner = new CompleteTaskRunner(routeId, nodeId, task, data, status, session); 317 runner.runUnrestricted(); 318 } 319 320 /** 321 * @since 7.4 322 */ 323 private class CompleteTaskRunner extends UnrestrictedSessionRunner { 324 325 String routeId; 326 327 String nodeId; 328 329 Task task; 330 331 Map<String, Object> data; 332 333 String status; 334 335 protected CompleteTaskRunner(final String routeId, final String nodeId, final Task task, 336 final Map<String, Object> data, final String status, CoreSession session) { 337 super(session); 338 this.routeId = routeId; 339 this.nodeId = nodeId; 340 this.task = task; 341 this.data = data; 342 this.status = status; 343 } 344 345 @Override 346 public void run() { 347 DocumentRoutingEngineService routingEngine = Framework.getService(DocumentRoutingEngineService.class); 348 DocumentModel routeDoc = session.getDocument(new IdRef(routeId)); 349 DocumentRoute routeInstance = routeDoc.getAdapter(DocumentRoute.class); 350 routingEngine.resume(routeInstance, nodeId, task != null ? task.getId() : null, data, status, session); 351 352 // If task is null, it means we are resuming the workflow and about to cancel pending tasks. 353 // Do not notify 354 if (task != null) { 355 String comment = data != null ? (String) data.get(GraphNode.NODE_VARIABLE_COMMENT) : null; 356 final Map<String, Serializable> extraEventProperties = new HashMap<>(); 357 extraEventProperties.put(DocumentRoutingConstants.WORKFLOW_TASK_COMPLETION_ACTION_KEY, status); 358 TaskEventNotificationHelper.notifyTaskEnded(session, session.getPrincipal(), task, comment, 359 TaskEventNames.WORKFLOW_TASK_COMPLETED, extraEventProperties); 360 } 361 } 362 363 } 364 365 @Override 366 public List<DocumentRoute> getAvailableDocumentRouteModel(CoreSession session) { 367 DocumentModelList list = session.query(AVAILABLE_ROUTES_MODEL_QUERY); 368 List<DocumentRoute> routes = new ArrayList<>(); 369 for (DocumentModel model : list) { 370 routes.add(model.getAdapter(DocumentRoute.class)); 371 } 372 return routes; 373 } 374 375 @Override 376 public List<DocumentRoute> getAvailableDocumentRoute(CoreSession session) { 377 DocumentModelList list = session.query(AVAILABLE_ROUTES_QUERY); 378 List<DocumentRoute> routes = new ArrayList<>(); 379 for (DocumentModel model : list) { 380 routes.add(model.getAdapter(DocumentRoute.class)); 381 } 382 return routes; 383 } 384 385 @Override 386 public String getOperationChainId(String documentType) { 387 return typeToChain.get(documentType); 388 } 389 390 @Override 391 public String getUndoFromRunningOperationChainId(String documentType) { 392 return undoChainIdFromRunning.get(documentType); 393 } 394 395 @Override 396 public String getUndoFromDoneOperationChainId(String documentType) { 397 return undoChainIdFromDone.get(documentType); 398 } 399 400 @Override 401 public DocumentRoute unlockDocumentRouteUnrestrictedSession(final DocumentRoute routeModel, 402 CoreSession userSession) { 403 CoreInstance.doPrivileged(userSession, session -> { 404 DocumentRoute route = session.getDocument(routeModel.getDocument().getRef()) 405 .getAdapter(DocumentRoute.class); 406 LockableDocumentRoute lockableRoute = route.getDocument().getAdapter(LockableDocumentRoute.class); 407 lockableRoute.unlockDocument(session); 408 }); 409 return userSession.getDocument(routeModel.getDocument().getRef()).getAdapter(DocumentRoute.class); 410 } 411 412 @Override 413 public DocumentRoute validateRouteModel(final DocumentRoute routeModel, CoreSession userSession) 414 throws DocumentRouteNotLockedException { 415 if (!routeModel.getDocument().isLocked()) { 416 throw new DocumentRouteNotLockedException(); 417 } 418 CoreInstance.doPrivileged(userSession, session -> { 419 DocumentRoute route = session.getDocument(routeModel.getDocument().getRef()) 420 .getAdapter(DocumentRoute.class); 421 route.validate(session); 422 }); 423 return userSession.getDocument(routeModel.getDocument().getRef()).getAdapter(DocumentRoute.class); 424 } 425 426 /** 427 * @deprecated since 5.9.2 - Use only routes of type 'graph' 428 */ 429 @Deprecated 430 @Override 431 public List<DocumentRouteTableElement> getRouteElements(DocumentRoute route, CoreSession session) { 432 RouteTable table = new RouteTable(route); 433 List<DocumentRouteTableElement> elements = new ArrayList<>(); 434 processElementsInFolder(route.getDocument(), elements, table, session, 0, null); 435 int maxDepth = 0; 436 for (DocumentRouteTableElement element : elements) { 437 int d = element.getDepth(); 438 maxDepth = d > maxDepth ? d : maxDepth; 439 } 440 table.setMaxDepth(maxDepth); 441 for (DocumentRouteTableElement element : elements) { 442 element.computeFirstChildList(); 443 } 444 return elements; 445 } 446 447 /** 448 * @deprecated since 5.9.2 - Use only routes of type 'graph' 449 */ 450 @Deprecated 451 protected void processElementsInFolder(DocumentModel doc, List<DocumentRouteTableElement> elements, 452 RouteTable table, CoreSession session, int depth, RouteFolderElement folder) { 453 DocumentModelList children = session.getChildren(doc.getRef()); 454 boolean first = true; 455 for (DocumentModel child : children) { 456 if (child.isFolder() && !session.getChildren(child.getRef()).isEmpty()) { 457 RouteFolderElement thisFolder = new RouteFolderElement(child.getAdapter(DocumentRouteElement.class), 458 table, first, folder, depth); 459 processElementsInFolder(child, elements, table, session, depth + 1, thisFolder); 460 } else { 461 if (folder != null) { 462 folder.increaseTotalChildCount(); 463 } else { 464 table.increaseTotalChildCount(); 465 } 466 elements.add(new DocumentRouteTableElement(child.getAdapter(DocumentRouteElement.class), table, depth, 467 folder, first)); 468 } 469 first = false; 470 } 471 } 472 473 @Deprecated 474 protected List<DocumentRouteTableElement> getRouteElements(DocumentRouteElement routeElementDocument, 475 CoreSession session, List<DocumentRouteTableElement> routeElements, int depth) { 476 return null; 477 } 478 479 @Override 480 public List<DocumentRoute> getDocumentRoutesForAttachedDocument(CoreSession session, String attachedDocId) { 481 List<DocumentRouteElement.ElementLifeCycleState> states = new ArrayList<>(); 482 states.add(DocumentRouteElement.ElementLifeCycleState.ready); 483 states.add(DocumentRouteElement.ElementLifeCycleState.running); 484 return getDocumentRoutesForAttachedDocument(session, attachedDocId, states); 485 } 486 487 @Override 488 public List<DocumentRoute> getDocumentRoutesForAttachedDocument(CoreSession session, String attachedDocId, 489 List<DocumentRouteElement.ElementLifeCycleState> states) { 490 DocumentModelList list; 491 StringBuilder statesString = new StringBuilder(); 492 if (states != null && !states.isEmpty()) { 493 statesString.append(" ecm:currentLifeCycleState IN ("); 494 for (DocumentRouteElement.ElementLifeCycleState state : states) { 495 statesString.append("'").append(state.name()).append("',"); 496 } 497 statesString.deleteCharAt(statesString.length() - 1); 498 statesString.append(") AND"); 499 } 500 String query = String.format("SELECT * FROM DocumentRoute WHERE " + statesString.toString() 501 + " docri:participatingDocuments/* = '%s'" 502 // ordering by dc:created makes sure that 503 // a sub-workflow is listed under its parent 504 + " ORDER BY dc:created", attachedDocId); 505 UnrestrictedQueryRunner queryRunner = new UnrestrictedQueryRunner(session, query); 506 list = queryRunner.runQuery(); 507 List<DocumentRoute> routes = new ArrayList<>(); 508 for (DocumentModel model : list) { 509 routes.add(model.getAdapter(DocumentRoute.class)); 510 } 511 return routes; 512 } 513 514 @Override 515 public boolean canUserValidateRoute(NuxeoPrincipal currentUser) { 516 return currentUser.getGroups().contains(DocumentRoutingConstants.ROUTE_MANAGERS_GROUP_NAME); 517 } 518 519 @Override 520 public boolean canValidateRoute(DocumentModel documentRoute, CoreSession coreSession) { 521 if (!coreSession.hasChildren(documentRoute.getRef())) { 522 // Cannot validate an empty route 523 return false; 524 } 525 return coreSession.hasPermission(documentRoute.getRef(), SecurityConstants.EVERYTHING); 526 } 527 528 // @deprecated since 5.9.2 - Use only routes of type 'graph' 529 @Override 530 @Deprecated 531 public void addRouteElementToRoute(DocumentRef parentDocumentRef, int idx, DocumentRouteElement routeElement, 532 CoreSession session) throws DocumentRouteNotLockedException { 533 DocumentRoute route = getParentRouteModel(parentDocumentRef, session); 534 if (!isLockedByCurrentUser(route, session)) { 535 throw new DocumentRouteNotLockedException(); 536 } 537 DocumentModelList children = session.query( 538 String.format(ORDERED_CHILDREN_QUERY, session.getDocument(parentDocumentRef).getId())); 539 DocumentModel sourceDoc; 540 try { 541 sourceDoc = children.get(idx); 542 addRouteElementToRoute(parentDocumentRef, sourceDoc.getName(), routeElement, session); 543 } catch (IndexOutOfBoundsException e) { 544 addRouteElementToRoute(parentDocumentRef, null, routeElement, session); 545 } 546 } 547 548 // @deprecated since 5.9.2 - Use only routes of type 'graph' 549 @Override 550 @Deprecated 551 public void addRouteElementToRoute(DocumentRef parentDocumentRef, String sourceName, 552 DocumentRouteElement routeElement, CoreSession session) throws DocumentRouteNotLockedException { 553 DocumentRoute parentRoute = getParentRouteModel(parentDocumentRef, session); 554 if (!isLockedByCurrentUser(parentRoute, session)) { 555 throw new DocumentRouteNotLockedException(); 556 } 557 PathSegmentService pss = Framework.getService(PathSegmentService.class); 558 DocumentModel docRouteElement = routeElement.getDocument(); 559 DocumentModel parentDocument = session.getDocument(parentDocumentRef); 560 docRouteElement.setPathInfo(parentDocument.getPathAsString(), pss.generatePathSegment(docRouteElement)); 561 String lifecycleState = parentDocument.getCurrentLifeCycleState() 562 .equals(DocumentRouteElement.ElementLifeCycleState.draft.name()) 563 ? DocumentRouteElement.ElementLifeCycleState.draft.name() 564 : DocumentRouteElement.ElementLifeCycleState.ready.name(); 565 docRouteElement.putContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME, lifecycleState); 566 docRouteElement = session.createDocument(docRouteElement); 567 session.orderBefore(parentDocumentRef, docRouteElement.getName(), sourceName); 568 session.save();// the new document will be queried later on 569 } 570 571 @Override 572 public void removeRouteElement(DocumentRouteElement routeElement, CoreSession session) 573 throws DocumentRouteNotLockedException { 574 DocumentRoute parentRoute = routeElement.getDocumentRoute(session); 575 if (!isLockedByCurrentUser(parentRoute, session)) { 576 throw new DocumentRouteNotLockedException(); 577 } 578 session.removeDocument(routeElement.getDocument().getRef()); 579 session.save();// the document will be queried later on 580 } 581 582 @Override 583 public DocumentModelList getOrderedRouteElement(String routeElementId, CoreSession session) { 584 String query = String.format(ORDERED_CHILDREN_QUERY, routeElementId); 585 return session.query(query); 586 } 587 588 @Override 589 public void lockDocumentRoute(DocumentRoute routeModel, CoreSession session) 590 throws DocumentRouteAlredayLockedException { 591 LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class); 592 boolean lockedByCurrent = isLockedByCurrentUser(routeModel, session); 593 if (lockableRoute.isLocked(session) && !lockedByCurrent) { 594 throw new DocumentRouteAlredayLockedException(); 595 } 596 if (!lockedByCurrent) { 597 lockableRoute.lockDocument(session); 598 } 599 } 600 601 @Override 602 public void unlockDocumentRoute(DocumentRoute routeModel, CoreSession session) 603 throws DocumentRouteNotLockedException { 604 LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class); 605 if (!lockableRoute.isLockedByCurrentUser(session)) { 606 throw new DocumentRouteNotLockedException(); 607 } 608 lockableRoute.unlockDocument(session); 609 } 610 611 @Override 612 public boolean isLockedByCurrentUser(DocumentRoute routeModel, CoreSession session) { 613 LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class); 614 return lockableRoute.isLockedByCurrentUser(session); 615 } 616 617 @Override 618 public void updateRouteElement(DocumentRouteElement routeElement, CoreSession session) 619 throws DocumentRouteNotLockedException { 620 if (!isLockedByCurrentUser(routeElement.getDocumentRoute(session), session)) { 621 throw new DocumentRouteNotLockedException(); 622 } 623 routeElement.save(session); 624 } 625 626 private DocumentRoute getParentRouteModel(DocumentRef documentRef, CoreSession session) { 627 DocumentModel parentDoc = session.getDocument(documentRef); 628 if (parentDoc.hasFacet(DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_FACET)) { 629 return parentDoc.getAdapter(DocumentRoute.class); 630 } 631 DocumentRouteElement rElement = parentDoc.getAdapter(DocumentRouteElement.class); 632 return rElement.getDocumentRoute(session); 633 634 } 635 636 @Override 637 public DocumentRoute saveRouteAsNewModel(DocumentRoute instance, CoreSession session) { 638 DocumentModel instanceModel = instance.getDocument(); 639 DocumentModel parent = persister.getParentFolderForNewModel(session, instanceModel); 640 String newName = persister.getNewModelName(instanceModel); 641 DocumentModel newmodel = persister.saveDocumentRouteInstanceAsNewModel(instanceModel, parent, newName, session); 642 DocumentRoute newRoute = newmodel.getAdapter(DocumentRoute.class); 643 if (!newRoute.isDraft()) { 644 newRoute.followTransition(DocumentRouteElement.ElementLifeCycleTransistion.toDraft, session, false); 645 } 646 newRoute.getDocument().setPropertyValue("dc:title", newName); 647 newRoute.setAttachedDocuments(new ArrayList<>()); 648 newRoute.save(session); 649 return newRoute; 650 } 651 652 @Override 653 public boolean isRoutable(DocumentModel doc) { 654 if (doc == null) { 655 return false; 656 } 657 String type = doc.getType(); 658 // TODO make configurable 659 return type.equals("File") || type.equals("Note"); 660 } 661 662 @Override 663 public void importAllRouteModels(CoreSession session) { 664 for (URL url : getRouteModelTemplateResources()) { 665 importRouteModel(url, true, session); 666 } 667 } 668 669 @Override 670 public DocumentRoute importRouteModel(URL modelToImport, boolean overwrite, CoreSession session) { 671 if (modelToImport == null) { 672 throw new NuxeoException(("No resource containing route templates found")); 673 } 674 Blob blob = new URLBlob(modelToImport); 675 final String file = modelToImport.getFile(); 676 DocumentModel doc; 677 try { 678 FileImporterContext context = FileImporterContext.builder(session, blob, 679 persister.getParentFolderForDocumentRouteModels(session).getPathAsString()) 680 .overwrite(true) 681 .fileName(file) 682 .build(); 683 doc = getFileManager().createOrUpdateDocument(context); 684 } catch (IOException e) { 685 throw new NuxeoException(e); 686 } 687 if (doc == null) { 688 throw new NuxeoException("Can not import document " + file); 689 } 690 // remove model from cache if any model with the same id existed 691 if (modelsChache != null) { 692 modelsChache.invalidate(doc.getName()); 693 } 694 695 return doc.getAdapter(DocumentRoute.class); 696 } 697 698 protected FileManager getFileManager() { 699 return Framework.getService(FileManager.class); 700 } 701 702 @Override 703 public void activate(ComponentContext context) { 704 super.activate(context); 705 modelsChache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build(); 706 repositoryInitializationHandler = new RouteModelsInitializator(); 707 repositoryInitializationHandler.install(); 708 } 709 710 @Override 711 public void deactivate(ComponentContext context) { 712 super.deactivate(context); 713 if (repositoryInitializationHandler != null) { 714 repositoryInitializationHandler.uninstall(); 715 } 716 } 717 718 @Override 719 public List<URL> getRouteModelTemplateResources() { 720 // test contrib parsing and deployment 721 return new ArrayList<>(routeResourcesRegistry.getRouteModelTemplateResources()); 722 } 723 724 @SuppressWarnings("unchecked") 725 @Override 726 public List<DocumentModel> searchRouteModels(CoreSession session, String searchString) { 727 PageProviderService pageProviderService = Framework.getService(PageProviderService.class); 728 Map<String, Serializable> props = new HashMap<>(); 729 props.put(MAX_RESULTS_PROPERTY, PAGE_SIZE_RESULTS_KEY); 730 props.put(CORE_SESSION_PROPERTY, (Serializable) session); 731 PageProvider<DocumentModel> pageProvider; 732 if (StringUtils.isEmpty(searchString)) { 733 pageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider( 734 DOC_ROUTING_SEARCH_ALL_ROUTE_MODELS_PROVIDER_NAME, null, null, 0L, props); 735 } else { 736 pageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider( 737 DOC_ROUTING_SEARCH_ROUTE_MODELS_WITH_TITLE_PROVIDER_NAME, null, null, 0L, props, 738 searchString + '%'); 739 } 740 List<DocumentModel> allRouteModels = new ArrayList<>(pageProvider.getCurrentPage()); 741 while (pageProvider.isNextPageAvailable()) { 742 pageProvider.nextPage(); 743 allRouteModels.addAll(pageProvider.getCurrentPage()); 744 } 745 return allRouteModels; 746 } 747 748 @Override 749 public void registerRouteResource(RouteModelResourceType res, RuntimeContext context) { 750 if (res.getPath() != null && res.getId() != null) { 751 if (routeResourcesRegistry.getResource(res.getId()) != null) { 752 routeResourcesRegistry.removeContribution(res); 753 } 754 if (res.getUrl() == null) { 755 res.setUrl(getUrlFromPath(res, context)); 756 } 757 routeResourcesRegistry.addContribution(res); 758 } 759 } 760 761 protected URL getUrlFromPath(RouteModelResourceType res, RuntimeContext extensionContext) { 762 String path = res.getPath(); 763 if (path == null) { 764 return null; 765 } 766 URL url; 767 try { 768 url = new URL(path); 769 } catch (MalformedURLException e) { 770 url = extensionContext.getLocalResource(path); 771 if (url == null) { 772 url = extensionContext.getResource(path); 773 } 774 if (url == null) { 775 url = res.getClass().getResource(path); 776 } 777 } 778 return url; 779 } 780 781 @Override 782 public DocumentRoute getRouteModelWithId(CoreSession session, String id) { 783 String routeDocModelId = getRouteModelDocIdWithId(session, id); 784 DocumentModel routeDoc = session.getDocument(new IdRef(routeDocModelId)); 785 return routeDoc.getAdapter(DocumentRoute.class); 786 } 787 788 @Override 789 public String getRouteModelDocIdWithId(CoreSession session, String id) { 790 if (modelsChache != null) { 791 String routeDocId = modelsChache.getIfPresent(id); 792 if (routeDocId != null) { 793 return routeDocId; 794 } 795 } 796 String query = String.format(ROUTE_MODEL_DOC_ID_WITH_ID_QUERY, NXQL.escapeString(id)); 797 List<String> routeIds = new ArrayList<>(); 798 try (IterableQueryResult results = session.queryAndFetch(query, "NXQL")) { 799 if (results.size() == 0) { 800 throw new NuxeoException("No route found for id: " + id); 801 } 802 if (results.size() != 1) { 803 throw new NuxeoException("More than one route model found with id: " + id); 804 } 805 for (Map<String, Serializable> map : results) { 806 routeIds.add(map.get("ecm:uuid").toString()); 807 } 808 } 809 String routeDocId = routeIds.get(0); 810 if (modelsChache == null) { 811 modelsChache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build(); 812 } 813 modelsChache.put(id, routeDocId); 814 return routeDocId; 815 } 816 817 @Override 818 @Deprecated 819 public void makeRoutingTasks(CoreSession coreSession, final List<Task> tasks) { 820 CoreInstance.doPrivileged(coreSession, session -> { 821 for (Task task : tasks) { 822 DocumentModel taskDoc = task.getDocument(); 823 taskDoc.addFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME); 824 session.saveDocument(taskDoc); 825 } 826 }); 827 } 828 829 @Override 830 public void endTask(CoreSession session, Task task, Map<String, Object> data, String status) { 831 String comment = (String) data.get(GraphNode.NODE_VARIABLE_COMMENT); 832 TaskService taskService = Framework.getService(TaskService.class); 833 taskService.endTask(session, session.getPrincipal(), task, comment, null, false); 834 835 Map<String, String> taskVariables = task.getVariables(); 836 String routeInstanceId = taskVariables.get(DocumentRoutingConstants.TASK_ROUTE_INSTANCE_DOCUMENT_ID_KEY); 837 if (StringUtils.isEmpty(routeInstanceId)) { 838 throw new DocumentRouteException("Can not resume workflow, no related route"); 839 } 840 completeTask(routeInstanceId, null, task, data, status, session); 841 } 842 843 @Override 844 public List<DocumentModel> getWorkflowInputDocuments(CoreSession session, Task task) { 845 String routeInstanceId; 846 try { 847 routeInstanceId = task.getProcessId(); 848 } catch (PropertyException e) { 849 throw new DocumentRouteException("Can not get the related workflow instance"); 850 } 851 if (StringUtils.isEmpty(routeInstanceId)) { 852 throw new DocumentRouteException("Can not get the related workflow instance"); 853 } 854 DocumentModel routeDoc; 855 try { 856 routeDoc = session.getDocument(new IdRef(routeInstanceId)); 857 } catch (DocumentNotFoundException e) { 858 throw new DocumentRouteException("No workflow with the id:" + routeInstanceId); 859 } 860 DocumentRoute route = routeDoc.getAdapter(DocumentRoute.class); 861 return route.getAttachedDocuments(session); 862 } 863 864 @Override 865 public void grantPermissionToTaskAssignees(CoreSession session, String permission, List<DocumentModel> docs, 866 Task task) { 867 setAclForActors(session, getRoutingACLName(task), permission, docs, task.getActors()); 868 } 869 870 @Override 871 public void grantPermissionToTaskDelegatedActors(CoreSession session, String permission, List<DocumentModel> docs, 872 Task task) { 873 setAclForActors(session, getDelegationACLName(task), permission, docs, task.getDelegatedActors()); 874 } 875 876 @Override 877 public void removePermissionFromTaskAssignees(CoreSession s, final List<DocumentModel> docs, Task task) { 878 final String aclName = getRoutingACLName(task); 879 CoreInstance.doPrivileged(s, session -> { 880 for (DocumentModel doc : docs) { 881 ACP acp = doc.getACP(); 882 acp.removeACL(aclName); 883 doc.setACP(acp, true); 884 } 885 }); 886 } 887 888 /** 889 * @since 7.4 890 */ 891 @Override 892 public void removePermissionsForTaskActors(CoreSession s, final List<DocumentModel> docs, String taskId) { 893 final String aclRoutingName = getRoutingACLName(taskId); 894 final String aclDelegationName = getDelegationACLName(taskId); 895 CoreInstance.doPrivileged(s, session -> { 896 for (DocumentModel doc : docs) { 897 ACP acp = doc.getACP(); 898 acp.removeACL(aclRoutingName); 899 acp.removeACL(aclDelegationName); 900 doc.setACP(acp, true); 901 } 902 }); 903 } 904 905 @Override 906 public void removePermissionsForTaskActors(CoreSession session, final List<DocumentModel> docs, Task task) { 907 removePermissionsForTaskActors(session, docs, task.getId()); 908 } 909 910 /** 911 * Finds an ACL name specific to the task (there may be several tasks applying permissions to the same document). 912 */ 913 protected static String getRoutingACLName(Task task) { 914 return getRoutingACLName(task.getId()); 915 } 916 917 /** 918 * @since 7.4 919 */ 920 protected static String getRoutingACLName(String taskId) { 921 return DocumentRoutingConstants.DOCUMENT_ROUTING_ACL + '/' + taskId; 922 } 923 924 protected static String getDelegationACLName(Task task) { 925 return getDelegationACLName(task.getId()); 926 } 927 928 /** 929 * @since 7.4 930 */ 931 protected static String getDelegationACLName(String taskId) { 932 return DocumentRoutingConstants.DOCUMENT_ROUTING_DELEGATION_ACL + '/' + taskId; 933 } 934 935 /** 936 * @since 7.1 937 */ 938 private final class WfCleaner extends UnrestrictedSessionRunner { 939 940 private static final String WORKFLOWS_QUERY = "SELECT ecm:uuid FROM DocumentRoute WHERE ecm:currentLifeCycleState IN ('done', 'canceled')"; 941 942 private static final String TASKS_QUERY = "SELECT ecm:uuid FROM Document WHERE ecm:mixinType = 'Task' AND nt:processId = '%s'"; 943 944 private final int limit; 945 946 private int numberOfCleanedUpWorkflows = 0; 947 948 private WfCleaner(String repositoryName, int limit) { 949 super(repositoryName); 950 this.limit = limit; 951 } 952 953 @Override 954 public void run() { 955 PartialList<Map<String, Serializable>> workflows = session.queryProjection(WORKFLOWS_QUERY, limit, 0); 956 numberOfCleanedUpWorkflows = workflows.size(); 957 958 for (Map<String, Serializable> workflow : workflows) { 959 String routeDocId = workflow.get(ECM_UUID).toString(); 960 final String associatedTaskQuery = String.format(TASKS_QUERY, routeDocId); 961 session.queryProjection(associatedTaskQuery, 0, 0) 962 .stream() 963 .map(task -> new IdRef(task.get(ECM_UUID).toString())) 964 .forEach(session::removeDocument); 965 session.removeDocument(new IdRef(routeDocId)); 966 } 967 } 968 969 public int getNumberOfCleanedUpWf() { 970 return numberOfCleanedUpWorkflows; 971 } 972 } 973 974 class UnrestrictedQueryRunner extends UnrestrictedSessionRunner { 975 976 String query; 977 978 DocumentModelList docs; 979 980 protected UnrestrictedQueryRunner(CoreSession session, String query) { 981 super(session); 982 this.query = query; 983 } 984 985 @Override 986 public void run() { 987 docs = session.query(query); 988 for (DocumentModel documentModel : docs) { 989 documentModel.detach(true); 990 } 991 } 992 993 public DocumentModelList runQuery() { 994 runUnrestricted(); 995 return docs; 996 } 997 } 998 999 /** 1000 * Cancel the workflow instance if all its attached document don't exist anymore. If the workflow is cancelled then 1001 * the isWowkflowCanceled is set to true. 1002 * 1003 * @since 8.4 1004 */ 1005 public static class AttachedDocumentsChecker extends UnrestrictedSessionRunner { 1006 1007 String workflowInstanceId; 1008 1009 boolean isWorkflowCanceled; 1010 1011 protected AttachedDocumentsChecker(CoreSession session, String workflowInstanceId) { 1012 super(session); 1013 this.workflowInstanceId = workflowInstanceId; 1014 } 1015 1016 @Override 1017 public void run() { 1018 DocumentModel routeDoc = session.getDocument(new IdRef(workflowInstanceId)); 1019 DocumentRoute routeInstance = routeDoc.getAdapter(DocumentRoute.class); 1020 List<String> attachedDocumentIds = routeInstance.getAttachedDocuments(); 1021 if (attachedDocumentIds.isEmpty()) { 1022 return; 1023 } 1024 for (String attachedDocumentId : attachedDocumentIds) { 1025 if (session.exists(new IdRef(attachedDocumentId))) { 1026 return; 1027 } 1028 } 1029 DocumentRoutingEngineService routingEngine = Framework.getService(DocumentRoutingEngineService.class); 1030 routingEngine.cancel(routeInstance, session); 1031 isWorkflowCanceled = true; 1032 } 1033 } 1034 1035 @Override 1036 public void finishTask(CoreSession session, DocumentRoute route, Task task, boolean delete) 1037 throws DocumentRouteException { 1038 DocumentModelList docs = route.getAttachedDocuments(session); 1039 try { 1040 removePermissionsForTaskActors(session, docs, task); 1041 // delete task 1042 if (delete) { 1043 session.removeDocument(new IdRef(task.getId())); 1044 } 1045 } catch (DocumentNotFoundException e) { 1046 throw new DocumentRouteException("Cannot finish task", e); 1047 } 1048 } 1049 1050 @Override 1051 public void cancelTask(CoreSession s, final String taskId) throws DocumentRouteException { 1052 CoreInstance.doPrivileged(s, session -> { 1053 DocumentModel taskDoc = session.getDocument(new IdRef(taskId)); 1054 Task task = taskDoc.getAdapter(Task.class); 1055 if (task == null) { 1056 throw new DocumentRouteException("Invalid taskId: " + taskId); 1057 } 1058 1059 if (!task.isOpened()) { 1060 log.info("Can not cancel task: {} as is not open", taskId); 1061 return; 1062 } 1063 task.cancel(session); 1064 1065 // if the task was created by an workflow , update info 1066 String routeId = task.getProcessId(); 1067 if (routeId != null) { 1068 DocumentModel routeDoc = session.getDocument(new IdRef(routeId)); 1069 GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class); 1070 if (routeInstance == null) { 1071 throw new DocumentRouteException("Invalid routeInstanceId: " + routeId); 1072 } 1073 1074 DocumentModelList docs = routeInstance.getAttachedDocumentModels(); 1075 removePermissionsForTaskActors(session, docs, task); 1076 // task is considered processed with the status "null" when is canceled 1077 updateTaskInfo(session, routeInstance, task, null); 1078 } 1079 session.saveDocument(task.getDocument()); 1080 1081 }); 1082 } 1083 1084 protected void updateTaskInfo(CoreSession session, GraphRoute graph, Task task, String status) { 1085 String nodeId = task.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 1086 if (StringUtils.isEmpty(nodeId)) { 1087 throw new DocumentRouteException("No nodeId found on task: " + task.getId()); 1088 } 1089 GraphNode node = graph.getNode(nodeId); 1090 1091 NuxeoPrincipal principal = session.getPrincipal(); 1092 String actor = principal.getActingUser(); 1093 node.updateTaskInfo(task.getId(), true, status, actor, null); 1094 } 1095 1096 @Override 1097 public void reassignTask(CoreSession s, final String taskId, final List<String> actors, final String comment) 1098 throws DocumentRouteException { 1099 CoreInstance.doPrivileged(s, session -> { 1100 DocumentModel taskDoc = session.getDocument(new IdRef(taskId)); 1101 Task task = taskDoc.getAdapter(Task.class); 1102 if (task == null) { 1103 throw new DocumentRouteException("Invalid taskId: " + taskId); 1104 } 1105 if (!task.isOpened()) { 1106 throw new DocumentRouteException("Task " + taskId + " is not opened, can not reassign it"); 1107 } 1108 String routeId = task.getProcessId(); 1109 if (routeId != null) { 1110 DocumentModel routeDoc = session.getDocument(new IdRef(routeId)); 1111 GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class); 1112 if (routeInstance == null) { 1113 throw new DocumentRouteException( 1114 "Invalid routeInstanceId: " + routeId + " referenced by the task " + taskId); 1115 } 1116 GraphNode node = routeInstance.getNode(task.getType()); 1117 if (node == null) { 1118 throw new DocumentRouteException("Invalid node " + routeId + " referenced by the task " + taskId); 1119 } 1120 if (!node.allowTaskReassignment()) { 1121 throw new DocumentRouteException("Task " + taskId + " can not be reassigned. Node " + node.getId() 1122 + " doesn't allow reassignment."); 1123 } 1124 DocumentModelList docs = routeInstance.getAttachedDocumentModels(); 1125 // remove permissions on the document following the 1126 // workflow for the current assignees 1127 removePermissionFromTaskAssignees(session, docs, task); 1128 Framework.getService(TaskService.class) 1129 .reassignTask(session, taskId, actors, comment, getWorkflowContextualInfo(session, task)); 1130 // refresh task 1131 task.getDocument().refresh(); 1132 // grant permission to the new assignees 1133 grantPermissionToTaskAssignees(session, node.getTaskAssigneesPermission(), docs, task); 1134 1135 // Audit task reassignment 1136 Map<String, Serializable> eventProperties = new HashMap<>(); 1137 eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, 1138 DocumentRoutingConstants.ROUTING_CATEGORY); 1139 eventProperties.put("taskName", task.getName()); 1140 eventProperties.put("actors", (Serializable) actors); 1141 eventProperties.put("modelId", routeInstance.getModelId()); 1142 eventProperties.put("modelName", routeInstance.getModelName()); 1143 eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, routeInstance.getInitiator()); 1144 eventProperties.put(RoutingAuditHelper.TASK_ACTOR, session.getPrincipal().getActingUser()); 1145 eventProperties.put("comment", comment); 1146 // compute duration since workflow started 1147 long timeSinceWfStarted = RoutingAuditHelper.computeDurationSinceWfStarted(task.getProcessId()); 1148 if (timeSinceWfStarted >= 0) { 1149 eventProperties.put(RoutingAuditHelper.TIME_SINCE_WF_STARTED, timeSinceWfStarted); 1150 } 1151 // compute duration since task started 1152 long timeSinceTaskStarted = RoutingAuditHelper.computeDurationSinceTaskStarted(task.getId()); 1153 if (timeSinceWfStarted >= 0) { 1154 eventProperties.put(RoutingAuditHelper.TIME_SINCE_TASK_STARTED, timeSinceTaskStarted); 1155 } 1156 DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(), 1157 task.getDocument()); 1158 envContext.setProperties(eventProperties); 1159 EventProducer eventProducer = Framework.getService(EventProducer.class); 1160 eventProducer.fireEvent( 1161 envContext.newEvent(DocumentRoutingConstants.Events.afterWorkflowTaskReassigned.name())); 1162 } 1163 }); 1164 } 1165 1166 protected Map<String, Serializable> getWorkflowContextualInfo(CoreSession session, Task item) { 1167 String workflowInstanceId = item.getProcessId(); 1168 String nodeId = item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 1169 1170 if (session != null && StringUtils.isNotEmpty(workflowInstanceId)) { 1171 NodeAccessRunner nodeAccessRunner = new NodeAccessRunner(session, workflowInstanceId, nodeId); 1172 nodeAccessRunner.runUnrestricted(); 1173 GraphNode node = nodeAccessRunner.getNode(); 1174 return node.getWorkflowContextualInfo(session, true); 1175 } 1176 return Collections.emptyMap(); 1177 } 1178 1179 @Override 1180 public void delegateTask(CoreSession s, final String taskId, final List<String> delegatedActors, 1181 final String comment) throws DocumentRouteException { 1182 CoreInstance.doPrivileged(s, session -> { 1183 DocumentModel taskDoc = session.getDocument(new IdRef(taskId)); 1184 Task task = taskDoc.getAdapter(Task.class); 1185 if (task == null) { 1186 throw new DocumentRouteException("Invalid taskId: " + taskId); 1187 } 1188 String routeId = task.getProcessId(); 1189 if (routeId != null) { 1190 DocumentModel routeDoc = session.getDocument(new IdRef(routeId)); 1191 GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class); 1192 if (routeInstance == null) { 1193 throw new DocumentRouteException( 1194 "Invalid routeInstanceId: " + routeId + " referenced by the task " + taskId); 1195 } 1196 GraphNode node = routeInstance.getNode(task.getType()); 1197 if (node == null) { 1198 throw new DocumentRouteException("Invalid node " + routeId + " referenced by the task " + taskId); 1199 } 1200 DocumentModelList docs = routeInstance.getAttachedDocumentModels(); 1201 Framework.getService(TaskService.class) 1202 .delegateTask(session, taskId, delegatedActors, comment, 1203 getWorkflowContextualInfo(session, task)); 1204 // refresh task 1205 task.getDocument().refresh(); 1206 // grant permission to the new assignees 1207 grantPermissionToTaskDelegatedActors(session, node.getTaskAssigneesPermission(), docs, task); 1208 1209 // Audit task delegation 1210 Map<String, Serializable> eventProperties = new HashMap<>(); 1211 eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, 1212 DocumentRoutingConstants.ROUTING_CATEGORY); 1213 eventProperties.put("taskName", task.getName()); 1214 eventProperties.put("delegatedActors", (Serializable) delegatedActors); 1215 eventProperties.put("modelId", routeInstance.getModelId()); 1216 eventProperties.put("modelName", routeInstance.getModelName()); 1217 eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, routeInstance.getInitiator()); 1218 eventProperties.put(RoutingAuditHelper.TASK_ACTOR, session.getPrincipal().getActingUser()); 1219 eventProperties.put("comment", comment); 1220 1221 // compute duration since workflow started 1222 long timeSinceWfStarted = RoutingAuditHelper.computeDurationSinceWfStarted(task.getProcessId()); 1223 if (timeSinceWfStarted >= 0) { 1224 eventProperties.put(RoutingAuditHelper.TIME_SINCE_WF_STARTED, timeSinceWfStarted); 1225 } 1226 // compute duration since task started 1227 long timeSinceTaskStarted = RoutingAuditHelper.computeDurationSinceTaskStarted(task.getId()); 1228 if (timeSinceWfStarted >= 0) { 1229 eventProperties.put(RoutingAuditHelper.TIME_SINCE_TASK_STARTED, timeSinceTaskStarted); 1230 } 1231 1232 DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(), 1233 task.getDocument()); 1234 envContext.setProperties(eventProperties); 1235 EventProducer eventProducer = Framework.getService(EventProducer.class); 1236 eventProducer.fireEvent( 1237 envContext.newEvent(DocumentRoutingConstants.Events.afterWorkflowTaskDelegated.name())); 1238 } 1239 }); 1240 } 1241 1242 protected void setAclForActors(CoreSession s, final String aclName, final String permission, 1243 final List<DocumentModel> docs, List<String> actors) { 1244 final List<String> actorIds = new ArrayList<>(); 1245 for (String actor : actors) { 1246 if (actor.startsWith(NuxeoPrincipal.PREFIX)) { 1247 actorIds.add(actor.substring(NuxeoPrincipal.PREFIX.length())); 1248 } else if (actor.startsWith(NuxeoGroup.PREFIX)) { 1249 actorIds.add(actor.substring(NuxeoGroup.PREFIX.length())); 1250 } else { 1251 actorIds.add(actor); 1252 } 1253 } 1254 CoreInstance.doPrivileged(s, session -> { 1255 for (DocumentModel doc : docs) { 1256 ACP acp = doc.getACP(); 1257 acp.removeACL(aclName); 1258 ACL acl = new ACLImpl(aclName); 1259 for (String actorId : actorIds) { 1260 acl.add(ACE.builder(actorId, permission).creator(ACTOR_ACE_CREATOR).build()); 1261 } 1262 acp.addACL(0, acl); // add first to get before blocks 1263 doc.setACP(acp, true); 1264 } 1265 }); 1266 } 1267 1268 @Override 1269 public void cleanupDoneAndCanceledRouteInstances(final String reprositoryName, final int limit) { 1270 doCleanupDoneAndCanceledRouteInstances(reprositoryName, limit); 1271 } 1272 1273 @Override 1274 public int doCleanupDoneAndCanceledRouteInstances(final String reprositoryName, final int limit) { 1275 WfCleaner unrestrictedSessionRunner = new WfCleaner(reprositoryName, limit); 1276 unrestrictedSessionRunner.runUnrestricted(); 1277 return unrestrictedSessionRunner.getNumberOfCleanedUpWf(); 1278 } 1279 1280 @Override 1281 public void invalidateRouteModelsCache() { 1282 modelsChache.invalidateAll(); 1283 } 1284 1285 /** 1286 * @since 7.2 1287 * @deprecated since 11.1 this method does not scale as it does not paginate results. Use 1288 * {@link org.nuxeo.ecm.platform.routing.core.provider.RoutingTaskPageProvider#getCurrentPage()} 1289 * instead. 1290 */ 1291 @Override 1292 @Deprecated 1293 public List<Task> getTasks(final DocumentModel document, String actorId, String workflowInstanceId, 1294 final String worflowModelName, CoreSession s) { 1295 StringBuilder query = new StringBuilder( 1296 String.format("SELECT * FROM Document WHERE ecm:mixinType = '%s' AND ecm:currentLifeCycleState = '%s'", 1297 TaskConstants.TASK_FACET_NAME, TaskConstants.TASK_OPENED_LIFE_CYCLE_STATE)); 1298 if (StringUtils.isNotBlank(actorId)) { 1299 List<String> actors = new ArrayList<>(); 1300 UserManager userManager = Framework.getService(UserManager.class); 1301 NuxeoPrincipal principal = userManager.getPrincipal(actorId); 1302 if (principal != null) { 1303 for (String actor : TaskActorsHelper.getTaskActors(principal)) { 1304 actors.add(NXQL.escapeString(actor)); 1305 } 1306 } else { 1307 actors.add(NXQL.escapeString(actorId)); 1308 } 1309 String actorsParam = StringUtils.join(actors, ", "); 1310 query.append(String.format(" AND (nt:actors/* IN (%s) OR nt:delegatedActors/* IN (%s))", actorsParam, 1311 actorsParam)); 1312 } 1313 if (StringUtils.isNotBlank(workflowInstanceId)) { 1314 query.append(String.format(" AND nt:processId = %s", NXQL.escapeString(workflowInstanceId))); 1315 } 1316 if (document != null) { 1317 query.append(String.format(" AND nt:targetDocumentsIds = '%s'", document.getId())); 1318 } 1319 query.append(String.format(" ORDER BY %s ASC", TaskConstants.TASK_DUE_DATE_PROPERTY_NAME)); 1320 final DocumentModelList documentModelList = s.query(query.toString()); 1321 1322 // User does not necessary have READ on the workflow instance 1323 return CoreInstance.doPrivileged(s, session -> { 1324 List<Task> result = new ArrayList<>(); 1325 for (DocumentModel documentModel : documentModelList) { 1326 final Task task = documentModel.getAdapter(Task.class); 1327 if (StringUtils.isNotBlank(worflowModelName)) { 1328 1329 final String processId = task.getProcessId(); 1330 if (processId != null && session.exists(new IdRef(processId))) { 1331 final DocumentRoute routeInstance = session.getDocument(new IdRef(processId)) 1332 .getAdapter(DocumentRoute.class); 1333 if (routeInstance != null) { 1334 final String routeInstanceName = routeInstance.getName(); 1335 if (routeInstanceName != null && (routeInstanceName.equals(worflowModelName) 1336 || routeInstanceName.matches("^(" + worflowModelName + ")\\.\\d+"))) { 1337 result.add(task); 1338 } 1339 } 1340 } 1341 } else { 1342 result.add(task); 1343 } 1344 } 1345 return result; 1346 }); 1347 } 1348 1349 /** 1350 * @since 7.2 1351 */ 1352 @Override 1353 public List<DocumentRoute> getDocumentRelatedWorkflows(DocumentModel document, CoreSession session) { 1354 final String query = String.format( 1355 "SELECT * FROM %s WHERE docri:participatingDocuments/* = '%s' AND ecm:currentLifeCycleState = '%s'", 1356 DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE, document.getId(), 1357 DocumentRouteElement.ElementLifeCycleState.running); 1358 DocumentModelList documentModelList = session.query(query); 1359 List<DocumentRoute> result = new ArrayList<>(); 1360 for (DocumentModel documentModel : documentModelList) { 1361 result.add(documentModel.getAdapter(GraphRoute.class)); 1362 } 1363 return result; 1364 } 1365 1366 /** 1367 * @since 7.2 1368 */ 1369 @Override 1370 public List<DocumentRoute> getRunningWorkflowInstancesLaunchedByCurrentUser(CoreSession session) { 1371 return getRunningWorkflowInstancesLaunchedByCurrentUser(session, null); 1372 } 1373 1374 /** 1375 * @since 7.2 1376 */ 1377 @Override 1378 public List<DocumentRoute> getRunningWorkflowInstancesLaunchedByCurrentUser(CoreSession session, 1379 String worflowModelName) { 1380 final String query = String.format( 1381 "SELECT * FROM %s WHERE docri:initiator = '%s' AND ecm:currentLifeCycleState = '%s'", 1382 DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE, session.getPrincipal().getName(), 1383 DocumentRouteElement.ElementLifeCycleState.running); 1384 DocumentModelList documentModelList = session.query(query); 1385 List<DocumentRoute> result = new ArrayList<>(); 1386 for (DocumentModel documentModel : documentModelList) { 1387 final GraphRoute graphRoute = documentModel.getAdapter(GraphRoute.class); 1388 if (StringUtils.isNotBlank(worflowModelName)) { 1389 final String modelId = graphRoute.getModelId(); 1390 if (StringUtils.isNotBlank(modelId)) { 1391 DocumentRoute model = session.getDocument(new IdRef(modelId)).getAdapter(DocumentRoute.class); 1392 if (worflowModelName.equals(model.getName())) { 1393 result.add(graphRoute); 1394 } 1395 } 1396 } else { 1397 result.add(graphRoute); 1398 } 1399 } 1400 return result; 1401 } 1402 1403 /** 1404 * Returns true id the document route is a model, false if it is just an instance i.e. a running workflow. 1405 * 1406 * @since 7.2 1407 */ 1408 @Override 1409 public boolean isWorkflowModel(final DocumentRoute documentRoute) { 1410 return documentRoute.isValidated(); 1411 } 1412 1413 @Override 1414 public boolean canCreateInstance(CoreSession session, List<String> documentIds, String workflowModelName) { 1415 DocumentRoute routeModel = getRouteModelWithId(session, workflowModelName); 1416 1417 if (routeModel == null) { 1418 throw new IllegalArgumentException(workflowModelName + " is not a valid workflow name"); 1419 } 1420 1421 GraphRoute graphRouteObj = routeModel.getDocument().getAdapter(GraphRoute.class); 1422 if (graphRouteObj == null) { 1423 // old workflow document => ignore 1424 return false; 1425 } 1426 1427 String filter = graphRouteObj.getAvailabilityFilter(); 1428 if (StringUtils.isBlank(filter)) { 1429 return true; 1430 } 1431 1432 ActionManager actionManager = Framework.getService(ActionManager.class); 1433 1434 DocumentModelList docs = session.getDocuments(documentIds.stream().map(IdRef::new).toArray(DocumentRef[]::new)); 1435 1436 return docs.stream().allMatch(doc -> { 1437 ActionContext actionContext = new ELActionContext(); 1438 actionContext.setCurrentDocument(doc); 1439 actionContext.setDocumentManager(session); 1440 actionContext.setCurrentPrincipal(session.getPrincipal()); 1441 return actionManager.checkFilter(filter, actionContext); 1442 }); 1443 } 1444 1445 @Override 1446 public List<DocumentRoute> getRunnableWorkflows(CoreSession session, List<String> documentIds) { 1447 List<DocumentModel> routeModels = searchRouteModels(session, ""); 1448 return routeModels.stream() 1449 .filter(route -> canCreateInstance(session, documentIds, route.getName())) 1450 .map(document -> document.getAdapter(DocumentRoute.class)) 1451 .collect(Collectors.toList()); 1452 } 1453}