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