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.temporal.ChronoUnit; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Date; 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(new Date()); 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 return newCatalog; 556 } 557 558 /** 559 * Get the programs of a catalog for all languages 560 * @param catalog The code of catalog 561 * @return The programs 562 */ 563 public AmetysObjectIterable<Program> getPrograms (String catalog) 564 { 565 return getPrograms(catalog, null); 566 } 567 568 /** 569 * Get the program's items of a catalog for all languages 570 * @param catalog The code of catalog 571 * @return The {@link ProgramItem} 572 */ 573 public AmetysObjectIterable<Content> getProgramItems(String catalog) 574 { 575 return _getContentsInCatalog(catalog, ProgramItem.PROGRAM_ITEM_CONTENT_TYPE); 576 } 577 578 private <T extends Content> AmetysObjectIterable<T> _getContentsInCatalog(String catalog, String contentTypeId) 579 { 580 List<Expression> exprs = new ArrayList<>(); 581 582 exprs.add(_cTypeEP.createHierarchicalCTExpression(contentTypeId)); 583 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 584 585 Expression expression = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 586 587 String query = ContentQueryHelper.getContentXPathQuery(expression); 588 return _resolver.query(query); 589 } 590 591 /** 592 * Get the programs of a catalog 593 * @param catalog The code of catalog 594 * @param lang The language. Can be null to get programs for all languages 595 * @return The programs 596 */ 597 public AmetysObjectIterable<Program> getPrograms (String catalog, String lang) 598 { 599 List<Expression> exprs = new ArrayList<>(); 600 exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 601 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 602 if (lang != null) 603 { 604 exprs.add(new LanguageExpression(Operator.EQ, lang)); 605 } 606 607 Expression programsExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 608 609 // Add sort criteria to get size 610 SortCriteria sortCriteria = new SortCriteria(); 611 sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 612 613 String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria); 614 return _resolver.query(programsQuery); 615 } 616 617 /** 618 * Copy the programs and its hierarchy from a catalog to another. 619 * The referenced courses are NOT copied. 620 * @param catalog The new catalog to populate 621 * @param catalogToCopy The catalog from which we copy the programs. 622 * @throws ProcessingException If an error occurred during copy 623 */ 624 public void copyCatalog(Catalog catalog, Catalog catalogToCopy) throws ProcessingException 625 { 626 String catalogToCopyName = catalogToCopy.getName(); 627 String catalogName = catalog.getName(); 628 String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED, ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED}; 629 try 630 { 631 Map<String, String> copiedPrograms = new HashMap<>(); 632 Map<String, String> copiedSubPrograms = new HashMap<>(); 633 Map<String, String> copiedContainers = new HashMap<>(); 634 Map<String, String> copiedCourseLists = new HashMap<>(); 635 Map<String, String> copiedCourses = new HashMap<>(); 636 Map<String, String> copiedCourseParts = new HashMap<>(); 637 638 Set<String> copyUpdaters = _copyUpdaterEP.getExtensionsIds(); 639 640 AmetysObjectIterable<Program> programs = getPrograms(catalogToCopyName); 641 642 // Do NOT commit yet to Solr in order to improve perfs 643 _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds); 644 long start = System.currentTimeMillis(); 645 646 getLogger().debug("Begin to iterate over programs for copying them"); 647 648 for (Program program : programs) 649 { 650 if (getLogger().isDebugEnabled()) 651 { 652 getLogger().debug("Start copying program '{}' (name: '{}', title: '{}')...", program.getId(), program.getName(), program.getTitle()); 653 } 654 655 Program newProgram = _odfHelper.copyProgramItem(program, catalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 656 657 for (String updaterId : copyUpdaters) 658 { 659 // Call updaters after copy of program 660 CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId); 661 updater.updateContent(catalogToCopyName, catalogName, program, newProgram); 662 } 663 } 664 665 for (String updaterId : copyUpdaters) 666 { 667 // Call updaters after full copy of catalog 668 CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId); 669 updater.updateContents(catalogToCopyName, catalogName, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 670 } 671 672 // Workflow 673 _addCopyStep(copiedPrograms.values()); 674 _addCopyStep(copiedSubPrograms.values()); 675 _addCopyStep(copiedContainers.values()); 676 _addCopyStep(copiedCourseLists.values()); 677 _addCopyStep(copiedCourses.values()); 678 _addCopyStep(copiedCourseParts.values()); 679 680 if (getLogger().isDebugEnabled()) 681 { 682 getLogger().debug("End of iteration over programs for copying them ({})", Duration.of((System.currentTimeMillis() - start) / 1000, ChronoUnit.SECONDS)); 683 } 684 685 } 686 catch (AmetysRepositoryException | WorkflowException e) 687 { 688 getLogger().error("Copy of items of catalog {} into catalog {} has failed", catalogToCopyName, catalogName); 689 throw new ProcessingException("Failed to copy catalog", e); 690 } 691 finally 692 { 693 _solrIndexHelper.restartSolrCommitForEvents(handledEventIds); 694 } 695 } 696 697 private void _addCopyStep(Collection<String> contentIds) throws AmetysRepositoryException, WorkflowException 698 { 699 for (String contentId : contentIds) 700 { 701 WorkflowAwareContent content = _resolver.resolveById(contentId); 702 _contentWorkflowHelper.doAction(content, getCopyActionId()); 703 } 704 } 705 706 /** 707 * Get the workflow action id for copy. 708 * @return The workflow action id 709 */ 710 protected int getCopyActionId() 711 { 712 return 210; 713 } 714 715 /** 716 * Delete catalog 717 * @param catalog the catalog to delete 718 * @return the result map 719 */ 720 public Map<String, Object> deleteCatalog(Catalog catalog) 721 { 722 Map<String, Object> result = new HashMap<>(); 723 result.put("id", catalog.getId()); 724 725 // Before deleting anything, we have to make sure that it's safe to delete the catalog and its programItems 726 String catalogName = catalog.getName(); 727 AmetysObjectIterable<Content> programItems = getProgramItems(catalogName); 728 AmetysObjectIterable<CoursePart> courseParts = _getCourseParts(catalogName); 729 730 List<Content> contentsToDelete = Stream.concat(courseParts.stream(), programItems.stream()).collect(Collectors.toUnmodifiableList()); 731 732 List<Content> referencingContents = _getExternalReferencingContents(contentsToDelete); 733 734 if (!referencingContents.isEmpty()) 735 { 736 for (Content content : referencingContents) 737 { 738 if (content instanceof ProgramItem || content instanceof CoursePart) 739 { 740 getLogger().error("{} '{}' ({}) is referencing a content of the catalog {} while being itself in the catalog {}. There is an inconsistency.", 741 content.getClass().getName(), 742 content.getTitle(), 743 content.getValue("code"), 744 catalogName, 745 content.getValue("catalog", false, StringUtils.EMPTY)); 746 } 747 else 748 { 749 getLogger().warn("Content {} ({}) is referencing a content of the catalog {}. There is an inconsistency.", 750 content.getTitle(), 751 content.getId(), 752 catalogName); 753 } 754 } 755 result.put("error", "referencing-contents"); 756 result.put("referencingContents", referencingContents); 757 return result; 758 } 759 760 // Everything is fine, we can delete the courseParts, the programItems and the catalog 761 String[] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_DELETED}; 762 try 763 { 764 _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds); 765 contentsToDelete.forEach(this::_deleteContent); 766 } 767 finally 768 { 769 _solrIndexHelper.restartSolrCommitForEvents(handledEventIds); 770 } 771 772 ModifiableAmetysObject parent = catalog.getParent(); 773 catalog.remove(); 774 parent.saveChanges(); 775 776 return result; 777 } 778 779 /** 780 * Get the course part of a catalog for all languages 781 * @param catalog The code of catalog 782 * @return The {@link CoursePart}s 783 */ 784 private AmetysObjectIterable<CoursePart> _getCourseParts(String catalog) 785 { 786 return _getContentsInCatalog(catalog, CoursePartFactory.COURSE_PART_CONTENT_TYPE); 787 } 788 789 /** 790 * Get the list of contents referencing one of the {@code ProgramItem}s in the set. 791 * This method will ignore content that are included in the set and any {@code CoursePart} belonging to a {@code Course} of the set 792 * @param contentsToDelete the contents to be tested 793 * @return the contents referencing those program items excluding the course parts id 794 */ 795 private List<Content> _getExternalReferencingContents(List<Content> contentsToDelete) 796 { 797 798 // Get all the Contents referencing one of the content to delete but not one of the content to delete 799 List<Content> referencingContents = contentsToDelete.stream() 800 .map(Content::getReferencingContents) // get the referencing contents 801 .flatMap(Collection::stream) // flatten the Collection 802 .distinct() // remove all duplicates 803 .filter(content -> !contentsToDelete.contains(content)) 804 .collect(Collectors.toUnmodifiableList()); // collect it into a list 805 return referencingContents; 806 } 807 808 private void _deleteContent(Content content) 809 { 810 811 Map<String, Object> eventParams = new HashMap<>(); 812 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 813 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 814 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 815 ModifiableAmetysObject parent = content.getParent(); 816 817 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams)); 818 819 // Remove the content. 820 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 821 if (lockedContent.isLocked()) 822 { 823 lockedContent.unlock(); 824 } 825 826 ((RemovableAmetysObject) content).remove(); 827 828 829 parent.saveChanges(); 830 831 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams)); 832 } 833 834 private ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 835 { 836 ModifiableTraversableAmetysObject definitionsNode; 837 if (parentNode.hasChild(nodeName)) 838 { 839 definitionsNode = parentNode.getChild(nodeName); 840 } 841 else 842 { 843 definitionsNode = parentNode.createChild(nodeName, nodeType); 844 parentNode.saveChanges(); 845 } 846 return definitionsNode; 847 } 848}