001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.odf; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.commons.lang.StringUtils; 031import org.apache.commons.lang3.ArrayUtils; 032import org.apache.commons.lang3.tuple.Pair; 033 034import org.ametys.cms.content.external.ExternalizableMetadataHelper; 035import org.ametys.cms.content.external.ExternalizableMetadataProviderExtensionPoint; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.repository.ContentQueryHelper; 039import org.ametys.cms.repository.ContentTypeExpression; 040import org.ametys.cms.repository.DefaultContent; 041import org.ametys.cms.repository.LanguageExpression; 042import org.ametys.cms.repository.ModifiableContent; 043import org.ametys.cms.repository.WorkflowAwareContent; 044import org.ametys.cms.workflow.ContentWorkflowHelper; 045import org.ametys.core.observation.ObservationManager; 046import org.ametys.core.ui.Callable; 047import org.ametys.core.user.CurrentUserProvider; 048import org.ametys.odf.course.Course; 049import org.ametys.odf.course.CourseContainer; 050import org.ametys.odf.course.ShareableCourseHelper; 051import org.ametys.odf.courselist.CourseList; 052import org.ametys.odf.courselist.CourseListContainer; 053import org.ametys.odf.coursepart.CoursePart; 054import org.ametys.odf.coursepart.CoursePartFactory; 055import org.ametys.odf.orgunit.OrgUnit; 056import org.ametys.odf.orgunit.RootOrgUnitProvider; 057import org.ametys.odf.program.AbstractProgram; 058import org.ametys.odf.program.AbstractTraversableProgramPart; 059import org.ametys.odf.program.Container; 060import org.ametys.odf.program.Program; 061import org.ametys.odf.program.ProgramPart; 062import org.ametys.odf.program.SubProgram; 063import org.ametys.odf.program.TraversableProgramPart; 064import org.ametys.plugins.repository.AmetysObject; 065import org.ametys.plugins.repository.AmetysObjectExistsException; 066import org.ametys.plugins.repository.AmetysObjectIterable; 067import org.ametys.plugins.repository.AmetysObjectIterator; 068import org.ametys.plugins.repository.AmetysObjectResolver; 069import org.ametys.plugins.repository.AmetysRepositoryException; 070import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 071import org.ametys.plugins.repository.RepositoryConstants; 072import org.ametys.plugins.repository.UnknownAmetysObjectException; 073import org.ametys.plugins.repository.collection.AmetysObjectCollection; 074import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 075import org.ametys.plugins.repository.query.SortCriteria; 076import org.ametys.plugins.repository.query.expression.AndExpression; 077import org.ametys.plugins.repository.query.expression.Expression; 078import org.ametys.plugins.repository.query.expression.Expression.Operator; 079import org.ametys.plugins.repository.query.expression.StringExpression; 080import org.ametys.runtime.i18n.I18nizableText; 081import org.ametys.runtime.plugin.component.AbstractLogEnabled; 082import org.ametys.runtime.plugin.component.PluginAware; 083 084import com.opensymphony.workflow.WorkflowException; 085 086/** 087 * Helper for ODF contents 088 * 089 */ 090public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware 091{ 092 /** The component role. */ 093 public static final String ROLE = ODFHelper.class.getName(); 094 095 /** The default id of initial workflow action */ 096 protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0; 097 098 /** Ametys object resolver */ 099 protected AmetysObjectResolver _resolver; 100 /** The content workflow helper */ 101 protected ContentWorkflowHelper _contentWorkflowHelper; 102 /** The content types manager */ 103 protected ContentTypeExtensionPoint _cTypeEP; 104 /** The observation manager */ 105 protected ObservationManager _observationManager; 106 /** The current user provider */ 107 protected CurrentUserProvider _currentUserProvider; 108 /** Root orgunit */ 109 protected RootOrgUnitProvider _ouRootProvider; 110 /** Provider for externalizable metadata */ 111 protected ExternalizableMetadataProviderExtensionPoint _externalizableMetadataProviderEP; 112 /** Helper for shareable course */ 113 protected ShareableCourseHelper _shareableCourseHelper; 114 115 private String _pluginName; 116 117 118 @Override 119 public void service(ServiceManager manager) throws ServiceException 120 { 121 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 122 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 123 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 124 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 125 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 126 _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 127 _externalizableMetadataProviderEP = (ExternalizableMetadataProviderExtensionPoint) manager.lookup(ExternalizableMetadataProviderExtensionPoint.ROLE); 128 _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE); 129 } 130 131 @Override 132 public void setPluginInfo(String pluginName, String featureName, String id) 133 { 134 _pluginName = pluginName; 135 } 136 137 /** 138 * Gets the root for ODF contents 139 * @return the root for ODF contents 140 */ 141 public AmetysObjectCollection getRootContent() 142 { 143 return getRootContent(false); 144 } 145 146 /** 147 * Gets the root for ODF contents 148 * @param create <code>true</code> to create automatically the root when missing. 149 * @return the root for ODF contents 150 */ 151 public AmetysObjectCollection getRootContent(boolean create) 152 { 153 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/"); 154 155 boolean needSave = false; 156 if (!pluginsNode.hasChild(_pluginName)) 157 { 158 if (create) 159 { 160 pluginsNode.createChild(_pluginName, "ametys:unstructured"); 161 needSave = true; 162 } 163 else 164 { 165 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing"); 166 } 167 } 168 169 ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName); 170 if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents")) 171 { 172 if (create) 173 { 174 pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection"); 175 needSave = true; 176 } 177 else 178 { 179 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing"); 180 } 181 } 182 183 if (needSave) 184 { 185 pluginsNode.saveChanges(); 186 } 187 188 return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 189 } 190 191 /** 192 * Get the {@link ProgramItem}s matching the given arguments 193 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 194 * @param code The code. Can be null to get program's items regardless of their code 195 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 196 * @param lang The search language. Can be null to get program's items regardless of their language 197 * @param <C> The content return type 198 * @return The matching program items 199 */ 200 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang) 201 { 202 return getProgramItems(cTypeId, code, catalogName, lang, null, null); 203 } 204 205 /** 206 * Get the {@link ProgramItem}s matching the given arguments 207 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 208 * @param code The code. Can be null to get program's items regardless of their code 209 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 210 * @param lang The search language. Can be null to get program's items regardless of their language 211 * @param additionnalExpr An additional expression for filtering result. Can be null 212 * @param sortCriteria criteria for sorting results 213 * @param <C> The content return type 214 * @return The matching program items 215 */ 216 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 217 { 218 List<Expression> exprs = new ArrayList<>(); 219 220 if (StringUtils.isNotEmpty(cTypeId)) 221 { 222 exprs.add(new ContentTypeExpression(Operator.EQ, cTypeId)); 223 } 224 if (StringUtils.isNotEmpty(code)) 225 { 226 exprs.add(new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, code)); 227 } 228 if (StringUtils.isNotEmpty(catalogName)) 229 { 230 exprs.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName)); 231 } 232 if (StringUtils.isNotEmpty(lang)) 233 { 234 exprs.add(new LanguageExpression(Operator.EQ, lang)); 235 } 236 if (additionnalExpr != null) 237 { 238 exprs.add(additionnalExpr); 239 } 240 241 Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 242 243 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria); 244 return _resolver.query(xpathQuery); 245 } 246 247 /** 248 * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language 249 * @param srcCoursePart The source course part 250 * @param catalogName The name of catalog to search into 251 * @param lang The search language 252 * @return The equivalent program item or <code>null</code> if not exists 253 */ 254 public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang) 255 { 256 return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang); 257 } 258 259 /** 260 * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 261 * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem} 262 * @param srcProgramItem The source program item 263 * @param catalogName The name of catalog to search into 264 * @param lang The search language 265 * @return The equivalent program item or <code>null</code> if not exists 266 */ 267 public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang) 268 { 269 return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang); 270 } 271 272 /** 273 * Get the equivalent {@link Content} having the same code in given catalog and language 274 * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject} 275 * @param contentType The content type to search for 276 * @param odfContentCode The code of the ODF content 277 * @param catalogName The name of catalog to search into 278 * @param lang The search language 279 * @return The equivalent content or <code>null</code> if not exists 280 */ 281 public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang) 282 { 283 Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType); 284 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 285 Expression catalogExpr = new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName); 286 Expression codeExpr = new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, odfContentCode); 287 288 Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr); 289 290 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 291 AmetysObjectIterable<T> contents = _resolver.query(xpathQuery); 292 AmetysObjectIterator<T> contentsIt = contents.iterator(); 293 if (contentsIt.hasNext()) 294 { 295 return contentsIt.next(); 296 } 297 298 return null; 299 } 300 301 /** 302 * Get the child program items of a {@link ProgramItem} 303 * @param programItem The program item 304 * @return The child program items 305 */ 306 public List<ProgramItem> getChildProgramItems(ProgramItem programItem) 307 { 308 List<ProgramItem> children = new ArrayList<>(); 309 310 if (programItem instanceof TraversableProgramPart) 311 { 312 children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren()); 313 } 314 315 if (programItem instanceof CourseContainer) 316 { 317 children.addAll(((CourseContainer) programItem).getCourses()); 318 } 319 320 if (programItem instanceof Course) 321 { 322 children.addAll(((Course) programItem).getCourseLists()); 323 } 324 325 return children; 326 } 327 328 /** 329 * Get the child subprograms of a {@link ProgramPart} 330 * @param programPart The program part 331 * @return The child subprograms 332 */ 333 public Set<SubProgram> getChildSubPrograms(ProgramPart programPart) 334 { 335 Set<SubProgram> subPrograms = new HashSet<>(); 336 337 if (programPart instanceof TraversableProgramPart) 338 { 339 if (programPart instanceof SubProgram) 340 { 341 subPrograms.add((SubProgram) programPart); 342 } 343 ((TraversableProgramPart) programPart).getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child))); 344 } 345 346 return subPrograms; 347 } 348 349 /** 350 * Gets (recursively) parent containers of this program item. 351 * @param programItem The program item 352 * @return parent containers of this program item. 353 */ 354 public Set<Container> getParentContainers(ProgramItem programItem) 355 { 356 return _getParentsOfType(programItem, Container.class); 357 } 358 359 /** 360 * Gets (recursively) parent programs of this program item. 361 * @param programItem The program item 362 * @return parent programs of this program item. 363 */ 364 public Set<Program> getParentPrograms(ProgramItem programItem) 365 { 366 return _getParentsOfType(programItem, Program.class); 367 } 368 369 /** 370 * Gets (recursively) parent abstract programs of this program item. 371 * @param programItem The program item 372 * @return parent abstract programs of this program item. 373 */ 374 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem) 375 { 376 return _getParentsOfType(programItem, AbstractProgram.class); 377 } 378 379 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest) 380 { 381 Set<ProgramItem> visitedProgramItems = new HashSet<>(); 382 visitedProgramItems.add(programItem); 383 return _getParentsOfType(programItem, visitedProgramItems, classToTest); 384 } 385 386 @SuppressWarnings("unchecked") 387 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest) 388 { 389 Set<T> parentsOfType = new HashSet<>(); 390 List<ProgramItem> parents = getParentProgramItems(programItem); 391 392 for (ProgramItem parent : parents) 393 { 394 // Only parents not already visited 395 if (visitedProgramItems.add(parent)) 396 { 397 // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram) 398 if (classToTest.isInstance(parent)) 399 { 400 parentsOfType.add((T) parent); 401 } 402 else 403 { 404 parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest)); 405 } 406 } 407 } 408 409 return parentsOfType; 410 } 411 412 /** 413 * Get the parent program items of a {@link ProgramItem} 414 * @param programItem The program item 415 * @return The parent program items 416 */ 417 public List<ProgramItem> getParentProgramItems(ProgramItem programItem) 418 { 419 List<ProgramItem> parents = new ArrayList<>(); 420 421 if (programItem instanceof ProgramPart) 422 { 423 parents.addAll(((ProgramPart) programItem).getProgramPartParents()); 424 } 425 426 if (programItem instanceof CourseList) 427 { 428 parents.addAll(((CourseList) programItem).getParentCourses()); 429 } 430 431 if (programItem instanceof Course) 432 { 433 parents.addAll(((Course) programItem).getParentCourseLists()); 434 } 435 436 return parents; 437 } 438 439 /** 440 * Get the nearest program item parent into the given parent {@link AbstractProgram} 441 * @param programItem The program item 442 * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned. 443 * @return The parent program item or null if not found. 444 */ 445 public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram) 446 { 447 if (programItem instanceof Program) 448 { 449 return null; 450 } 451 452 if (programItem instanceof ProgramPart) 453 { 454 List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents(); 455 456 for (ProgramPart parent : parents) 457 { 458 if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram))) 459 { 460 return parent; 461 } 462 else 463 { 464 ProgramItem ancestor = getParentProgramItem(parent, parentProgram); 465 if (ancestor != null) 466 { 467 return parent; 468 } 469 } 470 } 471 } 472 473 if (programItem instanceof CourseList) 474 { 475 for (Course parentCourse : ((CourseList) programItem).getParentCourses()) 476 { 477 ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram); 478 if (ancestor != null) 479 { 480 return parentCourse; 481 } 482 } 483 } 484 485 if (programItem instanceof Course) 486 { 487 for (CourseList cl : ((Course) programItem).getParentCourseLists()) 488 { 489 ProgramItem ancestor = getParentProgramItem(cl, parentProgram); 490 if (ancestor != null) 491 { 492 return cl; 493 } 494 } 495 } 496 497 return null; 498 } 499 500 /** 501 * Get information of the program item structure (type, if program has children) 502 * @param programItemId the program item id 503 * @return a map of information 504 */ 505 @Callable 506 public Map<String, Object> getStructureInfo(String programItemId) 507 { 508 Map<String, Object> results = new HashMap<>(); 509 510 if (StringUtils.isNotBlank(programItemId)) 511 { 512 Content content = _resolver.resolveById(programItemId); 513 if (content instanceof ProgramItem) 514 { 515 List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content); 516 results.put("hasChildren", !childProgramItems.isEmpty()); 517 518 List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content); 519 results.put("hasParent", !parentProgramItems.isEmpty()); 520 521 results.put("paths", getPaths((ProgramItem) content, " > ")); 522 } 523 } 524 525 return results; 526 } 527 528 /** 529 * Get all the paths of a ODF content.<br> 530 * The path is construct with the contents' title 531 * @param separator The path separator 532 * @param item The program item 533 * @return the paths in parent program items 534 */ 535 protected List<String> getPaths (ProgramItem item, String separator) 536 { 537 List<String> paths = new ArrayList<>(); 538 539 String title = ((Content) item).getTitle(); 540 541 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 542 if (parentProgramItems.isEmpty()) 543 { 544 paths.add(title); 545 return paths; 546 } 547 548 for (ProgramItem parentProgramItem : parentProgramItems) 549 { 550 for (String path : getPaths(parentProgramItem, separator)) 551 { 552 paths.add(path + separator + title); 553 } 554 } 555 556 return paths; 557 } 558 559 /** 560 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 561 * The path is construct with the contents' names and the used separator is '/'. 562 * @param programItemId The id of the program item 563 * @param programId The id of program. Can not be null. 564 * @return the path into the parent program or null if the item is not part of this program. 565 */ 566 @Callable 567 public String getPathInProgram (String programItemId, String programId) 568 { 569 ProgramItem item = _resolver.resolveById(programItemId); 570 Program program = _resolver.resolveById(programId); 571 572 return getPathInProgram(item, program); 573 } 574 575 /** 576 * Get the path of a ODF content into a {@link Program}.<br> 577 * The path is construct with the contents' names and the used separator is '/'. 578 * @param item The program item 579 * @param parentProgram The parent root (sub)program. Can not be null. 580 * @return the path from the parent program 581 */ 582 public String getPathInProgram (ProgramItem item, Program parentProgram) 583 { 584 if (item instanceof Program) 585 { 586 // The program item is already the program it self or another program 587 return item.equals(parentProgram) ? "" : null; 588 } 589 590 List<String> paths = new ArrayList<>(); 591 paths.add(item.getName()); 592 593 ProgramItem parent = getParentProgramItem(item, parentProgram); 594 while (parent != null && !(parent instanceof Program)) 595 { 596 paths.add(parent.getName()); 597 parent = getParentProgramItem(parent, parentProgram); 598 } 599 600 if (parent != null) 601 { 602 paths.add(parent.getName()); 603 Collections.reverse(paths); 604 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 605 } 606 607 return null; 608 } 609 610 /** 611 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 612 * The path is construct with the contents' names and the used separator is '/'. 613 * @param contentId The id of the content 614 * @param parentCourseId The id of parent course. Can not be null. 615 * @return the path into the parent course or null if the item is not part of this course. 616 */ 617 @Callable 618 public String getPathInCourse (String contentId, String parentCourseId) 619 { 620 Content content = _resolver.resolveById(contentId); 621 Course parentCourse = _resolver.resolveById(parentCourseId); 622 623 return getPathInCourse(content, parentCourse); 624 } 625 626 /** 627 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 628 * The path is construct with the contents' names and the used separator is '/'. 629 * @param courseOrList The course or the course list 630 * @param parentCourse The parent course. Can not be null. 631 * @return the path into the parent course or null if the item is not part of this course. 632 */ 633 public String getPathInCourse(Content courseOrList, Course parentCourse) 634 { 635 if (courseOrList.equals(parentCourse)) 636 { 637 return ""; 638 } 639 640 String path = _getPathInCourse(courseOrList, parentCourse); 641 642 return path; 643 } 644 645 private String _getPathInCourse(Content content, Content parentContent) 646 { 647 if (content.equals(parentContent)) 648 { 649 return content.getName(); 650 } 651 652 List<? extends Content> parents; 653 654 if (content instanceof Course) 655 { 656 parents = ((Course) content).getParentCourseLists(); 657 } 658 else if (content instanceof CourseList) 659 { 660 parents = ((CourseList) content).getParentCourses(); 661 } 662 else 663 { 664 throw new IllegalStateException(); 665 } 666 667 for (Content parent : parents) 668 { 669 String path = _getPathInCourse(parent, parentContent); 670 if (path != null) 671 { 672 return path + '/' + content.getName(); 673 } 674 } 675 return null; 676 } 677 678 /** 679 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 680 * The path is construct with the contents' names and the used separator is '/'. 681 * @param orgUnitId The id of the orgunit 682 * @param rootOrgUnitId The root orgunit id 683 * @return the path into the parent program or null if the item is not part of this program. 684 */ 685 @Callable 686 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 687 { 688 OrgUnit rootOU = null; 689 if (StringUtils.isNotBlank(rootOrgUnitId)) 690 { 691 rootOU = _resolver.resolveById(rootOrgUnitId); 692 } 693 else 694 { 695 rootOU = _ouRootProvider.getRoot(); 696 } 697 698 if (orgUnitId.equals(rootOU.getId())) 699 { 700 // The orgunit is already the root orgunit 701 return rootOU.getName(); 702 } 703 704 OrgUnit ou = _resolver.resolveById(orgUnitId); 705 706 List<String> paths = new ArrayList<>(); 707 paths.add(ou.getName()); 708 709 OrgUnit parent = ou.getParentOrgUnit(); 710 while (parent != null && !parent.getId().equals(rootOU.getId())) 711 { 712 paths.add(parent.getName()); 713 parent = parent.getParentOrgUnit(); 714 } 715 716 if (parent != null) 717 { 718 paths.add(rootOU.getName()); 719 Collections.reverse(paths); 720 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 721 } 722 723 return null; 724 } 725 726 /** 727 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 728 * The path is construct with the contents' names and the used separator is '/'. 729 * @param orgUnitId The id of the orgunit 730 * @return the path into the parent program or null if the item is not part of this program. 731 */ 732 @Callable 733 public String getOrgUnitPath(String orgUnitId) 734 { 735 return getOrgUnitPath(orgUnitId, null); 736 } 737 738 /** 739 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 740 * @param part The program part 741 * @param parentId The ancestor id 742 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 743 */ 744 public boolean hasAncestor (ProgramPart part, String parentId) 745 { 746 List<ProgramPart> parents = part.getProgramPartParents(); 747 748 for (ProgramPart parent : parents) 749 { 750 if (parent.getId().equals(parentId)) 751 { 752 return true; 753 } 754 else if (hasAncestor(parent, parentId)) 755 { 756 return true; 757 } 758 } 759 760 return false; 761 } 762 763 /** 764 * Check if a relation can be establish between two ODF contents 765 * @param srcContent The source content (copied or moved) 766 * @param targetContent The target content 767 * @param errors The list of error messages 768 * @param contextualParameters the contextual parameters 769 * @return true if the relation is valid, false otherwise 770 */ 771 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 772 { 773 boolean isCompatible = true; 774 775 if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem) 776 { 777 if (!_isContentTypeCompatible(srcContent, targetContent)) 778 { 779 // Invalid relations between content types 780 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 781 isCompatible = false; 782 } 783 else if (!((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog())) 784 { 785 // Catalog is invalid 786 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 787 isCompatible = false; 788 } 789 else if (!srcContent.getLanguage().equals(targetContent.getLanguage())) 790 { 791 // Language is invalid 792 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 793 isCompatible = false; 794 } 795 else if (srcContent instanceof Course && targetContent instanceof CourseList && _shareableCourseHelper.handleShareableCourse() && !"create".equals(contextualParameters.get("mode"))) 796 { 797 if (!_shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent)) 798 { 799 // Shareable fields don't match 800 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 801 isCompatible = false; 802 } 803 } 804 } 805 else 806 { 807 // No program items 808 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 809 isCompatible = false; 810 } 811 812 return isCompatible; 813 } 814 815 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 816 { 817 if (srcContent instanceof Container) 818 { 819 return targetContent instanceof AbstractTraversableProgramPart; 820 } 821 else if (srcContent instanceof SubProgram) 822 { 823 return targetContent instanceof AbstractProgram; 824 } 825 else if (srcContent instanceof CourseList) 826 { 827 return targetContent instanceof CourseListContainer; 828 } 829 else if (srcContent instanceof Course) 830 { 831 return targetContent instanceof CourseList; 832 } 833 834 return false; 835 836 } 837 838 private List<String> _getContentParameters(Content srcContent, Content targetContent) 839 { 840 List<String> parameters = new ArrayList<>(); 841 parameters.add(srcContent.getTitle()); 842 parameters.add(srcContent.getId()); 843 parameters.add(targetContent.getTitle()); 844 parameters.add(targetContent.getId()); 845 return parameters; 846 } 847 /** 848 * Copy a {@link ProgramItem} 849 * @param srcContent The program item to copy 850 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 851 * @param fullCopy Set to <code>true</code> to copy the sub-structure 852 * @param copiedPrograms the id of initial programs with their copied content 853 * @param copiedSubPrograms the id of initial subprograms with their copied content 854 * @param copiedContainers the id of initial containers with their copied content 855 * @param copiedCourseLists the id of initial course lists with their copied content 856 * @param copiedCourses the id of initial courses with their copied content 857 * @param copiedCourseParts the id of initial course parts with their copied content 858 * @return The created content 859 * @param <C> The modifiable content return type 860 * @throws AmetysRepositoryException If an error occurred during copy 861 * @throws WorkflowException If an error occurred during copy 862 */ 863 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 864 { 865 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 866 } 867 868 /** 869 * Copy a {@link ProgramItem} 870 * @param srcContent The program item to copy 871 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 872 * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object. 873 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 874 * @param fullCopy Set to <code>true</code> to copy the sub-structure 875 * @param copiedPrograms the id of initial programs with their copied content 876 * @param copiedSubPrograms the id of initial subprograms with their copied content 877 * @param copiedContainers the id of initial containers with their copied content 878 * @param copiedCourseLists the id of initial course lists with their copied content 879 * @param copiedCourses the id of initial courses with their copied content 880 * @param copiedCourseParts the id of initial course parts with their copied content 881 * @param <C> The modifiable content return type 882 * @return The created content 883 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 884 * @throws AmetysRepositoryException If an error occurred 885 * @throws WorkflowException If an error occurred 886 */ 887 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 888 { 889 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 890 } 891 892 /** 893 * Copy a {@link CoursePart} 894 * @param srcContent The course part to copy 895 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 896 * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object. 897 * @param initWorkflowActionId The initial workflow action id 898 * @param fullCopy Set to <code>true</code> to copy the sub-structure 899 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 900 * @param copiedPrograms the id of initial programs with their copied content 901 * @param copiedSubPrograms the id of initial subprograms with their copied content 902 * @param copiedContainers the id of initial containers with their copied content 903 * @param copiedCourseLists the id of initial course lists with their copied content 904 * @param copiedCourses the id of initial courses with their copied content 905 * @param copiedCourseParts the id of initial course parts with their copied content 906 * @param <C> The modifiable content return type 907 * @return The created content 908 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 909 * @throws AmetysRepositoryException If an error occurred 910 * @throws WorkflowException If an error occurred 911 */ 912 public <C extends ModifiableContent> C copyCoursePart(CoursePart srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 913 { 914 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 915 } 916 917 /** 918 * Copy a {@link ProgramItem} 919 * @param srcContent The program item to copy 920 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 921 * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object. 922 * @param initWorkflowActionId The initial workflow action id 923 * @param fullCopy Set to <code>true</code> to copy the sub-structure 924 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 925 * @param copiedPrograms the id of initial programs with their copied content 926 * @param copiedSubPrograms the id of initial subprograms with their copied content 927 * @param copiedContainers the id of initial containers with their copied content 928 * @param copiedCourseLists the id of initial course lists with their copied content 929 * @param copiedCourses the id of initial courses with their copied content 930 * @param copiedCourseParts the id of initial course parts with their copied content 931 * @param <C> The modifiable content return type 932 * @return The created content 933 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 934 * @throws AmetysRepositoryException If an error occurred 935 * @throws WorkflowException If an error occurred 936 */ 937 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 938 { 939 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 940 } 941 942 /** 943 * Copy a {@link ProgramItem} 944 * @param srcContent The program item to copy 945 * @param catalog The catalog 946 * @param code The odf content code 947 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 948 * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object. 949 * @param initWorkflowActionId The initial workflow action id 950 * @param fullCopy Set to <code>true</code> to copy the sub-structure 951 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 952 * @param copiedPrograms the id of initial programs with their copied content 953 * @param copiedSubPrograms the id of initial subprograms with their copied content 954 * @param copiedContainers the id of initial containers with their copied content 955 * @param copiedCourseLists the id of initial course lists with their copied content 956 * @param copiedCourses the id of initial courses with their copied content 957 * @param copiedCourseParts the id of initial course parts with their copied content 958 * @param <C> The modifiable content return type 959 * @return The created content 960 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 961 * @throws AmetysRepositoryException If an error occurred 962 * @throws WorkflowException If an error occurred 963 */ 964 @SuppressWarnings("unchecked") 965 private <C extends ModifiableContent> C _copyODFContent(Content srcContent, String catalog, String code, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 966 { 967 String computedTargetLanguage = targetContentLanguage; 968 if (computedTargetLanguage == null) 969 { 970 computedTargetLanguage = srcContent.getLanguage(); 971 } 972 973 String computeTargetName = targetContentName; 974 if (computeTargetName == null) 975 { 976 // Compute content name from source content and requested language 977 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 978 } 979 980 String computeTargetCatalog = targetCatalog; 981 if (computeTargetCatalog == null) 982 { 983 computeTargetCatalog = catalog; 984 } 985 986 String principalContentType = srcContent.getTypes()[0]; 987 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 988 if (createdContent != null) 989 { 990 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 991 } 992 else 993 { 994 // Copy content waiting for observers to be completed and copying ACL 995 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true); 996 997 if (fullCopy) 998 { 999 _cleanContentMetadata(createdContent); 1000 1001 if (targetCatalog != null) 1002 { 1003 boolean hasChanges = false; 1004 if (createdContent instanceof ProgramItem) 1005 { 1006 ((ProgramItem) createdContent).setCatalog(targetCatalog); 1007 hasChanges = true; 1008 } 1009 else if (createdContent instanceof CoursePart) 1010 { 1011 ((CoursePart) createdContent).setCatalog(targetCatalog); 1012 hasChanges = true; 1013 } 1014 1015 if (hasChanges) 1016 { 1017 createdContent.saveChanges(); 1018 } 1019 } 1020 1021 if (srcContent instanceof ProgramItem) 1022 { 1023 copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1024 } 1025 } 1026 1027 _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1028 } 1029 1030 return (C) createdContent; 1031 } 1032 1033 private void _putInCopiedMap(Content srcContent, ModifiableContent createdContent, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) 1034 { 1035 if (createdContent instanceof Program) 1036 { 1037 copiedPrograms.put(srcContent.getId(), createdContent.getId()); 1038 } 1039 else if (createdContent instanceof SubProgram) 1040 { 1041 copiedSubPrograms.put(srcContent.getId(), createdContent.getId()); 1042 } 1043 else if (createdContent instanceof Container) 1044 { 1045 copiedContainers.put(srcContent.getId(), createdContent.getId()); 1046 } 1047 else if (createdContent instanceof CourseList) 1048 { 1049 copiedCourseLists.put(srcContent.getId(), createdContent.getId()); 1050 } 1051 else if (createdContent instanceof Course) 1052 { 1053 copiedCourses.put(srcContent.getId(), createdContent.getId()); 1054 } 1055 else if (createdContent instanceof CoursePart) 1056 { 1057 copiedCourseParts.put(srcContent.getId(), createdContent.getId()); 1058 } 1059 } 1060 1061 /** 1062 * Copy the structure of a {@link ProgramItem} 1063 * @param srcContent the content to copy 1064 * @param targetContent the target content 1065 * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object. 1066 * @param initWorkflowActionId The initial workflow action id 1067 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1068 * @param copiedPrograms the id of initial programs with their copied content 1069 * @param copiedSubPrograms the id of initial subprograms with their copied content 1070 * @param copiedContainers the id of initial containers with their copied content 1071 * @param copiedCourseLists the id of initial course lists with their copied content 1072 * @param copiedCourses the id of initial courses with their copied content 1073 * @param copiedCourseParts the id of initial course parts with their copied content 1074 * @throws AmetysRepositoryException If an error occurred during copy 1075 * @throws WorkflowException If an error occurred during copy 1076 */ 1077 protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1078 { 1079 List<ProgramItem> srcChildContents = new ArrayList<>(); 1080 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1081 1082 String childMetadataPath = null; 1083 String parentMetadataPath = null; 1084 1085 if (srcContent instanceof TraversableProgramPart) 1086 { 1087 childMetadataPath = TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS; 1088 parentMetadataPath = ProgramPart.METADATA_PARENT_PROGRAM_PARTS; 1089 srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren()); 1090 } 1091 else if (srcContent instanceof CourseList) 1092 { 1093 childMetadataPath = CourseList.METADATA_CHILD_COURSES; 1094 parentMetadataPath = Course.METADATA_PARENT_COURSE_LISTS; 1095 srcChildContents.addAll(((CourseList) srcContent).getCourses()); 1096 } 1097 else if (srcContent instanceof Course) 1098 { 1099 childMetadataPath = Course.METADATA_CHILD_COURSE_LISTS; 1100 parentMetadataPath = CourseList.METADATA_PARENT_COURSES; 1101 srcChildContents.addAll(((Course) srcContent).getCourseLists()); 1102 1103 List<String> refCoursePartIds = new ArrayList<>(); 1104 for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts()) 1105 { 1106 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1107 refCoursePartIds.add(targetChildContent.getId()); 1108 } 1109 _addFormValues(values, Course.METADATA_CHILD_COURSE_PARTS, CoursePart.METADATA_PARENT_COURSES, refCoursePartIds); 1110 } 1111 1112 List<String> refChildIds = new ArrayList<>(); 1113 for (ProgramItem srcChildContent : srcChildContents) 1114 { 1115 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1116 refChildIds.add(targetChildContent.getId()); 1117 } 1118 1119 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1120 1121 _editChildRelation((WorkflowAwareContent) targetContent, values); 1122 1123 } 1124 1125 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1126 { 1127 if (!refChildIds.isEmpty()) 1128 { 1129 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1130 } 1131 } 1132 1133 private void _editChildRelation(WorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1134 { 1135 if (!values.isEmpty()) 1136 { 1137 ModifiableCompositeMetadata holder = parentContent.getMetadataHolder(); 1138 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1139 { 1140 String childMetadataName = entry.getKey().getLeft(); 1141 String parentMetadataName = entry.getKey().getRight(); 1142 List<String> childContents = entry.getValue(); 1143 1144 holder.setMetadata(childMetadataName, childContents.toArray(new String[childContents.size()])); 1145 1146 for (String childContentId : childContents) 1147 { 1148 ModifiableContent content = _resolver.resolveById(childContentId); 1149 ModifiableCompositeMetadata childHolder = content.getMetadataHolder(); 1150 String[] parentContents = childHolder.getStringArray(parentMetadataName, ArrayUtils.EMPTY_STRING_ARRAY); 1151 childHolder.setMetadata(parentMetadataName, ArrayUtils.add(parentContents, parentContent.getId())); 1152 } 1153 } 1154 } 1155 } 1156 1157 /** 1158 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1159 * @param createdContent The created content to clean 1160 */ 1161 protected void _cleanContentMetadata(ModifiableContent createdContent) 1162 { 1163 ModifiableCompositeMetadata metadataHolder = createdContent.getMetadataHolder(); 1164 if (createdContent instanceof ProgramPart) 1165 { 1166 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, ProgramPart.METADATA_PARENT_PROGRAM_PARTS); 1167 } 1168 1169 if (createdContent instanceof TraversableProgramPart) 1170 { 1171 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS); 1172 } 1173 1174 if (createdContent instanceof CourseList) 1175 { 1176 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_CHILD_COURSES); 1177 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_PARENT_COURSES); 1178 } 1179 1180 if (createdContent instanceof Course) 1181 { 1182 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_CHILD_COURSE_LISTS); 1183 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_PARENT_COURSE_LISTS); 1184 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_CHILD_COURSE_PARTS); 1185 } 1186 1187 if (createdContent instanceof CoursePart) 1188 { 1189 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CoursePart.METADATA_PARENT_COURSES); 1190 } 1191 } 1192}