001/* 002 * Copyright 2014 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.catalog; 017 018import java.time.Duration; 019import java.time.ZonedDateTime; 020import java.time.temporal.ChronoUnit; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.cocoon.ProcessingException; 039import org.apache.cocoon.components.ContextHelper; 040import org.apache.cocoon.environment.Request; 041import org.apache.commons.lang.StringUtils; 042 043import org.ametys.cms.ObservationConstants; 044import org.ametys.cms.content.archive.ArchiveConstants; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.data.ContentValue; 047import org.ametys.cms.indexing.solr.SolrIndexHelper; 048import org.ametys.cms.repository.Content; 049import org.ametys.cms.repository.ContentQueryHelper; 050import org.ametys.cms.repository.ContentTypeExpression; 051import org.ametys.cms.repository.LanguageExpression; 052import org.ametys.cms.repository.ModifiableDefaultContent; 053import org.ametys.cms.repository.WorkflowAwareContent; 054import org.ametys.cms.workflow.ContentWorkflowHelper; 055import org.ametys.core.observation.Event; 056import org.ametys.core.observation.ObservationManager; 057import org.ametys.core.schedule.progression.ContainerProgressionTracker; 058import org.ametys.core.schedule.progression.SimpleProgressionTracker; 059import org.ametys.core.ui.Callable; 060import org.ametys.core.user.CurrentUserProvider; 061import org.ametys.odf.ODFHelper; 062import org.ametys.odf.ProgramItem; 063import org.ametys.odf.course.Course; 064import org.ametys.odf.course.CourseContainer; 065import org.ametys.odf.courselist.CourseList; 066import org.ametys.odf.courselist.CourseListContainer; 067import org.ametys.odf.coursepart.CoursePart; 068import org.ametys.odf.coursepart.CoursePartFactory; 069import org.ametys.odf.data.EducationalPath; 070import org.ametys.odf.program.Program; 071import org.ametys.odf.program.ProgramFactory; 072import org.ametys.odf.program.TraversableProgramPart; 073import org.ametys.plugins.repository.AmetysObject; 074import org.ametys.plugins.repository.AmetysObjectIterable; 075import org.ametys.plugins.repository.AmetysObjectResolver; 076import org.ametys.plugins.repository.AmetysRepositoryException; 077import org.ametys.plugins.repository.ModifiableAmetysObject; 078import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 079import org.ametys.plugins.repository.RemovableAmetysObject; 080import org.ametys.plugins.repository.RepositoryConstants; 081import org.ametys.plugins.repository.TraversableAmetysObject; 082import org.ametys.plugins.repository.UnknownAmetysObjectException; 083import org.ametys.plugins.repository.lock.LockableAmetysObject; 084import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 085import org.ametys.plugins.repository.query.QueryHelper; 086import org.ametys.plugins.repository.query.SortCriteria; 087import org.ametys.plugins.repository.query.expression.AndExpression; 088import org.ametys.plugins.repository.query.expression.Expression; 089import org.ametys.plugins.repository.query.expression.Expression.Operator; 090import org.ametys.plugins.repository.query.expression.StringExpression; 091import org.ametys.runtime.plugin.component.AbstractLogEnabled; 092import org.ametys.runtime.plugin.component.PluginAware; 093 094import com.opensymphony.workflow.WorkflowException; 095 096/** 097 * Component to handle ODF catalogs 098 */ 099public class CatalogsManager extends AbstractLogEnabled implements Serviceable, Component, PluginAware, Contextualizable 100{ 101 /** Avalon Role */ 102 public static final String ROLE = CatalogsManager.class.getName(); 103 104 private AmetysObjectResolver _resolver; 105 106 private CopyCatalogUpdaterExtensionPoint _copyUpdaterEP; 107 108 private ObservationManager _observationManager; 109 110 private CurrentUserProvider _userProvider; 111 112 private ContentWorkflowHelper _contentWorkflowHelper; 113 114 private String _pluginName; 115 116 private ODFHelper _odfHelper; 117 118 private ContentTypeExtensionPoint _cTypeEP; 119 120 private Context _context; 121 122 private SolrIndexHelper _solrIndexHelper; 123 124 private String _defaultCatalogId; 125 126 private CurrentUserProvider _currentUserProvider; 127 128 public void contextualize(Context context) throws ContextException 129 { 130 _context = context; 131 } 132 133 @Override 134 public void service(ServiceManager manager) throws ServiceException 135 { 136 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 137 _copyUpdaterEP = (CopyCatalogUpdaterExtensionPoint) manager.lookup(CopyCatalogUpdaterExtensionPoint.ROLE); 138 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 139 _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 140 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 141 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 142 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 143 _solrIndexHelper = (SolrIndexHelper) manager.lookup(SolrIndexHelper.ROLE); 144 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 145 } 146 147 public void setPluginInfo(String pluginName, String featureName, String id) 148 { 149 _pluginName = pluginName; 150 } 151 152 /** 153 * Get the list of catalogs 154 * @return the catalogs 155 */ 156 public List<Catalog> getCatalogs() 157 { 158 List<Catalog> result = new ArrayList<>(); 159 160 TraversableAmetysObject catalogsNode = getCatalogsRootNode(); 161 162 AmetysObjectIterable<Catalog> catalogs = catalogsNode.getChildren(); 163 for (Catalog catalog : catalogs) 164 { 165 result.add(catalog); 166 } 167 168 return result; 169 } 170 171 /** 172 * Get a catalog matching with the given name 173 * @param name The name 174 * @return a catalog, or null if not found 175 */ 176 public Catalog getCatalog(String name) 177 { 178 ModifiableTraversableAmetysObject catalogsNode = getCatalogsRootNode(); 179 180 if (StringUtils.isNotEmpty(name) && catalogsNode.hasChild(name)) 181 { 182 return catalogsNode.getChild(name); 183 } 184 185 // Not found 186 return null; 187 } 188 189 /** 190 * Returns the name of the default catalog 191 * @return the name of the default catalog 192 */ 193 @Callable 194 public String getDefaultCatalogName() 195 { 196 return Optional.ofNullable(getDefaultCatalog()) 197 .map(Catalog::getName) 198 .orElse(null); 199 } 200 201 /** 202 * Returns the default catalog 203 * @return the default catalog or null if no default catalog was defined. 204 */ 205 public synchronized Catalog getDefaultCatalog() 206 { 207 if (_defaultCatalogId == null) 208 { 209 updateDefaultCatalog(); 210 } 211 212 if (_defaultCatalogId != null) 213 { 214 try 215 { 216 return _resolver.resolveById(_defaultCatalogId); 217 } 218 catch (UnknownAmetysObjectException e) 219 { 220 _defaultCatalogId = null; 221 } 222 } 223 224 return null; 225 } 226 227 /** 228 * Updates the default catalog (if it's null or if the user has updated it). 229 */ 230 void updateDefaultCatalog() 231 { 232 List<Catalog> catalogs = getCatalogs(); 233 for (Catalog catalog : catalogs) 234 { 235 if (catalog.isDefault()) 236 { 237 _defaultCatalogId = catalog.getId(); 238 return; 239 } 240 } 241 242 // If no default catalog found, get the only catalog if it exists 243 if (catalogs.size() == 1) 244 { 245 _defaultCatalogId = catalogs.get(0).getId(); 246 } 247 } 248 249 /** 250 * Get the name of the catalog of a ODF content 251 * @param contentId The id of content 252 * @return The catalog's name 253 */ 254 @Callable 255 public String getContentCatalog(String contentId) 256 { 257 Content content = _resolver.resolveById(contentId); 258 259 if (content instanceof ProgramItem) 260 { 261 return ((ProgramItem) content).getCatalog(); 262 } 263 264 // Get catalog from its parents (unecessary ?) 265 AmetysObject parent = content.getParent(); 266 while (parent != null) 267 { 268 if (parent instanceof ProgramItem) 269 { 270 return ((ProgramItem) parent).getCatalog(); 271 } 272 parent = parent.getParent(); 273 } 274 275 return null; 276 } 277 278 /** 279 * Determines if the catalog can be modified from the given content 280 * @param contentId The content id 281 * @return A map with success=false if the catalog cannot be edited 282 */ 283 @Callable 284 public Map<String, Object> canEditCatalog(String contentId) 285 { 286 Map<String, Object> result = new HashMap<>(); 287 288 Content content = _resolver.resolveById(contentId); 289 290 if (content instanceof ProgramItem) 291 { 292 if (_isReferenced(content)) 293 { 294 result.put("success", false); 295 result.put("error", "referenced"); 296 } 297 else if (_hasSharedContent((ProgramItem) content, (ProgramItem) content)) 298 { 299 result.put("success", false); 300 result.put("error", "hasSharedContent"); 301 } 302 else 303 { 304 result.put("success", true); 305 } 306 307 } 308 else 309 { 310 result.put("success", false); 311 result.put("error", "typeError"); 312 } 313 314 return result; 315 } 316 317 private boolean _isReferenced (Content content) 318 { 319 return !_odfHelper.getParentProgramItems((ProgramItem) content).isEmpty(); 320 } 321 322 private boolean _hasSharedContent (ProgramItem rootProgramItem, ProgramItem programItem) 323 { 324 List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem); 325 326 for (ProgramItem child : children) 327 { 328 if (_isShared(rootProgramItem, child)) 329 { 330 return true; 331 } 332 } 333 334 if (programItem instanceof Course) 335 { 336 List<CoursePart> courseParts = ((Course) programItem).getCourseParts(); 337 for (CoursePart coursePart : courseParts) 338 { 339 List<ProgramItem> parentCourses = coursePart.getCourses() 340 .stream() 341 .map(ProgramItem.class::cast) 342 .collect(Collectors.toList()); 343 if (parentCourses.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parentCourses)) 344 { 345 return true; 346 } 347 } 348 } 349 350 return false; 351 } 352 353 private boolean _isShared(ProgramItem rootProgramItem, ProgramItem programItem) 354 { 355 try 356 { 357 List<ProgramItem> parents = _odfHelper.getParentProgramItems(programItem); 358 if (parents.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parents) 359 || _hasSharedContent(rootProgramItem, programItem)) 360 { 361 return true; 362 } 363 } 364 catch (UnknownAmetysObjectException e) 365 { 366 // Nothing 367 } 368 369 return false; 370 } 371 372 private boolean _isPartOfSameStructure(ProgramItem rootProgramItem, List<ProgramItem> programItems) 373 { 374 for (ProgramItem programItem : programItems) 375 { 376 boolean isPartOfInitalStructure = false; 377 378 List<EducationalPath> ancestorPaths = _odfHelper.getEducationalPaths(programItem); 379 380 for (EducationalPath ancestorPath : ancestorPaths) 381 { 382 isPartOfInitalStructure = ancestorPath.resolveProgramItems(_resolver).anyMatch(p -> p.equals(rootProgramItem)); 383 break; 384 } 385 386 if (!isPartOfInitalStructure) 387 { 388 // The content is shared outside the program item to edit 389 return false; 390 } 391 } 392 393 return true; 394 } 395 396 /** 397 * Set the catalog of a content. This will modify recursively the catalog of referenced children 398 * @param catalog The catalog 399 * @param contentId The id of content to edit 400 * @throws WorkflowException if an error occurred 401 */ 402 @Callable 403 public void setContentCatalog(String catalog, String contentId) throws WorkflowException 404 { 405 Content content = _resolver.resolveById(contentId); 406 407 if (content instanceof ProgramItem) 408 { 409 _setCatalog(content, catalog); 410 } 411 else 412 { 413 throw new IllegalArgumentException("You can not edit the catalog of the content " + contentId); 414 } 415 } 416 417 private void _setCatalog (Content content, String catalogName) throws WorkflowException 418 { 419 if (content instanceof ProgramItem) 420 { 421 String oldCatalog = ((ProgramItem) content).getCatalog(); 422 if (!catalogName.equals(oldCatalog)) 423 { 424 ((ProgramItem) content).setCatalog(catalogName); 425 426 if (content instanceof WorkflowAwareContent) 427 { 428 _applyChanges((WorkflowAwareContent) content); 429 } 430 } 431 } 432 else if (content instanceof CoursePart) 433 { 434 String oldCatalog = ((CoursePart) content).getCatalog(); 435 if (!catalogName.equals(oldCatalog)) 436 { 437 ((CoursePart) content).setCatalog(catalogName); 438 439 if (content instanceof WorkflowAwareContent) 440 { 441 _applyChanges((WorkflowAwareContent) content); 442 } 443 } 444 } 445 446 _setCatalogToChildren(content, catalogName); 447 } 448 449 private void _setCatalogToChildren (Content content, String catalogName) throws WorkflowException 450 { 451 if (content instanceof TraversableProgramPart) 452 { 453 ContentValue[] children = content.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]); 454 for (ContentValue child : children) 455 { 456 try 457 { 458 _setCatalog(child.getContent(), catalogName); 459 } 460 catch (UnknownAmetysObjectException e) 461 { 462 // Nothing 463 } 464 } 465 } 466 467 if (content instanceof CourseContainer) 468 { 469 for (Course course : ((CourseContainer) content).getCourses()) 470 { 471 _setCatalog(course, catalogName); 472 } 473 } 474 475 if (content instanceof CourseListContainer) 476 { 477 for (CourseList cl : ((CourseListContainer) content).getCourseLists()) 478 { 479 _setCatalog(cl, catalogName); 480 } 481 } 482 483 if (content instanceof Course) 484 { 485 for (CoursePart coursePart : ((Course) content).getCourseParts()) 486 { 487 _setCatalog(coursePart, catalogName); 488 } 489 } 490 } 491 492 private void _applyChanges(WorkflowAwareContent content) throws WorkflowException 493 { 494 ((ModifiableDefaultContent) content).setLastContributor(_userProvider.getUser()); 495 ((ModifiableDefaultContent) content).setLastModified(ZonedDateTime.now()); 496 497 // Remove the proposal date. 498 content.setProposalDate(null); 499 500 // Save changes 501 content.saveChanges(); 502 503 // Notify listeners 504 Map<String, Object> eventParams = new HashMap<>(); 505 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 506 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 507 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _userProvider.getUser(), eventParams)); 508 509 _contentWorkflowHelper.doAction(content, 22); 510 } 511 512 /** 513 * Get the root catalogs storage object. 514 * @return the root catalogs node 515 * @throws AmetysRepositoryException if a repository error occurs. 516 */ 517 public ModifiableTraversableAmetysObject getCatalogsRootNode() throws AmetysRepositoryException 518 { 519 String originalWorkspace = null; 520 Request request = ContextHelper.getRequest(_context); 521 try 522 { 523 originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 524 if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace)) 525 { 526 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 527 } 528 529 ModifiableTraversableAmetysObject rootNode = _resolver.resolveByPath("/"); 530 ModifiableTraversableAmetysObject pluginsNode = _getOrCreateNode(rootNode, "ametys:plugins", "ametys:unstructured"); 531 ModifiableTraversableAmetysObject pluginNode = _getOrCreateNode(pluginsNode, _pluginName, "ametys:unstructured"); 532 533 return _getOrCreateNode(pluginNode, "catalogs", "ametys:unstructured"); 534 } 535 catch (AmetysRepositoryException e) 536 { 537 throw new AmetysRepositoryException("Unable to get the ODF catalogs root node", e); 538 } 539 finally 540 { 541 if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace)) 542 { 543 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace); 544 } 545 } 546 } 547 548 /** 549 * Create a new catalog 550 * @param name The unique name 551 * @param title The title of catalog 552 * @return the created catalog 553 */ 554 public Catalog createCatalog(String name, String title) 555 { 556 Catalog newCatalog = null; 557 558 ModifiableTraversableAmetysObject catalogsNode = getCatalogsRootNode(); 559 560 newCatalog = catalogsNode.createChild(name, "ametys:catalog"); 561 newCatalog.setTitle(title); 562 563 if (getCatalogs().size() == 1) 564 { 565 newCatalog.setDefault(true); 566 } 567 568 newCatalog.saveChanges(); 569 570 return newCatalog; 571 } 572 573 /** 574 * Get the programs of a catalog for all languages 575 * @param catalog The code of catalog 576 * @return The programs 577 */ 578 public AmetysObjectIterable<Program> getPrograms (String catalog) 579 { 580 return getPrograms(catalog, null); 581 } 582 583 /** 584 * Get the program's items of a catalog for all languages 585 * @param catalog The code of catalog 586 * @return The {@link ProgramItem} 587 */ 588 private AmetysObjectIterable<Content> _getProgramItems(String catalog) 589 { 590 return _getContentsInCatalog(catalog, ProgramItem.PROGRAM_ITEM_CONTENT_TYPE); 591 } 592 593 /** 594 * Get the contents of a content type in a catalog. 595 * @param <T> The type of the elements to get 596 * @param catalog The catalog name 597 * @param contentTypeId The content type identifier 598 * @return An iterable of contents with given content type in the catalog 599 */ 600 private <T extends Content> AmetysObjectIterable<T> _getContentsInCatalog(String catalog, String contentTypeId) 601 { 602 List<Expression> exprs = new ArrayList<>(); 603 604 exprs.add(_cTypeEP.createHierarchicalCTExpression(contentTypeId)); 605 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 606 607 Expression expression = new AndExpression(exprs.toArray(Expression[]::new)); 608 609 String query = ContentQueryHelper.getContentXPathQuery(expression); 610 return _resolver.query(query); 611 } 612 613 /** 614 * Get the programs of a catalog 615 * @param catalog The code of catalog 616 * @param lang The language. Can be null to get programs for all languages 617 * @return The programs 618 */ 619 public AmetysObjectIterable<Program> getPrograms (String catalog, String lang) 620 { 621 List<Expression> exprs = new ArrayList<>(); 622 exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 623 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 624 if (lang != null) 625 { 626 exprs.add(new LanguageExpression(Operator.EQ, lang)); 627 } 628 629 Expression programsExpression = new AndExpression(exprs.toArray(Expression[]::new)); 630 631 // Add sort criteria to get size 632 SortCriteria sortCriteria = new SortCriteria(); 633 sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 634 635 String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria); 636 return _resolver.query(programsQuery); 637 } 638 639 /** 640 * Copy the programs and its hierarchy from a catalog to another. 641 * The referenced courses are NOT copied. 642 * @param catalog The new catalog to populate 643 * @param catalogToCopy The catalog from which we copy the programs. 644 * @param progressionTracker the progression tracker for catalog copy 645 * @throws ProcessingException If an error occurred during copy 646 */ 647 public void copyCatalog(Catalog catalog, Catalog catalogToCopy, ContainerProgressionTracker progressionTracker) throws ProcessingException 648 { 649 String catalogToCopyName = catalogToCopy.getName(); 650 String catalogName = catalog.getName(); 651 String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED, ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED}; 652 try 653 { 654 Map<Content, Content> copiedContents = new HashMap<>(); 655 656 Set<String> copyUpdaters = _copyUpdaterEP.getExtensionsIds(); 657 658 AmetysObjectIterable<Program> programs = getPrograms(catalogToCopyName); 659 660 SimpleProgressionTracker copyStep = (SimpleProgressionTracker) progressionTracker.getCurrentStep(); 661 copyStep.setSize(programs.getSize()); 662 663 // Do NOT commit yet to Solr in order to improve perfs 664 _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds); 665 long start = System.currentTimeMillis(); 666 667 getLogger().debug("Begin to iterate over programs for copying them"); 668 669 for (Program program : programs) 670 { 671 if (getLogger().isDebugEnabled()) 672 { 673 getLogger().debug("Start copying program '{}' (name: '{}', title: '{}')...", program.getId(), program.getName(), program.getTitle()); 674 } 675 676 _odfHelper.copyProgramItem(program, catalogName, true, copiedContents); 677 copyStep.increment(); 678 } 679 680 SimpleProgressionTracker updatesAfterCopy = (SimpleProgressionTracker) progressionTracker.getCurrentStep(); 681 updatesAfterCopy.setSize(copyUpdaters.size()); 682 683 for (String updaterId : copyUpdaters) 684 { 685 // Call updaters after full copy of catalog 686 CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId); 687 updater.updateContents(catalogToCopyName, catalogName, copiedContents, null); 688 updater.copyAdditionalContents(catalogToCopyName, catalogName); 689 690 updatesAfterCopy.increment(); 691 } 692 693 // Workflow 694 _addCopyStep(copiedContents.values(), (SimpleProgressionTracker) progressionTracker.getCurrentStep()); 695 696 if (getLogger().isDebugEnabled()) 697 { 698 getLogger().debug("End of iteration over programs for copying them ({})", Duration.of((System.currentTimeMillis() - start) / 1000, ChronoUnit.SECONDS)); 699 } 700 701 } 702 catch (AmetysRepositoryException | WorkflowException e) 703 { 704 getLogger().error("Copy of items of catalog {} into catalog {} has failed", catalogToCopyName, catalogName); 705 throw new ProcessingException("Failed to copy catalog", e); 706 } 707 finally 708 { 709 SimpleProgressionTracker restartCommitStep = (SimpleProgressionTracker) progressionTracker.getCurrentStep(); 710 restartCommitStep.setSize(1); 711 _solrIndexHelper.restartSolrCommitForEvents(handledEventIds); 712 restartCommitStep.increment(); 713 } 714 } 715 716 private void _addCopyStep(Collection<Content> contents, SimpleProgressionTracker progressionTracker) throws AmetysRepositoryException, WorkflowException 717 { 718 progressionTracker.setSize(contents.size()); 719 720 for (Content content : contents) 721 { 722 if (content instanceof WorkflowAwareContent workflowAwareContent) 723 { 724 _contentWorkflowHelper.doAction(workflowAwareContent, getCopyActionId()); 725 } 726 progressionTracker.increment(); 727 } 728 } 729 730 /** 731 * Get the workflow action id for copy. 732 * @return The workflow action id 733 */ 734 protected int getCopyActionId() 735 { 736 return 210; 737 } 738 739 /** 740 * Delete catalog 741 * @param catalog the catalog to delete 742 * @return the result map 743 */ 744 public Map<String, Object> deleteCatalog(Catalog catalog) 745 { 746 Map<String, Object> result = new HashMap<>(); 747 result.put("id", catalog.getId()); 748 749 String catalogName = catalog.getName(); 750 List<Content> contentsToDelete = getContents(catalogName); 751 752 // Before deleting anything, we have to make sure that it's safe to delete the catalog and its programItems 753 List<Content> referencingContents = _getExternalReferencingContents(contentsToDelete); 754 755 if (!referencingContents.isEmpty()) 756 { 757 for (Content content : referencingContents) 758 { 759 if (content instanceof ProgramItem || content instanceof CoursePart) 760 { 761 getLogger().error("{} '{}' ({}) is referencing a content of the catalog {} while being itself in the catalog {}. There is an inconsistency.", 762 content.getClass().getName(), 763 content.getTitle(), 764 content.getValue("code"), 765 catalogName, 766 content.getValue("catalog", false, StringUtils.EMPTY)); 767 } 768 else 769 { 770 getLogger().warn("Content {} ({}) is referencing a content of the catalog {}. There is an inconsistency.", 771 content.getTitle(), 772 content.getId(), 773 catalogName); 774 } 775 } 776 result.put("error", "referencing-contents"); 777 result.put("referencingContents", referencingContents); 778 return result; 779 } 780 781 // Everything is fine, we can delete the courseParts, the programItems and the catalog 782 String[] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_DELETED}; 783 try 784 { 785 _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds); 786 contentsToDelete.forEach(this::_deleteContent); 787 } 788 finally 789 { 790 _solrIndexHelper.restartSolrCommitForEvents(handledEventIds); 791 } 792 793 ModifiableAmetysObject parent = catalog.getParent(); 794 catalog.remove(); 795 parent.saveChanges(); 796 797 return result; 798 } 799 800 /** 801 * Get all the contents to delete when deleting a catalog. 802 * @param catalogName The catalog name 803 * @return a {@link Stream} of {@link Content} to delete 804 */ 805 public List<Content> getContents(String catalogName) 806 { 807 List<Content> contents = new ArrayList<>(); 808 contents.addAll(_getProgramItems(catalogName).stream().toList()); 809 contents.addAll(_getCourseParts(catalogName).stream().toList()); 810 811 for (String updaterId : _copyUpdaterEP.getExtensionsIds()) 812 { 813 CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId); 814 contents.addAll(updater.getAdditionalContents(catalogName)); 815 } 816 817 return contents; 818 } 819 820 /** 821 * Get the course part of a catalog for all languages 822 * @param catalog The code of catalog 823 * @return The {@link CoursePart}s 824 */ 825 private AmetysObjectIterable<CoursePart> _getCourseParts(String catalog) 826 { 827 return _getContentsInCatalog(catalog, CoursePartFactory.COURSE_PART_CONTENT_TYPE); 828 } 829 830 /** 831 * Get the list of contents referencing one of the {@code ProgramItem}s in the set. 832 * This method will ignore content that are included in the set and any {@code CoursePart} belonging to a {@code Course} of the set 833 * @param contentsToDelete the contents to be tested 834 * @return the contents referencing those program items excluding the course parts id 835 */ 836 private List<Content> _getExternalReferencingContents(List<Content> contentsToDelete) 837 { 838 839 // Get all the Contents referencing one of the content to delete but not one of the content to delete 840 List<Content> referencingContents = contentsToDelete.stream() 841 .map(Content::getReferencingContents) // get the referencing contents 842 .flatMap(Collection::stream) // flatten the Collection 843 .distinct() // remove all duplicates 844 .filter(content -> !contentsToDelete.contains(content)) 845 .collect(Collectors.toUnmodifiableList()); // collect it into a list 846 return referencingContents; 847 } 848 849 private void _deleteContent(Content content) 850 { 851 Map<String, Object> eventParams = new HashMap<>(); 852 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 853 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 854 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 855 ModifiableAmetysObject parent = content.getParent(); 856 857 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams)); 858 859 // Remove the content. 860 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 861 if (lockedContent.isLocked()) 862 { 863 lockedContent.unlock(); 864 } 865 866 ((RemovableAmetysObject) content).remove(); 867 868 869 parent.saveChanges(); 870 871 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams)); 872 } 873 874 private ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 875 { 876 ModifiableTraversableAmetysObject definitionsNode; 877 if (parentNode.hasChild(nodeName)) 878 { 879 definitionsNode = parentNode.getChild(nodeName); 880 } 881 else 882 { 883 definitionsNode = parentNode.createChild(nodeName, nodeType); 884 parentNode.saveChanges(); 885 } 886 return definitionsNode; 887 } 888}