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