001/* 002 * Copyright 2017 Anyware Services 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 */ 016package org.ametys.odf.workflow; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.TreeSet; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.ArrayUtils; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.CmsConstants; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.repository.WorkflowAwareContent; 039import org.ametys.cms.rights.ContentRightAssignmentContext; 040import org.ametys.cms.workflow.ContentWorkflowHelper; 041import org.ametys.core.right.RightManager; 042import org.ametys.core.right.RightManager.RightResult; 043import org.ametys.core.ui.Callable; 044import org.ametys.core.user.CurrentUserProvider; 045import org.ametys.odf.ODFHelper; 046import org.ametys.odf.ProgramItem; 047import org.ametys.odf.cdmfr.CDMFRHandler; 048import org.ametys.odf.course.Course; 049import org.ametys.odf.course.CourseFactory; 050import org.ametys.odf.courselist.CourseListFactory; 051import org.ametys.odf.orgunit.OrgUnit; 052import org.ametys.odf.orgunit.OrgUnitFactory; 053import org.ametys.odf.person.PersonFactory; 054import org.ametys.odf.program.AbstractProgram; 055import org.ametys.odf.program.ContainerFactory; 056import org.ametys.odf.program.ProgramFactory; 057import org.ametys.odf.program.SubProgramFactory; 058import org.ametys.plugins.repository.AmetysObjectResolver; 059import org.ametys.plugins.repository.UnknownAmetysObjectException; 060import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 061import org.ametys.plugins.workflow.AbstractWorkflowComponent; 062import org.ametys.plugins.workflow.AbstractWorkflowComponent.ConditionFailure; 063import org.ametys.plugins.workflow.component.CheckRightsCondition; 064import org.ametys.plugins.workflow.support.WorkflowProvider; 065import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 066import org.ametys.runtime.authentication.AccessDeniedException; 067import org.ametys.runtime.plugin.component.AbstractLogEnabled; 068 069import com.opensymphony.workflow.InvalidActionException; 070import com.opensymphony.workflow.WorkflowException; 071import com.opensymphony.workflow.spi.Step; 072 073/** 074 * Helper for ODF contents on their workflow 075 * 076 */ 077public class ODFWorkflowHelper extends AbstractLogEnabled implements Component, Serviceable 078{ 079 /** The component role. */ 080 public static final String ROLE = ODFWorkflowHelper.class.getName(); 081 082 /** The validate step id */ 083 public static final int VALIDATED_STEP_ID = 3; 084 085 /** The action id of global validation */ 086 public static final int VALIDATE_ACTION_ID = 4; 087 088 /** The action id of global unpublishment */ 089 public static final int UNPUBLISH_ACTION_ID = 10; 090 091 /** Constant for storing the result map into the transient variables map. */ 092 protected static final String CONTENTS_IN_ERROR_KEY = "contentsInError"; 093 094 /** Constant for storing the content with right error into the transient variables map. */ 095 protected static final String CONTENTS_WITH_RIGHT_ERROR_KEY = "contentsWithRightError"; 096 097 /** Constant for storing the result map into the transient variables map. */ 098 protected static final String VALIDATED_CONTENTS_KEY = "validatedContents"; 099 100 /** Constant for storing the unpublish result map into the transient variables map. */ 101 protected static final String UNPUBLISHED_CONTENTS_KEY = "unpublishedContents"; 102 103 /** The Ametys object resolver */ 104 protected AmetysObjectResolver _resolver; 105 /** The workflow provider */ 106 protected WorkflowProvider _workflowProvider; 107 /** The ODF helper */ 108 protected ODFHelper _odfHelper; 109 /** The workflow helper for contents */ 110 protected ContentWorkflowHelper _contentWorkflowHelper; 111 /** The CDM-fr handler */ 112 protected CDMFRHandler _cdmfrHandler; 113 /** The right manager */ 114 protected RightManager _rightManager; 115 /** The user provider */ 116 protected CurrentUserProvider _currentUserProvider; 117 118 private enum WorkflowActionStatus 119 { 120 SUCCESS, 121 RIGHT_ERROR, 122 OTHER_ERROR 123 } 124 125 @Override 126 public void service(ServiceManager manager) throws ServiceException 127 { 128 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 129 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 130 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 131 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 132 _cdmfrHandler = (CDMFRHandler) manager.lookup(CDMFRHandler.ROLE); 133 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 134 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 135 } 136 137 /** 138 * Check if the contents has referenced contents that are not already validated (children excluded) 139 * @param contentIds The id of contents to check 140 * @return A map with success key to true if referenced contents are validated. A map with the invalidated contents otherwise. 141 */ 142 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) // Use by validation actions 143 public Map<String, Object> checkReferences(List<String> contentIds) 144 { 145 Map<String, Object> result = new HashMap<>(); 146 147 List<Map<String, Object>> contentsInError = new ArrayList<>(); 148 149 for (String contentId : contentIds) 150 { 151 Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator()); 152 153 WorkflowAwareContent content = _resolver.resolveById(contentId); 154 if (!_rightManager.currentUserHasReadAccess(content)) 155 { 156 throw new AccessDeniedException("User " + _currentUserProvider.getUser() + " tried to access information about content of id '" + contentId + " without sufficient right"); 157 } 158 159 _checkValidateStep(content, invalidatedContents, false); 160 161 // Remove initial content from invalidated contents 162 invalidatedContents.remove(content); 163 164 if (!invalidatedContents.isEmpty()) 165 { 166 List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream() 167 .map(c -> _content2Json(c)) 168 .collect(Collectors.toList()); 169 170 Map<String, Object> contentInError = new HashMap<>(); 171 contentInError.put("id", content.getId()); 172 contentInError.put("code", ((ProgramItem) content).getDisplayCode()); 173 contentInError.put("title", content.getTitle()); 174 contentInError.put("invalidatedContents", invalidatedContentsAsJson); 175 contentsInError.add(contentInError); 176 } 177 } 178 179 result.put("contentsInError", contentsInError); 180 result.put("success", contentsInError.isEmpty()); 181 return result; 182 } 183 184 /** 185 * Get the global validation status of a content 186 * @param contentId the id of content 187 * @return the result 188 */ 189 @Callable (rights = Callable.READ_ACCESS, paramIndex = 0, rightContext = ContentRightAssignmentContext.ID) 190 public Map<String, Object> getGlobalValidationStatus(String contentId) 191 { 192 Map<String, Object> result = new HashMap<>(); 193 194 WorkflowAwareContent waContent = _resolver.resolveById(contentId); 195 196 // Order invalidated contents by types 197 Set<Content> invalidatedContents = getInvalidatedContents(waContent); 198 199 List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream() 200 .map(c -> _content2Json(c)) 201 .collect(Collectors.toList()); 202 203 result.put("invalidatedContents", invalidatedContentsAsJson); 204 result.put("globalValidated", invalidatedContents.isEmpty()); 205 206 return result; 207 } 208 209 /** 210 * Get the invalidated contents referenced by a ODF content 211 * @param content the initial ODF content 212 * @return the set of referenced invalidated contents 213 */ 214 public Set<Content> getInvalidatedContents(WorkflowAwareContent content) 215 { 216 Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator()); 217 218 _checkValidateStep(content, invalidatedContents, true); 219 220 return invalidatedContents; 221 } 222 223 /** 224 * Determines if a content is already in validated step 225 * @param content The content to test 226 * @return true if the content is already validated 227 */ 228 public boolean isInValidatedStep (WorkflowAwareContent content) 229 { 230 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 231 long workflowId = content.getWorkflowId(); 232 233 List<Step> steps = workflow.getCurrentSteps(workflowId); 234 for (Step step : steps) 235 { 236 if (step.getStepId() == VALIDATED_STEP_ID) 237 { 238 return true; 239 } 240 } 241 242 return false; 243 } 244 245 private void _checkValidateStep (WorkflowAwareContent content, Set<Content> invalidatedContents, boolean checkChildren) 246 { 247 if (!isInValidatedStep(content)) 248 { 249 invalidatedContents.add(content); 250 } 251 252 if (checkChildren && content instanceof ProgramItem) 253 { 254 // Check the structure recursively 255 List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content) 256 .stream() 257 .filter(ProgramItem::isPublishable) 258 .toList(); 259 for (ProgramItem child : children) 260 { 261 WorkflowAwareContent waChild = (WorkflowAwareContent) child; 262 _checkValidateStep(waChild, invalidatedContents, checkChildren); 263 } 264 } 265 266 // Validate others referenced contents 267 if (content instanceof AbstractProgram) 268 { 269 _checkReferencedContents(((AbstractProgram) content).getOrgUnits(), invalidatedContents, checkChildren); 270 _checkReferencedContents(((AbstractProgram) content).getContacts(), invalidatedContents, checkChildren); 271 } 272 else if (content instanceof Course) 273 { 274 _checkReferencedContents(((Course) content).getOrgUnits(), invalidatedContents, checkChildren); 275 _checkReferencedContents(((Course) content).getContacts(), invalidatedContents, checkChildren); 276 } 277 else if (content instanceof OrgUnit) 278 { 279 _checkReferencedContents(((OrgUnit) content).getContacts(), invalidatedContents, checkChildren); 280 } 281 } 282 283 private void _checkReferencedContents(Collection<String> refContentIds, Set<Content> invalidatedContents, boolean recursively) 284 { 285 for (String id : refContentIds) 286 { 287 try 288 { 289 if (StringUtils.isNotEmpty(id)) 290 { 291 WorkflowAwareContent refContent = _resolver.resolveById(id); 292 if (recursively) 293 { 294 _checkValidateStep(refContent, invalidatedContents, recursively); 295 } 296 else if (!isInValidatedStep(refContent)) 297 { 298 invalidatedContents.add(refContent); 299 } 300 } 301 } 302 catch (UnknownAmetysObjectException e) 303 { 304 // Nothing 305 } 306 } 307 } 308 309 /** 310 * Global validation on a contents. 311 * Validate the contents with their whole structure and the others referenced contacts and orgunits. 312 * @param contentIds the id of contents to validation recursively 313 * @return the result for each initial contents 314 */ 315 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 316 public Map<String, Object> globalValidate(List<String> contentIds) 317 { 318 Map<String, Object> result = new HashMap<>(); 319 320 Set<String> allValidatedContentIds = new HashSet<>(); 321 try 322 { 323 _cdmfrHandler.suspendCDMFRObserver(); 324 325 for (String contentId : contentIds) 326 { 327 Map<String, Object> contentResult = new HashMap<>(); 328 329 contentResult.put(CONTENTS_IN_ERROR_KEY, new HashSet<>()); 330 contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, new HashSet<>()); 331 contentResult.put(VALIDATED_CONTENTS_KEY, new HashSet<>()); 332 333 ProgramItem programItem = _resolver.resolveById(contentId); 334 if (programItem.isPublishable()) 335 { 336 _validateRecursively((WorkflowAwareContent) programItem, contentResult); 337 } 338 339 @SuppressWarnings("unchecked") 340 Set<Content> contentsInError = (Set<Content>) contentResult.get(CONTENTS_IN_ERROR_KEY); 341 List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream() 342 .map(c -> _content2Json(c)) 343 .toList(); 344 345 contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson); 346 347 @SuppressWarnings("unchecked") 348 Set<Content> contentsWithRightError = (Set<Content>) contentResult.get(CONTENTS_WITH_RIGHT_ERROR_KEY); 349 List<Map<String, Object>> contentsWithRightErrorAsJson = contentsWithRightError.stream() 350 .map(c -> _content2Json(c)) 351 .toList(); 352 353 contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightErrorAsJson); 354 355 @SuppressWarnings("unchecked") 356 Set<String> validatedContent = (Set<String>) contentResult.get(VALIDATED_CONTENTS_KEY); 357 allValidatedContentIds.addAll(validatedContent); 358 359 result.put(contentId, contentResult); 360 } 361 } 362 finally 363 { 364 _cdmfrHandler.unsuspendCDMFRObserver(allValidatedContentIds); 365 } 366 367 return result; 368 } 369 370 /** 371 * Get the JSON representation of the content 372 * @param content the content 373 * @return the content properties 374 */ 375 protected Map<String, Object> _content2Json(Content content) 376 { 377 Map<String, Object> content2json = new HashMap<>(); 378 content2json.put("title", content.getTitle()); 379 content2json.put("id", content.getId()); 380 381 if (content instanceof ProgramItem) 382 { 383 content2json.put("code", ((ProgramItem) content).getDisplayCode()); 384 } 385 else if (content instanceof OrgUnit) 386 { 387 content2json.put("code", ((OrgUnit) content).getUAICode()); 388 } 389 390 return content2json; 391 } 392 393 /** 394 * Validate the referenced contents recursively 395 * @param content The validated content 396 * @param result the result object to fill during process 397 */ 398 protected void _validateRecursively (WorkflowAwareContent content, Map<String, Object> result) 399 { 400 @SuppressWarnings("unchecked") 401 Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY); 402 @SuppressWarnings("unchecked") 403 Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY); 404 @SuppressWarnings("unchecked") 405 Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY); 406 407 if (!isInValidatedStep(content)) 408 { 409 // Validate content itself 410 WorkflowActionStatus status = _doValidateWorkflowAction (content, VALIDATE_ACTION_ID); 411 if (status == WorkflowActionStatus.RIGHT_ERROR) 412 { 413 contentsWithRightError.add(content); 414 } 415 else if (status == WorkflowActionStatus.OTHER_ERROR) 416 { 417 contentsInError.add(content); 418 } 419 else 420 { 421 validatedContentIds.add(content.getId()); 422 } 423 } 424 425 if (content instanceof ProgramItem) 426 { 427 // Validate the structure recursively 428 List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content) 429 .stream() 430 .filter(ProgramItem::isPublishable) 431 .toList(); 432 for (ProgramItem child : children) 433 { 434 _validateRecursively((WorkflowAwareContent) child, result); 435 } 436 } 437 438 // Validate others referenced contents 439 if (content instanceof AbstractProgram) 440 { 441 _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), result); 442 _validateReferencedContents(((AbstractProgram) content).getContacts(), result); 443 } 444 else if (content instanceof Course) 445 { 446 _validateReferencedContents(((Course) content).getOrgUnits(), result); 447 _validateReferencedContents(((Course) content).getContacts(), result); 448 } 449 else if (content instanceof OrgUnit) 450 { 451 _validateReferencedContents(((OrgUnit) content).getContacts(), result); 452 } 453 } 454 455 /** 456 * Validate the list of referenced contents 457 * @param refContentIds The id of contents to validate 458 * @param result the result object to fill during process 459 */ 460 protected void _validateReferencedContents (Collection<String> refContentIds, Map<String, Object> result) 461 { 462 @SuppressWarnings("unchecked") 463 Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY); 464 @SuppressWarnings("unchecked") 465 Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY); 466 @SuppressWarnings("unchecked") 467 Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY); 468 469 for (String id : refContentIds) 470 { 471 try 472 { 473 if (StringUtils.isNotEmpty(id)) 474 { 475 WorkflowAwareContent content = _resolver.resolveById(id); 476 if (!isInValidatedStep(content)) 477 { 478 WorkflowActionStatus status = _doValidateWorkflowAction (content, VALIDATE_ACTION_ID); 479 if (status == WorkflowActionStatus.RIGHT_ERROR) 480 { 481 contentsWithRightError.add(content); 482 } 483 else if (status == WorkflowActionStatus.OTHER_ERROR) 484 { 485 contentsInError.add(content); 486 } 487 else 488 { 489 validatedContentIds.add(content.getId()); 490 } 491 } 492 } 493 } 494 catch (UnknownAmetysObjectException e) 495 { 496 // Nothing 497 } 498 } 499 } 500 501 /** 502 * Validate a content 503 * @param content The content to validate 504 * @param actionId The id of validate action 505 * @return the validate workflow status 506 */ 507 protected WorkflowActionStatus _doValidateWorkflowAction (WorkflowAwareContent content, int actionId) 508 { 509 Map<String, Object> inputs = new HashMap<>(); 510 try 511 { 512 _contentWorkflowHelper.doAction(content, actionId, inputs, false); 513 return WorkflowActionStatus.SUCCESS; 514 } 515 catch (InvalidActionException | WorkflowException e) 516 { 517 String failureString = _getActionFailuresAsString(inputs); 518 if (e instanceof InvalidActionException) 519 { 520 getLogger().warn("Unable to validate content \"{}\" ({}): mandatory metadata are probably missing or the content is locked{}", content.getTitle(), content.getId(), failureString, e); 521 } 522 else 523 { 524 getLogger().warn("Failed to validate content \"{}\" ({}){}", content.getTitle(), content.getId(), failureString, e); 525 } 526 return _isRightConditionFailure(inputs) ? WorkflowActionStatus.RIGHT_ERROR : WorkflowActionStatus.OTHER_ERROR; 527 } 528 } 529 530 private boolean _isRightConditionFailure(Map<String, Object> actionInputs) 531 { 532 if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY)) 533 { 534 @SuppressWarnings("unchecked") 535 List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.getOrDefault(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>()); 536 return failures.stream() 537 .filter(c -> c.type().equals(CheckRightsCondition.class.getName())) 538 .findFirst() 539 .isPresent(); 540 } 541 542 return false; 543 } 544 545 private String _getActionFailuresAsString(Map<String, Object> actionInputs) 546 { 547 String failuresAsString = ""; 548 if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY)) 549 { 550 @SuppressWarnings("unchecked") 551 List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.getOrDefault(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>()); 552 if (!failures.isEmpty()) 553 { 554 failuresAsString = ", due to the following error(s):\n" + String.join("\n", failures.stream().map(ConditionFailure::text).toList()); 555 } 556 } 557 558 return failuresAsString; 559 } 560 561 /** 562 * Set the publishable state of contents 563 * @param contentIds The id of contents 564 * @param isPublishable <code>true</code> to set content as publishable, <code>false</code> otherwise 565 * @return The result map 566 */ 567 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 568 public Map<String, Object> setPublishableState (List<String> contentIds, boolean isPublishable) 569 { 570 Map<String, Object> result = new HashMap<>(); 571 572 for (String id : contentIds) 573 { 574 Map<String, Object> contentResult = new HashMap<>(); 575 Set<Content> contentsInError = new HashSet<>(); 576 contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInError); 577 Set<Content> contentsWithRightError = new HashSet<>(); 578 contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightError); 579 contentResult.put(UNPUBLISHED_CONTENTS_KEY, new HashSet<>()); 580 581 WorkflowAwareContent content = _resolver.resolveById(id); 582 if (content instanceof ProgramItem programItem) 583 { 584 try 585 { 586 if (_rightManager.currentUserHasRight("Workflow_Rights_Unpublish", programItem) != RightResult.RIGHT_ALLOW) 587 { 588 contentsWithRightError.add((Content) programItem); 589 } 590 else 591 { 592 programItem.setPublishable(isPublishable); 593 content.saveChanges(); 594 595 if (!isPublishable) 596 { 597 _unpublishRecursively(programItem, contentResult); 598 } 599 } 600 } 601 catch (Exception e) 602 { 603 getLogger().error("Unable to set publishable property for content '{}' with id '{}'", content.getTitle(), content.getId(), e); 604 contentsInError.add(content); 605 } 606 607 List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream() 608 .map(c -> _content2Json(c)) 609 .collect(Collectors.toList()); 610 contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson); 611 612 List<Map<String, Object>> contentsWithRightErrorAsJson = contentsWithRightError.stream() 613 .map(c -> _content2Json(c)) 614 .collect(Collectors.toList()); 615 contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightErrorAsJson); 616 617 result.put(id, contentResult); 618 } 619 } 620 621 return result; 622 } 623 624 /** 625 * Unpublish the referenced contents recursively 626 * @param programItem The content to unpublish 627 * @param result the result object to fill during process 628 */ 629 protected void _unpublishRecursively (ProgramItem programItem, Map<String, Object> result) 630 { 631 @SuppressWarnings("unchecked") 632 Set<String> unpublishedContentIds = (Set<String>) result.get(UNPUBLISHED_CONTENTS_KEY); 633 @SuppressWarnings("unchecked") 634 Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY); 635 @SuppressWarnings("unchecked") 636 Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY); 637 638 if (_isPublished(programItem)) 639 { 640 // Unpublish content itself 641 WorkflowActionStatus status = _doUnpublishWorkflowAction((WorkflowAwareContent) programItem, UNPUBLISH_ACTION_ID); 642 if (status == WorkflowActionStatus.RIGHT_ERROR) 643 { 644 contentsWithRightError.add((Content) programItem); 645 } 646 else if (status == WorkflowActionStatus.OTHER_ERROR) 647 { 648 contentsInError.add((Content) programItem); 649 } 650 else 651 { 652 unpublishedContentIds.add(programItem.getId()); 653 } 654 } 655 656 // Unpublish the structure recursively 657 List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem); 658 for (ProgramItem child : children) 659 { 660 boolean hasOtherPublishedParent = _odfHelper.getParentProgramItems(child) 661 .stream() 662 .filter(p -> !p.getId().equals(programItem.getId())) 663 .filter(this::_isPublished) 664 .findFirst() 665 .isPresent(); 666 667 // Don't unpublish the content if it has an other published parent 668 if (!hasOtherPublishedParent) 669 { 670 _unpublishRecursively(child, result); 671 } 672 } 673 } 674 675 /** 676 * Unpublish a content 677 * @param content The content to unpublish 678 * @param actionId The id of unpublish action 679 * @return the workflow action status 680 */ 681 protected WorkflowActionStatus _doUnpublishWorkflowAction (WorkflowAwareContent content, int actionId) 682 { 683 Map<String, Object> inputs = new HashMap<>(); 684 try 685 { 686 _contentWorkflowHelper.doAction(content, actionId, inputs, false); 687 return WorkflowActionStatus.SUCCESS; 688 } 689 catch (Exception e) 690 { 691 String failureString = _getActionFailuresAsString(inputs); 692 getLogger().warn("Failed to unpublish content \"{}\" ({}){}", content.getTitle(), content.getId(), failureString, e); 693 return _isRightConditionFailure(inputs) ? WorkflowActionStatus.RIGHT_ERROR : WorkflowActionStatus.OTHER_ERROR; 694 } 695 } 696 697 /** 698 * <code>true</code> if the parent is publishable 699 * @param content the content 700 * @return <code>true</code> if the parent is publishable 701 */ 702 public boolean isParentPublishable(ProgramItem content) 703 { 704 List<ProgramItem> parents = _odfHelper.getParentProgramItems(content); 705 if (parents.isEmpty()) 706 { 707 return true; 708 } 709 710 return parents.stream() 711 .filter(c -> c.isPublishable() && isParentPublishable(c)) 712 .findAny() 713 .isPresent(); 714 } 715 716 private boolean _isPublished(ProgramItem content) 717 { 718 return content instanceof VersionAwareAmetysObject versionAAO ? ArrayUtils.contains(versionAAO.getAllLabels(), CmsConstants.LIVE_LABEL) : false; 719 } 720 721 class ContentTypeComparator implements Comparator<Content> 722 { 723 String[] _orderedContentTypes = new String[] { 724 ProgramFactory.PROGRAM_CONTENT_TYPE, 725 SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, 726 ContainerFactory.CONTAINER_CONTENT_TYPE, 727 CourseListFactory.COURSE_LIST_CONTENT_TYPE, 728 CourseFactory.COURSE_CONTENT_TYPE, 729 OrgUnitFactory.ORGUNIT_CONTENT_TYPE, 730 PersonFactory.PERSON_CONTENT_TYPE 731 }; 732 733 @Override 734 public int compare(Content c1, Content c2) 735 { 736 if (c1 == c2) 737 { 738 return 0; 739 } 740 741 String cTypeId1 = c1.getTypes()[0]; 742 String cTypeId2 = c2.getTypes()[0]; 743 744 int i1 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId1); 745 int i2 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId2); 746 747 if (i1 == i2) 748 { 749 // order by title for content of same type 750 int compareTo = c1.getTitle().compareTo(c2.getTitle()); 751 if (compareTo == 0) 752 { 753 // for content of same title, order by id to do not return 0 to add it in TreeSet 754 // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 755 return c1.getId().compareTo(c2.getId()); 756 } 757 else 758 { 759 return compareTo; 760 } 761 } 762 763 return i1 != -1 && i1 < i2 ? -1 : 1; 764 } 765 } 766 767}