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