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