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; 031 032import org.ametys.cms.ObservationConstants; 033import org.ametys.cms.content.external.ExternalizableMetadataHelper; 034import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 035import org.ametys.cms.form.AbstractField.MODE; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.ContentQueryHelper; 038import org.ametys.cms.repository.ContentTypeExpression; 039import org.ametys.cms.repository.DefaultContent; 040import org.ametys.cms.repository.LanguageExpression; 041import org.ametys.cms.repository.ModifiableContent; 042import org.ametys.cms.repository.WorkflowAwareContent; 043import org.ametys.cms.workflow.ContentWorkflowHelper; 044import org.ametys.cms.workflow.EditContentFunction; 045import org.ametys.core.observation.Event; 046import org.ametys.core.observation.ObservationManager; 047import org.ametys.core.ui.Callable; 048import org.ametys.core.user.CurrentUserProvider; 049import org.ametys.odf.course.Course; 050import org.ametys.odf.course.CourseContainer; 051import org.ametys.odf.courselist.CourseList; 052import org.ametys.odf.orgunit.OrgUnit; 053import org.ametys.odf.orgunit.RootOrgUnitProvider; 054import org.ametys.odf.program.AbstractProgram; 055import org.ametys.odf.program.Container; 056import org.ametys.odf.program.Program; 057import org.ametys.odf.program.ProgramPart; 058import org.ametys.odf.program.SubProgram; 059import org.ametys.odf.program.TraversableProgramPart; 060import org.ametys.plugins.repository.AmetysObjectExistsException; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.plugins.repository.AmetysObjectIterator; 063import org.ametys.plugins.repository.AmetysObjectResolver; 064import org.ametys.plugins.repository.AmetysRepositoryException; 065import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 066import org.ametys.plugins.repository.RepositoryConstants; 067import org.ametys.plugins.repository.UnknownAmetysObjectException; 068import org.ametys.plugins.repository.collection.AmetysObjectCollection; 069import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 070import org.ametys.plugins.repository.query.SortCriteria; 071import org.ametys.plugins.repository.query.expression.AndExpression; 072import org.ametys.plugins.repository.query.expression.Expression; 073import org.ametys.plugins.repository.query.expression.Expression.Operator; 074import org.ametys.plugins.repository.query.expression.StringExpression; 075import org.ametys.plugins.workflow.AbstractWorkflowComponent; 076import org.ametys.runtime.plugin.component.AbstractLogEnabled; 077import org.ametys.runtime.plugin.component.PluginAware; 078 079import com.opensymphony.workflow.WorkflowException; 080 081/** 082 * Helper for ODF contents 083 * 084 */ 085public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware 086{ 087 /** The component role. */ 088 public static final String ROLE = ODFHelper.class.getName(); 089 090 /** The default id of initial workflow action */ 091 protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0; 092 /** The default id of edit workflow action */ 093 protected static final int __EDIT_WORKFLOW_ACTION_ID = 2; 094 095 /** Ametys object resolver */ 096 protected AmetysObjectResolver _resolver; 097 /** The content workflow helper */ 098 protected ContentWorkflowHelper _contentWorkflowHelper; 099 /** The content types manager */ 100 protected ContentTypeExtensionPoint _cTypeEP; 101 /** The observation manager */ 102 protected ObservationManager _observationManager; 103 /** The current user provider */ 104 protected CurrentUserProvider _currentUserProvider; 105 /** Root orgunit */ 106 protected RootOrgUnitProvider _ouRootProvider; 107 108 private String _pluginName; 109 110 @Override 111 public void service(ServiceManager manager) throws ServiceException 112 { 113 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 114 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 115 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 116 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 117 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 118 _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 119 } 120 121 @Override 122 public void setPluginInfo(String pluginName, String featureName, String id) 123 { 124 _pluginName = pluginName; 125 } 126 127 /** 128 * Gets the root for ODF contents 129 * @return the root for ODF contents 130 */ 131 public AmetysObjectCollection getRootContent() 132 { 133 return getRootContent(false); 134 } 135 136 /** 137 * Gets the root for ODF contents 138 * @param create <code>true</code> to create automatically the root when missing. 139 * @return the root for ODF contents 140 */ 141 public AmetysObjectCollection getRootContent(boolean create) 142 { 143 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/"); 144 145 boolean needSave = false; 146 if (!pluginsNode.hasChild(_pluginName)) 147 { 148 if (create) 149 { 150 pluginsNode.createChild(_pluginName, "ametys:unstructured"); 151 needSave = true; 152 } 153 else 154 { 155 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing"); 156 } 157 } 158 159 ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName); 160 if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents")) 161 { 162 if (create) 163 { 164 pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection"); 165 needSave = true; 166 } 167 else 168 { 169 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing"); 170 } 171 } 172 173 if (needSave) 174 { 175 pluginsNode.saveChanges(); 176 } 177 178 return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 179 } 180 181 /** 182 * Get the {@link ProgramItem}s matching the given arguments 183 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 184 * @param code The code. Can be null to get program's items regardless of their code 185 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 186 * @param lang The search language. Can be null to get program's items regardless of their language 187 * @param <C> The content return type 188 * @return The matching program items 189 */ 190 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang) 191 { 192 return getProgramItems(cTypeId, code, catalogName, lang, null, null); 193 } 194 195 /** 196 * Get the {@link ProgramItem}s matching the given arguments 197 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 198 * @param code The code. Can be null to get program's items regardless of their code 199 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 200 * @param lang The search language. Can be null to get program's items regardless of their language 201 * @param additionnalExpr An additional expression for filtering result. Can be null 202 * @param sortCriteria criteria for sorting results 203 * @param <C> The content return type 204 * @return The matching program items 205 */ 206 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 207 { 208 List<Expression> exprs = new ArrayList<>(); 209 210 if (StringUtils.isNotEmpty(cTypeId)) 211 { 212 exprs.add(new ContentTypeExpression(Operator.EQ, cTypeId)); 213 } 214 if (StringUtils.isNotEmpty(code)) 215 { 216 exprs.add(new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, code)); 217 } 218 if (StringUtils.isNotEmpty(catalogName)) 219 { 220 exprs.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName)); 221 } 222 if (StringUtils.isNotEmpty(lang)) 223 { 224 exprs.add(new LanguageExpression(Operator.EQ, lang)); 225 } 226 if (additionnalExpr != null) 227 { 228 exprs.add(additionnalExpr); 229 } 230 231 Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 232 233 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria); 234 return _resolver.query(xpathQuery); 235 } 236 237 /** 238 * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 239 * @param srcProgramItem The source program item 240 * @param catalogName The name of catalog to search into 241 * @param lang The search language 242 * @return The equivalent program item or <code>null</code> if not exists 243 */ 244 public Content getProgramItem(ProgramItem srcProgramItem, String catalogName, String lang) 245 { 246 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 247 Expression catalogExpr = new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName); 248 Expression codeExpr = new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, srcProgramItem.getCode()); 249 250 Expression expr = new AndExpression(langExpr, catalogExpr, codeExpr); 251 252 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 253 AmetysObjectIterable<Content> contents = _resolver.query(xpathQuery); 254 AmetysObjectIterator<Content> contentsIt = contents.iterator(); 255 if (contentsIt.hasNext()) 256 { 257 return contentsIt.next(); 258 } 259 260 return null; 261 } 262 263 /** 264 * Get the child program items of a {@link ProgramItem} 265 * @param programItem The program item 266 * @return The child program items 267 */ 268 public List<ProgramItem> getChildProgramItems(ProgramItem programItem) 269 { 270 List<ProgramItem> children = new ArrayList<>(); 271 272 if (programItem instanceof TraversableProgramPart) 273 { 274 children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren()); 275 } 276 277 if (programItem instanceof CourseContainer) 278 { 279 children.addAll(((CourseContainer) programItem).getCourses()); 280 } 281 282 if (programItem instanceof Course) 283 { 284 children.addAll(((Course) programItem).getCourseLists()); 285 } 286 287 return children; 288 } 289 290 /** 291 * Gets (recursively) parent abstract programs of this program item. 292 * @param programItem The program item 293 * @return parent abstract programs of this program item. 294 */ 295 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem) 296 { 297 Set<ProgramItem> visitedProgramItems = new HashSet<>(); 298 visitedProgramItems.add(programItem); 299 return _getParentAbstractPrograms(programItem, visitedProgramItems); 300 } 301 302 private Set<AbstractProgram> _getParentAbstractPrograms(ProgramItem programItem, Set<ProgramItem> visitedProgramItems) 303 { 304 Set<AbstractProgram> parentAbstractPrograms = new HashSet<>(); 305 List<ProgramItem> parents = getParentProgramItems(programItem); 306 307 for (ProgramItem parent : parents) 308 { 309 // Only parents not already visited 310 if (visitedProgramItems.add(parent)) 311 { 312 if (parent instanceof AbstractProgram) 313 { 314 parentAbstractPrograms.add((AbstractProgram) parent); 315 } 316 else 317 { 318 parentAbstractPrograms.addAll(_getParentAbstractPrograms(parent, visitedProgramItems)); 319 } 320 } 321 } 322 323 return parentAbstractPrograms; 324 } 325 326 /** 327 * Get the parent program items of a {@link ProgramItem} 328 * @param programItem The program item 329 * @return The parent program items 330 */ 331 public List<ProgramItem> getParentProgramItems(ProgramItem programItem) 332 { 333 List<ProgramItem> parents = new ArrayList<>(); 334 335 if (programItem instanceof ProgramPart) 336 { 337 parents.addAll(((ProgramPart) programItem).getProgramPartParents()); 338 } 339 340 if (programItem instanceof CourseList) 341 { 342 parents.addAll(((CourseList) programItem).getParentCourses()); 343 } 344 345 if (programItem instanceof Course) 346 { 347 parents.addAll(((Course) programItem).getParentCourseLists()); 348 } 349 350 return parents; 351 } 352 353 /** 354 * Get the nearest program item parent into the given parent {@link AbstractProgram} 355 * @param programItem The program item 356 * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned. 357 * @return The parent program item or null if not found. 358 */ 359 public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram) 360 { 361 if (programItem instanceof Program) 362 { 363 return null; 364 } 365 366 if (programItem instanceof ProgramPart) 367 { 368 List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents(); 369 370 for (ProgramPart parent : parents) 371 { 372 if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram))) 373 { 374 return parent; 375 } 376 else 377 { 378 ProgramItem ancestor = getParentProgramItem(parent, parentProgram); 379 if (ancestor != null) 380 { 381 return parent; 382 } 383 } 384 } 385 } 386 387 if (programItem instanceof CourseList) 388 { 389 for (Course parentCourse : ((CourseList) programItem).getParentCourses()) 390 { 391 ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram); 392 if (ancestor != null) 393 { 394 return parentCourse; 395 } 396 } 397 } 398 399 if (programItem instanceof Course) 400 { 401 for (CourseList cl : ((Course) programItem).getParentCourseLists()) 402 { 403 ProgramItem ancestor = getParentProgramItem(cl, parentProgram); 404 if (ancestor != null) 405 { 406 return cl; 407 } 408 } 409 } 410 411 return null; 412 } 413 414 /** 415 * Get information of the program item structure (type, if program has children) 416 * @param programItemId the program item id 417 * @return a map of information 418 */ 419 @Callable 420 public Map<String, Object> getStructureInfo(String programItemId) 421 { 422 Map<String, Object> results = new HashMap<>(); 423 424 if (StringUtils.isNotBlank(programItemId)) 425 { 426 Content content = _resolver.resolveById(programItemId); 427 if (content instanceof ProgramItem) 428 { 429 List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content); 430 results.put("hasChildren", !childProgramItems.isEmpty()); 431 432 List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content); 433 results.put("hasParent", !parentProgramItems.isEmpty()); 434 435 results.put("paths", getPaths((ProgramItem) content, " > ")); 436 } 437 } 438 439 return results; 440 } 441 442 /** 443 * Get all the paths of a ODF content.<br> 444 * The path is construct with the contents' title 445 * @param separator The path separator 446 * @param item The program item 447 * @return the paths in parent program items 448 */ 449 protected List<String> getPaths (ProgramItem item, String separator) 450 { 451 List<String> paths = new ArrayList<>(); 452 453 String title = ((Content) item).getTitle(); 454 455 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 456 if (parentProgramItems.isEmpty()) 457 { 458 paths.add(title); 459 return paths; 460 } 461 462 for (ProgramItem parentProgramItem : parentProgramItems) 463 { 464 for (String path : getPaths(parentProgramItem, separator)) 465 { 466 paths.add(path + separator + title); 467 } 468 } 469 470 return paths; 471 } 472 473 /** 474 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 475 * The path is construct with the contents' names and the used separator is '/'. 476 * @param programItemId The id of the program item 477 * @param programId The id of program. Can not be null. 478 * @return the path into the parent program or null if the item is not part of this program. 479 */ 480 @Callable 481 public String getPathInProgram (String programItemId, String programId) 482 { 483 ProgramItem item = _resolver.resolveById(programItemId); 484 Program program = _resolver.resolveById(programId); 485 486 return getPathInProgram(item, program); 487 } 488 489 /** 490 * Get the path of a ODF content into a {@link Program}.<br> 491 * The path is construct with the contents' names and the used separator is '/'. 492 * @param item The program item 493 * @param parentProgram The parent root (sub)program. Can not be null. 494 * @return the path from the parent program 495 */ 496 public String getPathInProgram (ProgramItem item, Program parentProgram) 497 { 498 if (item instanceof Program) 499 { 500 // The program item is already the program it self or another program 501 return item.equals(parentProgram) ? "" : null; 502 } 503 504 List<String> paths = new ArrayList<>(); 505 paths.add(item.getName()); 506 507 ProgramItem parent = getParentProgramItem(item, parentProgram); 508 while (parent != null && !(parent instanceof Program)) 509 { 510 paths.add(parent.getName()); 511 parent = getParentProgramItem(parent, parentProgram); 512 } 513 514 if (parent != null) 515 { 516 paths.add(parent.getName()); 517 Collections.reverse(paths); 518 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 519 } 520 521 return null; 522 } 523 524 /** 525 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 526 * The path is construct with the contents' names and the used separator is '/'. 527 * @param contentId The id of the content 528 * @param parentCourseId The id of parent course. Can not be null. 529 * @return the path into the parent course or null if the item is not part of this course. 530 */ 531 @Callable 532 public String getPathInCourse (String contentId, String parentCourseId) 533 { 534 Content content = _resolver.resolveById(contentId); 535 Course parentCourse = _resolver.resolveById(parentCourseId); 536 537 return getPathInCourse(content, parentCourse); 538 } 539 540 /** 541 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 542 * The path is construct with the contents' names and the used separator is '/'. 543 * @param courseOrList The course or the course list 544 * @param parentCourse The parent course. Can not be null. 545 * @return the path into the parent course or null if the item is not part of this course. 546 */ 547 public String getPathInCourse(Content courseOrList, Course parentCourse) 548 { 549 if (courseOrList.equals(parentCourse)) 550 { 551 return ""; 552 } 553 554 String path = _getPathInCourse(courseOrList, parentCourse); 555 556 return path; 557 } 558 559 private String _getPathInCourse(Content content, Content parentContent) 560 { 561 if (content.equals(parentContent)) 562 { 563 return content.getName(); 564 } 565 566 List<? extends Content> parents; 567 568 if (content instanceof Course) 569 { 570 parents = ((Course) content).getParentCourseLists(); 571 } 572 else if (content instanceof CourseList) 573 { 574 parents = ((CourseList) content).getParentCourses(); 575 } 576 else 577 { 578 throw new IllegalStateException(); 579 } 580 581 for (Content parent : parents) 582 { 583 String path = _getPathInCourse(parent, parentContent); 584 if (path != null) 585 { 586 return path + '/' + content.getName(); 587 } 588 } 589 return null; 590 } 591 592 /** 593 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 594 * The path is construct with the contents' names and the used separator is '/'. 595 * @param orgUnitId The id of the orgunit 596 * @param rootOrgUnitId The root orgunit id 597 * @return the path into the parent program or null if the item is not part of this program. 598 */ 599 @Callable 600 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 601 { 602 OrgUnit rootOU = null; 603 if (StringUtils.isNotBlank(rootOrgUnitId)) 604 { 605 rootOU = _resolver.resolveById(rootOrgUnitId); 606 } 607 else 608 { 609 rootOU = _ouRootProvider.getRoot(); 610 } 611 612 if (orgUnitId.equals(rootOU.getId())) 613 { 614 // The orgunit is already the root orgunit 615 return rootOU.getName(); 616 } 617 618 OrgUnit ou = _resolver.resolveById(orgUnitId); 619 620 List<String> paths = new ArrayList<>(); 621 paths.add(ou.getName()); 622 623 OrgUnit parent = ou.getParentOrgUnit(); 624 while (parent != null && !parent.getId().equals(rootOU.getId())) 625 { 626 paths.add(parent.getName()); 627 parent = parent.getParentOrgUnit(); 628 } 629 630 if (parent != null) 631 { 632 paths.add(rootOU.getName()); 633 Collections.reverse(paths); 634 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 635 } 636 637 return null; 638 } 639 640 /** 641 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 642 * The path is construct with the contents' names and the used separator is '/'. 643 * @param orgUnitId The id of the orgunit 644 * @return the path into the parent program or null if the item is not part of this program. 645 */ 646 @Callable 647 public String getOrgUnitPath(String orgUnitId) 648 { 649 return getOrgUnitPath(orgUnitId, null); 650 } 651 652 /** 653 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 654 * @param part The program part 655 * @param parentId The ancestor id 656 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 657 */ 658 public boolean hasAncestor (ProgramPart part, String parentId) 659 { 660 List<ProgramPart> parents = part.getProgramPartParents(); 661 662 for (ProgramPart parent : parents) 663 { 664 if (parent.getId().equals(parentId)) 665 { 666 return true; 667 } 668 else if (hasAncestor(parent, parentId)) 669 { 670 return true; 671 } 672 } 673 674 return false; 675 } 676 677 /** 678 * Copy a {@link ProgramItem} 679 * @param srcContent The program item to copy 680 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 681 * @param fullCopy Set to <code>true</code> to copy the sub-structure 682 * @param copiedPrograms the id of initial programs with their copied content 683 * @param copiedSubPrograms the id of initial subprograms with their copied content 684 * @param copiedContainers the id of initial containers with their copied content 685 * @param copiedCourseLists the id of initial course lists with their copied content 686 * @param copiedCourses the id of initial courses with their copied content 687 * @return The created content 688 * @param <C> The modifiable content return type 689 * @throws AmetysRepositoryException If an error occurred during copy 690 * @throws WorkflowException If an error occurred during copy 691 */ 692 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) throws AmetysRepositoryException, WorkflowException 693 { 694 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, __EDIT_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses); 695 } 696 697 /** 698 * Copy a {@link ProgramItem} 699 * @param srcContent The program item to copy 700 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 701 * @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. 702 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 703 * @param fullCopy Set to <code>true</code> to copy the sub-structure 704 * @param copiedPrograms the id of initial programs with their copied content 705 * @param copiedSubPrograms the id of initial subprograms with their copied content 706 * @param copiedContainers the id of initial containers with their copied content 707 * @param copiedCourseLists the id of initial course lists with their copied content 708 * @param copiedCourses the id of initial courses with their copied content 709 * @param <C> The modifiable content return type 710 * @return The created content 711 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 712 * @throws AmetysRepositoryException If an error occurred 713 * @throws WorkflowException If an error occurred 714 */ 715 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) throws AmetysRepositoryException, WorkflowException 716 { 717 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, __EDIT_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses); 718 } 719 720 721 /** 722 * Copy a {@link ProgramItem} 723 * @param srcContent The program item to copy 724 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 725 * @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. 726 * @param initWorkflowActionId The initial workflow action id 727 * @param editWorkflowActionId The workflow action id to edit the relationship 728 * @param fullCopy Set to <code>true</code> to copy the sub-structure 729 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 730 * @param copiedPrograms the id of initial programs with their copied content 731 * @param copiedSubPrograms the id of initial subprograms with their copied content 732 * @param copiedContainers the id of initial containers with their copied content 733 * @param copiedCourseLists the id of initial course lists with their copied content 734 * @param copiedCourses the id of initial courses with their copied content 735 * @param <C> The modifiable content return type 736 * @return The created content 737 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 738 * @throws AmetysRepositoryException If an error occurred 739 * @throws WorkflowException If an error occurred 740 */ 741 @SuppressWarnings("unchecked") 742 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, int editWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses) throws AmetysRepositoryException, WorkflowException 743 { 744 String computedTargetLanguage = targetContentLanguage; 745 if (computedTargetLanguage == null) 746 { 747 computedTargetLanguage = ((Content) srcContent).getLanguage(); 748 } 749 750 String computeTargetName = targetContentName; 751 if (computeTargetName == null) 752 { 753 // Compute content name from source content and requested language 754 computeTargetName = ((Content) srcContent).getName() + (targetContentLanguage != null && !targetContentLanguage.equals(((Content) srcContent).getName()) ? "-" + targetContentLanguage : ""); 755 } 756 757 String computeTargetCatalog = targetCatalog; 758 if (computeTargetCatalog == null) 759 { 760 computeTargetCatalog = srcContent.getCatalog(); 761 } 762 763 if (getProgramItem(srcContent, computeTargetCatalog, computedTargetLanguage) != null) 764 { 765 throw new AmetysObjectExistsException("A program item already exists with same code, catalog and language [" + srcContent.getCode() + ", " + computeTargetCatalog + ", " + targetContentLanguage + "]"); 766 } 767 768 // Copy content waiting for observers to be completed and copying ACL 769 ModifiableContent createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, true, true); 770 String createdContentId = createdContent.getId(); 771 772 if (fullCopy) 773 { 774 _cleanContentMetadata(createdContent); 775 776 if (targetCatalog != null && createdContent instanceof ProgramItem) 777 { 778 ((ProgramItem) createdContent).setCatalog(targetCatalog); 779 createdContent.saveChanges(); 780 // Content is modified as we changed the value of its catalog, notify it 781 Map<String, Object> eventParams = new HashMap<>(); 782 eventParams.put(ObservationConstants.ARGS_CONTENT, createdContent); 783 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, createdContentId); 784 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 785 } 786 787 copyProgramItemStructure(srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, editWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses); 788 } 789 790 if (createdContent instanceof Program) 791 { 792 copiedPrograms.put(((Content) srcContent).getId(), createdContentId); 793 } 794 else if (createdContent instanceof SubProgram) 795 { 796 copiedSubPrograms.put(((Content) srcContent).getId(), createdContentId); 797 } 798 else if (createdContent instanceof Container) 799 { 800 copiedContainers.put(((Content) srcContent).getId(), createdContentId); 801 } 802 else if (createdContent instanceof CourseList) 803 { 804 copiedCourseLists.put(((Content) srcContent).getId(), createdContentId); 805 } 806 else if (createdContent instanceof Course) 807 { 808 copiedCourses.put(((Content) srcContent).getId(), createdContentId); 809 } 810 811 return (C) createdContent; 812 } 813 814 /** 815 * Copy the structure of a {@link ProgramItem} 816 * @param srcContent the content to copy 817 * @param targetContent the target content 818 * @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. 819 * @param initWorkflowActionId The initial workflow action id 820 * @param editWorkflowActionId The workflow action id to edit the relationship 821 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 822 * @param copiedPrograms the id of initial programs with their copied content 823 * @param copiedSubPrograms the id of initial subprograms with their copied content 824 * @param copiedContainers the id of initial containers with their copied content 825 * @param copiedCourseLists the id of initial course lists with their copied content 826 * @param copiedCourses the id of initial courses with their copied content 827 * @throws AmetysRepositoryException If an error occurred during copy 828 * @throws WorkflowException If an error occurred during copy 829 */ 830 protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, int editWorkflowActionId, String targetCatalogName, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses) throws AmetysRepositoryException, WorkflowException 831 { 832 List<String> refChildIds = new ArrayList<>(); 833 834 List<ProgramItem> srcChildContents = new ArrayList<>(); 835 836 String childMetadataPath = null; 837 838 if (srcContent instanceof TraversableProgramPart) 839 { 840 childMetadataPath = TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS; 841 srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren()); 842 } 843 else if (srcContent instanceof CourseList) 844 { 845 childMetadataPath = CourseList.METADATA_CHILD_COURSES; 846 srcChildContents.addAll(((CourseList) srcContent).getCourses()); 847 } 848 else if (srcContent instanceof Course) 849 { 850 childMetadataPath = Course.METADATA_CHILD_COURSE_LISTS; 851 srcChildContents.addAll(((Course) srcContent).getCourseLists()); 852 } 853 854 for (ProgramItem srcChildContent : srcChildContents) 855 { 856 Content targetChildContent = getProgramItem(srcChildContent, targetCatalogName, targetContentLanguage); 857 if (targetChildContent == null) 858 { 859 targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, editWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses); 860 } 861 refChildIds.add(targetChildContent.getId()); 862 } 863 864 _editChildRelation((WorkflowAwareContent) targetContent, refChildIds, editWorkflowActionId, childMetadataPath); 865 } 866 867 private void _editChildRelation(WorkflowAwareContent parentContent, List<String> refChildIds, int actionId, String childMetadataPath) throws AmetysRepositoryException, WorkflowException 868 { 869 if (refChildIds.size() > 0) 870 { 871 Map<String, Object> values = new HashMap<>(); 872 873 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + childMetadataPath, refChildIds); 874 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + childMetadataPath + ".mode", MODE.REPLACE.name()); 875 876 Map<String, Object> contextParameters = new HashMap<>(); 877 contextParameters.put("quit", true); 878 contextParameters.put("values", values); 879 880 Map<String, Object> inputs = new HashMap<>(); 881 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 882 883 _contentWorkflowHelper.doAction(parentContent, actionId, inputs); 884 } 885 } 886 887 /** 888 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 889 * @param createdContent The created content to clean 890 */ 891 protected void _cleanContentMetadata(ModifiableContent createdContent) 892 { 893 ModifiableCompositeMetadata metadataHolder = createdContent.getMetadataHolder(); 894 if (createdContent instanceof ProgramPart) 895 { 896 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, ProgramPart.METADATA_PARENT_PROGRAM_PARTS); 897 } 898 899 if (createdContent instanceof TraversableProgramPart) 900 { 901 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS); 902 } 903 904 if (createdContent instanceof CourseList) 905 { 906 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_CHILD_COURSES); 907 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_PARENT_COURSES); 908 } 909 910 if (createdContent instanceof Course) 911 { 912 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_CHILD_COURSE_LISTS); 913 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_PARENT_COURSE_LISTS); 914 } 915 } 916}