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.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.commons.lang.StringUtils; 039import org.apache.commons.lang3.ArrayUtils; 040import org.apache.commons.lang3.tuple.Pair; 041 042import org.ametys.cms.content.external.ExternalizableMetadataHelper; 043import org.ametys.cms.content.external.ExternalizableMetadataProviderExtensionPoint; 044import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 045import org.ametys.cms.data.ContentDataHelper; 046import org.ametys.cms.repository.Content; 047import org.ametys.cms.repository.ContentQueryHelper; 048import org.ametys.cms.repository.ContentTypeExpression; 049import org.ametys.cms.repository.DefaultContent; 050import org.ametys.cms.repository.LanguageExpression; 051import org.ametys.cms.repository.ModifiableContent; 052import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 053import org.ametys.cms.workflow.ContentWorkflowHelper; 054import org.ametys.core.observation.ObservationManager; 055import org.ametys.core.ui.Callable; 056import org.ametys.core.user.CurrentUserProvider; 057import org.ametys.odf.course.Course; 058import org.ametys.odf.course.CourseContainer; 059import org.ametys.odf.course.ShareableCourseHelper; 060import org.ametys.odf.courselist.CourseList; 061import org.ametys.odf.courselist.CourseList.ChoiceType; 062import org.ametys.odf.courselist.CourseListContainer; 063import org.ametys.odf.coursepart.CoursePart; 064import org.ametys.odf.coursepart.CoursePartFactory; 065import org.ametys.odf.orgunit.OrgUnit; 066import org.ametys.odf.orgunit.RootOrgUnitProvider; 067import org.ametys.odf.program.AbstractProgram; 068import org.ametys.odf.program.AbstractTraversableProgramPart; 069import org.ametys.odf.program.Container; 070import org.ametys.odf.program.Program; 071import org.ametys.odf.program.ProgramFactory; 072import org.ametys.odf.program.ProgramPart; 073import org.ametys.odf.program.SubProgram; 074import org.ametys.odf.program.TraversableProgramPart; 075import org.ametys.odf.workflow.ValidateODFContentFunction; 076import org.ametys.plugins.repository.AmetysObject; 077import org.ametys.plugins.repository.AmetysObjectExistsException; 078import org.ametys.plugins.repository.AmetysObjectIterable; 079import org.ametys.plugins.repository.AmetysObjectIterator; 080import org.ametys.plugins.repository.AmetysObjectResolver; 081import org.ametys.plugins.repository.AmetysRepositoryException; 082import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 083import org.ametys.plugins.repository.RepositoryConstants; 084import org.ametys.plugins.repository.UnknownAmetysObjectException; 085import org.ametys.plugins.repository.collection.AmetysObjectCollection; 086import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 087import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 088import org.ametys.plugins.repository.query.QueryHelper; 089import org.ametys.plugins.repository.query.SortCriteria; 090import org.ametys.plugins.repository.query.expression.AndExpression; 091import org.ametys.plugins.repository.query.expression.Expression; 092import org.ametys.plugins.repository.query.expression.Expression.Operator; 093import org.ametys.plugins.repository.query.expression.OrExpression; 094import org.ametys.plugins.repository.query.expression.StringExpression; 095import org.ametys.runtime.i18n.I18nizableText; 096import org.ametys.runtime.plugin.component.AbstractLogEnabled; 097import org.ametys.runtime.plugin.component.PluginAware; 098 099import com.opensymphony.workflow.WorkflowException; 100 101/** 102 * Helper for ODF contents 103 * 104 */ 105public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware, Contextualizable 106{ 107 /** The component role. */ 108 public static final String ROLE = ODFHelper.class.getName(); 109 110 /** Request attribute to get the "Live" version of contents */ 111 public static final String REQUEST_ATTRIBUTE_VALID_LABEL = "live-version"; 112 113 /** The default id of initial workflow action */ 114 protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0; 115 116 /** Ametys object resolver */ 117 protected AmetysObjectResolver _resolver; 118 /** The content workflow helper */ 119 protected ContentWorkflowHelper _contentWorkflowHelper; 120 /** The content types manager */ 121 protected ContentTypeExtensionPoint _cTypeEP; 122 /** The observation manager */ 123 protected ObservationManager _observationManager; 124 /** The current user provider */ 125 protected CurrentUserProvider _currentUserProvider; 126 /** Root orgunit */ 127 protected RootOrgUnitProvider _ouRootProvider; 128 /** Provider for externalizable metadata */ 129 protected ExternalizableMetadataProviderExtensionPoint _externalizableMetadataProviderEP; 130 /** Helper for shareable course */ 131 protected ShareableCourseHelper _shareableCourseHelper; 132 /** The Avalon context */ 133 protected Context _context; 134 135 private String _pluginName; 136 137 public void contextualize(Context context) throws ContextException 138 { 139 _context = context; 140 } 141 142 @Override 143 public void service(ServiceManager manager) throws ServiceException 144 { 145 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 146 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 147 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 148 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 149 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 150 _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 151 _externalizableMetadataProviderEP = (ExternalizableMetadataProviderExtensionPoint) manager.lookup(ExternalizableMetadataProviderExtensionPoint.ROLE); 152 _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE); 153 } 154 155 @Override 156 public void setPluginInfo(String pluginName, String featureName, String id) 157 { 158 _pluginName = pluginName; 159 } 160 161 /** 162 * Gets the root for ODF contents 163 * @return the root for ODF contents 164 */ 165 public AmetysObjectCollection getRootContent() 166 { 167 return getRootContent(false); 168 } 169 170 /** 171 * Gets the root for ODF contents 172 * @param create <code>true</code> to create automatically the root when missing. 173 * @return the root for ODF contents 174 */ 175 public AmetysObjectCollection getRootContent(boolean create) 176 { 177 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/"); 178 179 boolean needSave = false; 180 if (!pluginsNode.hasChild(_pluginName)) 181 { 182 if (create) 183 { 184 pluginsNode.createChild(_pluginName, "ametys:unstructured"); 185 needSave = true; 186 } 187 else 188 { 189 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing"); 190 } 191 } 192 193 ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName); 194 if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents")) 195 { 196 if (create) 197 { 198 pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection"); 199 needSave = true; 200 } 201 else 202 { 203 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing"); 204 } 205 } 206 207 if (needSave) 208 { 209 pluginsNode.saveChanges(); 210 } 211 212 return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 213 } 214 215 /** 216 * Get the {@link ProgramItem}s matching the given arguments 217 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 218 * @param code The code. Can be null to get program's items regardless of their code 219 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 220 * @param lang The search language. Can be null to get program's items regardless of their language 221 * @param <C> The content return type 222 * @return The matching program items 223 */ 224 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang) 225 { 226 return getProgramItems(cTypeId, code, catalogName, lang, null, null); 227 } 228 229 /** 230 * Get the {@link ProgramItem}s matching the given arguments 231 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 232 * @param code The code. Can be null to get program's items regardless of their code 233 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 234 * @param lang The search language. Can be null to get program's items regardless of their language 235 * @param <C> The content return type 236 * @return The matching program items 237 */ 238 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang) 239 { 240 return getProgramItems(cTypeIds, code, catalogName, lang, null, null); 241 } 242 243 /** 244 * Get the {@link ProgramItem}s matching the given arguments 245 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 246 * @param code The code. Can be null to get program's items regardless of their code 247 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 248 * @param lang The search language. Can be null to get program's items regardless of their language 249 * @param additionnalExpr An additional expression for filtering result. Can be null 250 * @param sortCriteria criteria for sorting results 251 * @param <C> The content return type 252 * @return The matching program items 253 */ 254 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 255 { 256 return getProgramItems(cTypeId != null ? Collections.singletonList(cTypeId) : Collections.EMPTY_LIST, code, catalogName, lang, additionnalExpr, sortCriteria); 257 } 258 259 /** 260 * Get the {@link ProgramItem}s matching the given arguments 261 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 262 * @param code The code. Can be null to get program's items regardless of their code 263 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 264 * @param lang The search language. Can be null to get program's items regardless of their language 265 * @param additionnalExpr An additional expression for filtering result. Can be null 266 * @param sortCriteria criteria for sorting results 267 * @param <C> The content return type 268 * @return The matching program items 269 */ 270 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 271 { 272 List<Expression> exprs = new ArrayList<>(); 273 274 if (!cTypeIds.isEmpty()) 275 { 276 exprs.add(new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()]))); 277 } 278 if (StringUtils.isNotEmpty(code)) 279 { 280 exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code)); 281 } 282 if (StringUtils.isNotEmpty(catalogName)) 283 { 284 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName)); 285 } 286 if (StringUtils.isNotEmpty(lang)) 287 { 288 exprs.add(new LanguageExpression(Operator.EQ, lang)); 289 } 290 if (additionnalExpr != null) 291 { 292 exprs.add(additionnalExpr); 293 } 294 295 Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 296 297 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria); 298 return _resolver.query(xpathQuery); 299 } 300 301 /** 302 * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language 303 * @param srcCoursePart The source course part 304 * @param catalogName The name of catalog to search into 305 * @param lang The search language 306 * @return The equivalent program item or <code>null</code> if not exists 307 */ 308 public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang) 309 { 310 return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang); 311 } 312 313 /** 314 * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 315 * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem} 316 * @param srcProgramItem The source program item 317 * @param catalogName The name of catalog to search into 318 * @param lang The search language 319 * @return The equivalent program item or <code>null</code> if not exists 320 */ 321 public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang) 322 { 323 return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang); 324 } 325 326 /** 327 * Get the equivalent {@link Content} having the same code in given catalog and language 328 * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject} 329 * @param contentType The content type to search for 330 * @param odfContentCode The code of the ODF content 331 * @param catalogName The name of catalog to search into 332 * @param lang The search language 333 * @return The equivalent content or <code>null</code> if not exists 334 */ 335 public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang) 336 { 337 Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType); 338 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 339 Expression catalogExpr = new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName); 340 Expression codeExpr = new StringExpression(ProgramItem.CODE, Operator.EQ, odfContentCode); 341 342 Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr); 343 344 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 345 AmetysObjectIterable<T> contents = _resolver.query(xpathQuery); 346 AmetysObjectIterator<T> contentsIt = contents.iterator(); 347 if (contentsIt.hasNext()) 348 { 349 return contentsIt.next(); 350 } 351 352 return null; 353 } 354 355 /** 356 * Get the child program items of a {@link ProgramItem} 357 * @param programItem The program item 358 * @return The child program items 359 */ 360 public List<ProgramItem> getChildProgramItems(ProgramItem programItem) 361 { 362 List<ProgramItem> children = new ArrayList<>(); 363 364 if (programItem instanceof TraversableProgramPart) 365 { 366 children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren()); 367 } 368 369 if (programItem instanceof CourseContainer) 370 { 371 children.addAll(((CourseContainer) programItem).getCourses()); 372 } 373 374 if (programItem instanceof Course) 375 { 376 children.addAll(((Course) programItem).getCourseLists()); 377 } 378 379 return children; 380 } 381 382 /** 383 * Get the child subprograms of a {@link ProgramPart} 384 * @param programPart The program part 385 * @return The child subprograms 386 */ 387 public Set<SubProgram> getChildSubPrograms(ProgramPart programPart) 388 { 389 Set<SubProgram> subPrograms = new HashSet<>(); 390 391 if (programPart instanceof TraversableProgramPart) 392 { 393 if (programPart instanceof SubProgram) 394 { 395 subPrograms.add((SubProgram) programPart); 396 } 397 ((TraversableProgramPart) programPart).getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child))); 398 } 399 400 return subPrograms; 401 } 402 403 /** 404 * Gets (recursively) parent containers of this program item. 405 * @param programItem The program item 406 * @return parent containers of this program item. 407 */ 408 public Set<Container> getParentContainers(ProgramItem programItem) 409 { 410 return _getParentsOfType(programItem, Container.class); 411 } 412 413 /** 414 * Gets (recursively) parent programs of this program item. 415 * @param programItem The program item 416 * @return parent programs of this program item. 417 */ 418 public Set<Program> getParentPrograms(ProgramItem programItem) 419 { 420 return _getParentsOfType(programItem, Program.class); 421 } 422 423 /** 424 * Gets (recursively) parent abstract programs of this program item. 425 * @param programItem The program item 426 * @return parent abstract programs of this program item. 427 */ 428 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem) 429 { 430 return _getParentsOfType(programItem, AbstractProgram.class); 431 } 432 433 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest) 434 { 435 Set<ProgramItem> visitedProgramItems = new HashSet<>(); 436 visitedProgramItems.add(programItem); 437 return _getParentsOfType(programItem, visitedProgramItems, classToTest); 438 } 439 440 @SuppressWarnings("unchecked") 441 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest) 442 { 443 Set<T> parentsOfType = new HashSet<>(); 444 List<ProgramItem> parents = getParentProgramItems(programItem); 445 446 for (ProgramItem parent : parents) 447 { 448 // Only parents not already visited 449 if (visitedProgramItems.add(parent)) 450 { 451 // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram) 452 if (classToTest.isInstance(parent)) 453 { 454 parentsOfType.add((T) parent); 455 } 456 else 457 { 458 parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest)); 459 } 460 } 461 } 462 463 return parentsOfType; 464 } 465 466 /** 467 * Get the child programs of an {@link OrgUnit} 468 * @param orgUnit the orgUnit 469 * @param catalog the catalog 470 * @param lang the lang 471 * @return The child programs 472 */ 473 public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang) 474 { 475 List<Program> programs = new ArrayList<>(); 476 List<Expression> programExpressions = new ArrayList<>(); 477 programExpressions.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 478 479 programExpressions.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 480 programExpressions.add(new LanguageExpression(Operator.EQ, lang)); 481 482 List<Expression> expressions = new ArrayList<>(); 483 for (String orgUnitId : getSubOrgUnitIds(orgUnit)) 484 { 485 expressions.add(new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId)); 486 } 487 488 programExpressions.add(new OrExpression(expressions.toArray(new Expression[0]))); 489 String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0]))); 490 AmetysObjectIterable<Program> programsIterable = _resolver.query(programQuery); 491 AmetysObjectIterator<Program> programsIterator = programsIterable.iterator(); 492 while (programsIterator.hasNext()) 493 { 494 programs.add(programsIterator.next()); 495 } 496 return programs; 497 } 498 499 /** 500 * Get the current orgunit and its suborgunits recursively identifiers. 501 * @param orgUnit The orgunit at the top 502 * @return A {@link List} of {@link OrgUnit} ids 503 */ 504 public List<String> getSubOrgUnitIds(OrgUnit orgUnit) 505 { 506 List<String> orgUnitIds = new ArrayList<>(); 507 orgUnitIds.add(orgUnit.getId()); 508 for (String id : orgUnit.getSubOrgUnits()) 509 { 510 OrgUnit childOrgUnit = _resolver.resolveById(id); 511 orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit)); 512 } 513 514 return orgUnitIds; 515 } 516 517 /** 518 * Determines if the {@link ProgramItem} has parent program items 519 * @param programItem The program item 520 * @return true if has parent program items 521 */ 522 public boolean hasParentProgramItems(ProgramItem programItem) 523 { 524 boolean hasParent = false; 525 526 if (programItem instanceof ProgramPart) 527 { 528 hasParent = !((ProgramPart) programItem).getProgramPartParents().isEmpty() || hasParent; 529 } 530 531 if (programItem instanceof CourseList) 532 { 533 hasParent = !((CourseList) programItem).getParentCourses().isEmpty() || hasParent; 534 } 535 536 if (programItem instanceof Course) 537 { 538 hasParent = !((Course) programItem).getParentCourseLists().isEmpty() || hasParent; 539 } 540 541 return hasParent; 542 } 543 544 /** 545 * Get the parent program items of a {@link ProgramItem} 546 * @param programItem The program item 547 * @return The parent program items 548 */ 549 public List<ProgramItem> getParentProgramItems(ProgramItem programItem) 550 { 551 List<ProgramItem> parents = new ArrayList<>(); 552 553 if (programItem instanceof ProgramPart) 554 { 555 parents.addAll(((ProgramPart) programItem).getProgramPartParents()); 556 } 557 558 if (programItem instanceof CourseList) 559 { 560 parents.addAll(((CourseList) programItem).getParentCourses()); 561 } 562 563 if (programItem instanceof Course) 564 { 565 parents.addAll(((Course) programItem).getParentCourseLists()); 566 } 567 568 return parents; 569 } 570 571 /** 572 * Get the nearest program item parent into the given parent {@link AbstractProgram} 573 * @param programItem The program item 574 * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned. 575 * @return The parent program item or null if not found. 576 */ 577 public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram) 578 { 579 if (programItem instanceof Program) 580 { 581 return null; 582 } 583 584 if (programItem instanceof ProgramPart) 585 { 586 List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents(); 587 588 for (ProgramPart parent : parents) 589 { 590 if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram))) 591 { 592 return parent; 593 } 594 else 595 { 596 ProgramItem ancestor = getParentProgramItem(parent, parentProgram); 597 if (ancestor != null) 598 { 599 return parent; 600 } 601 } 602 } 603 } 604 605 if (programItem instanceof CourseList) 606 { 607 for (Course parentCourse : ((CourseList) programItem).getParentCourses()) 608 { 609 ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram); 610 if (ancestor != null) 611 { 612 return parentCourse; 613 } 614 } 615 } 616 617 if (programItem instanceof Course) 618 { 619 for (CourseList cl : ((Course) programItem).getParentCourseLists()) 620 { 621 ProgramItem ancestor = getParentProgramItem(cl, parentProgram); 622 if (ancestor != null) 623 { 624 return cl; 625 } 626 } 627 } 628 629 return null; 630 } 631 632 /** 633 * Get information of the program item structure (type, if program has children) 634 * @param programItemId the program item id 635 * @return a map of information 636 */ 637 @Callable 638 public Map<String, Object> getStructureInfo(String programItemId) 639 { 640 Map<String, Object> results = new HashMap<>(); 641 642 if (StringUtils.isNotBlank(programItemId)) 643 { 644 Content content = _resolver.resolveById(programItemId); 645 if (content instanceof ProgramItem) 646 { 647 results.put("id", programItemId); 648 results.put("title", content.getTitle()); 649 results.put("code", ((ProgramItem) content).getCode()); 650 651 List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content); 652 results.put("hasChildren", !childProgramItems.isEmpty()); 653 654 List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content); 655 results.put("hasParent", !parentProgramItems.isEmpty()); 656 657 results.put("paths", getPaths((ProgramItem) content, " > ")); 658 } 659 } 660 661 return results; 662 } 663 664 /** 665 * Get information of the program item structure (type, if program has children) 666 * @param programItemIds the list of program item id 667 * @return a map of information 668 */ 669 @Callable 670 public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds) 671 { 672 Map<String, Map<String, Object>> results = new HashMap<>(); 673 674 for (String programItemId : programItemIds) 675 { 676 results.put(programItemId, getStructureInfo(programItemId)); 677 } 678 679 return results; 680 } 681 682 /** 683 * Get all the paths of a ODF content.<br> 684 * The path is construct with the contents' title 685 * @param separator The path separator 686 * @param item The program item 687 * @return the paths in parent program items 688 */ 689 protected List<String> getPaths(ProgramItem item, String separator) 690 { 691 List<String> paths = new ArrayList<>(); 692 693 List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(item); 694 for (List<ProgramItem> ancestorPath : ancestorPaths) 695 { 696 List<String> titles = ancestorPath.stream().map(p -> ((Content) p).getTitle() + " (" + p.getCode() + ")").collect(Collectors.toList()); 697 paths.add(String.join(separator, titles)); 698 } 699 700 return paths; 701 } 702 703 /** 704 * Get the full path to program item for highest ancestors. The path includes this final item. 705 * @param item the program item 706 * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item. 707 */ 708 public List<List<ProgramItem>> getPathOfAncestors(ProgramItem item) 709 { 710 List<List<ProgramItem>> ancestors = new ArrayList<>(); 711 712 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 713 if (parentProgramItems.isEmpty()) 714 { 715 List<ProgramItem> items = new ArrayList<>(); 716 items.add(item); 717 ancestors.add(items); 718 return ancestors; 719 } 720 721 for (ProgramItem parentProgramItem : parentProgramItems) 722 { 723 for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem)) 724 { 725 ancestorPaths.add(item); 726 ancestors.add(ancestorPaths); 727 } 728 } 729 730 return ancestors; 731 } 732 733 /** 734 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 735 * The path is construct with the contents' names and the used separator is '/'. 736 * @param programItemId The id of the program item 737 * @param programId The id of program. Can not be null. 738 * @return the path into the parent program or null if the item is not part of this program. 739 */ 740 @Callable 741 public String getPathInProgram (String programItemId, String programId) 742 { 743 ProgramItem item = _resolver.resolveById(programItemId); 744 Program program = _resolver.resolveById(programId); 745 746 return getPathInProgram(item, program); 747 } 748 749 /** 750 * Get the path of a ODF content into a {@link Program}.<br> 751 * The path is construct with the contents' names and the used separator is '/'. 752 * @param item The program item 753 * @param parentProgram The parent root (sub)program. Can not be null. 754 * @return the path from the parent program 755 */ 756 public String getPathInProgram (ProgramItem item, Program parentProgram) 757 { 758 if (item instanceof Program) 759 { 760 // The program item is already the program it self or another program 761 return item.equals(parentProgram) ? "" : null; 762 } 763 764 List<String> paths = new ArrayList<>(); 765 paths.add(item.getName()); 766 767 ProgramItem parent = getParentProgramItem(item, parentProgram); 768 while (parent != null && !(parent instanceof Program)) 769 { 770 paths.add(parent.getName()); 771 parent = getParentProgramItem(parent, parentProgram); 772 } 773 774 if (parent != null) 775 { 776 paths.add(parent.getName()); 777 Collections.reverse(paths); 778 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 779 } 780 781 return null; 782 } 783 784 /** 785 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 786 * The path is construct with the contents' names and the used separator is '/'. 787 * @param contentId The id of the content 788 * @param parentCourseId The id of parent course. Can not be null. 789 * @return the path into the parent course or null if the item is not part of this course. 790 */ 791 @Callable 792 public String getPathInCourse (String contentId, String parentCourseId) 793 { 794 Content content = _resolver.resolveById(contentId); 795 Course parentCourse = _resolver.resolveById(parentCourseId); 796 797 return getPathInCourse(content, parentCourse); 798 } 799 800 /** 801 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 802 * The path is construct with the contents' names and the used separator is '/'. 803 * @param courseOrList The course or the course list 804 * @param parentCourse The parent course. Can not be null. 805 * @return the path into the parent course or null if the item is not part of this course. 806 */ 807 public String getPathInCourse(Content courseOrList, Course parentCourse) 808 { 809 if (courseOrList.equals(parentCourse)) 810 { 811 return ""; 812 } 813 814 String path = _getPathInCourse(courseOrList, parentCourse); 815 816 return path; 817 } 818 819 private String _getPathInCourse(Content content, Content parentContent) 820 { 821 if (content.equals(parentContent)) 822 { 823 return content.getName(); 824 } 825 826 List<? extends Content> parents; 827 828 if (content instanceof Course) 829 { 830 parents = ((Course) content).getParentCourseLists(); 831 } 832 else if (content instanceof CourseList) 833 { 834 parents = ((CourseList) content).getParentCourses(); 835 } 836 else 837 { 838 throw new IllegalStateException(); 839 } 840 841 for (Content parent : parents) 842 { 843 String path = _getPathInCourse(parent, parentContent); 844 if (path != null) 845 { 846 return path + '/' + content.getName(); 847 } 848 } 849 return null; 850 } 851 852 /** 853 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 854 * The path is construct with the contents' names and the used separator is '/'. 855 * @param orgUnitId The id of the orgunit 856 * @param rootOrgUnitId The root orgunit id 857 * @return the path into the parent program or null if the item is not part of this program. 858 */ 859 @Callable 860 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 861 { 862 OrgUnit rootOU = null; 863 if (StringUtils.isNotBlank(rootOrgUnitId)) 864 { 865 rootOU = _resolver.resolveById(rootOrgUnitId); 866 } 867 else 868 { 869 rootOU = _ouRootProvider.getRoot(); 870 } 871 872 if (orgUnitId.equals(rootOU.getId())) 873 { 874 // The orgunit is already the root orgunit 875 return rootOU.getName(); 876 } 877 878 OrgUnit ou = _resolver.resolveById(orgUnitId); 879 880 List<String> paths = new ArrayList<>(); 881 paths.add(ou.getName()); 882 883 OrgUnit parent = ou.getParentOrgUnit(); 884 while (parent != null && !parent.getId().equals(rootOU.getId())) 885 { 886 paths.add(parent.getName()); 887 parent = parent.getParentOrgUnit(); 888 } 889 890 if (parent != null) 891 { 892 paths.add(rootOU.getName()); 893 Collections.reverse(paths); 894 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 895 } 896 897 return null; 898 } 899 900 /** 901 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 902 * The path is construct with the contents' names and the used separator is '/'. 903 * @param orgUnitId The id of the orgunit 904 * @return the path into the parent program or null if the item is not part of this program. 905 */ 906 @Callable 907 public String getOrgUnitPath(String orgUnitId) 908 { 909 return getOrgUnitPath(orgUnitId, null); 910 } 911 912 /** 913 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 914 * @param part The program part 915 * @param parentId The ancestor id 916 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 917 */ 918 public boolean hasAncestor (ProgramPart part, String parentId) 919 { 920 List<ProgramPart> parents = part.getProgramPartParents(); 921 922 for (ProgramPart parent : parents) 923 { 924 if (parent.getId().equals(parentId)) 925 { 926 return true; 927 } 928 else if (hasAncestor(parent, parentId)) 929 { 930 return true; 931 } 932 } 933 934 return false; 935 } 936 937 /** 938 * Check if a relation can be establish between two ODF contents 939 * @param srcContent The source content (copied or moved) 940 * @param targetContent The target content 941 * @param errors The list of error messages 942 * @param contextualParameters the contextual parameters 943 * @return true if the relation is valid, false otherwise 944 */ 945 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 946 { 947 boolean isCompatible = true; 948 949 if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit) 950 { 951 if (!_isContentTypeCompatible(srcContent, targetContent)) 952 { 953 // Invalid relations between content types 954 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 955 isCompatible = false; 956 } 957 else if (!_isCatalogCompatible(srcContent, targetContent)) 958 { 959 // Catalog is invalid 960 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 961 isCompatible = false; 962 } 963 else if (!srcContent.getLanguage().equals(targetContent.getLanguage())) 964 { 965 // Language is invalid 966 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 967 isCompatible = false; 968 } 969 // We check shareable fields only if the course content is not created (or created by copy) and not moved 970 else if (srcContent instanceof Course 971 && targetContent instanceof CourseList 972 && _shareableCourseHelper.handleShareableCourse() 973 && !"create".equals(contextualParameters.get("mode")) 974 && !"copy".equals(contextualParameters.get("mode")) 975 && !"move".equals(contextualParameters.get("mode")) 976 // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields 977 && !_isCourseAlreadyBelongToCourseList((Course) srcContent, (CourseList) targetContent)) 978 { 979 if (!_shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent)) 980 { 981 // Shareable fields don't match 982 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 983 isCompatible = false; 984 } 985 } 986 } 987 else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit) 988 { 989 // If the target isn't ODF related but the source is, the relation is not compatible. 990 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 991 isCompatible = false; 992 } 993 994 return isCompatible; 995 } 996 997 private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList) 998 { 999 return courseList.getCourses().contains(course); 1000 } 1001 1002 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 1003 { 1004 if (srcContent instanceof Container || srcContent instanceof SubProgram) 1005 { 1006 return targetContent instanceof AbstractTraversableProgramPart; 1007 } 1008 else if (srcContent instanceof CourseList) 1009 { 1010 return targetContent instanceof CourseListContainer; 1011 } 1012 else if (srcContent instanceof Course) 1013 { 1014 return targetContent instanceof CourseList; 1015 } 1016 else if (srcContent instanceof OrgUnit) 1017 { 1018 return targetContent instanceof OrgUnit; 1019 } 1020 1021 return false; 1022 } 1023 1024 private boolean _isCatalogCompatible(Content srcContent, Content targetContent) 1025 { 1026 if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem) 1027 { 1028 return ((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog()); 1029 } 1030 return true; 1031 } 1032 1033 private List<String> _getContentParameters(Content srcContent, Content targetContent) 1034 { 1035 List<String> parameters = new ArrayList<>(); 1036 parameters.add(srcContent.getTitle()); 1037 parameters.add(srcContent.getId()); 1038 parameters.add(targetContent.getTitle()); 1039 parameters.add(targetContent.getId()); 1040 return parameters; 1041 } 1042 /** 1043 * Copy a {@link ProgramItem} 1044 * @param srcContent The program item to copy 1045 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1046 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1047 * @param copiedPrograms the id of initial programs with their copied content 1048 * @param copiedSubPrograms the id of initial subprograms with their copied content 1049 * @param copiedContainers the id of initial containers with their copied content 1050 * @param copiedCourseLists the id of initial course lists with their copied content 1051 * @param copiedCourses the id of initial courses with their copied content 1052 * @param copiedCourseParts the id of initial course parts with their copied content 1053 * @return The created content 1054 * @param <C> The modifiable content return type 1055 * @throws AmetysRepositoryException If an error occurred during copy 1056 * @throws WorkflowException If an error occurred during copy 1057 */ 1058 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 1059 { 1060 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1061 } 1062 1063 /** 1064 * Copy a {@link ProgramItem} 1065 * @param srcContent The program item to copy 1066 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1067 * @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. 1068 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1069 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1070 * @param copiedPrograms the id of initial programs with their copied content 1071 * @param copiedSubPrograms the id of initial subprograms with their copied content 1072 * @param copiedContainers the id of initial containers with their copied content 1073 * @param copiedCourseLists the id of initial course lists with their copied content 1074 * @param copiedCourses the id of initial courses with their copied content 1075 * @param copiedCourseParts the id of initial course parts with their copied content 1076 * @param <C> The modifiable content return type 1077 * @return The created content 1078 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1079 * @throws AmetysRepositoryException If an error occurred 1080 * @throws WorkflowException If an error occurred 1081 */ 1082 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 1083 { 1084 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1085 } 1086 1087 /** 1088 * Copy a {@link CoursePart} 1089 * @param srcContent The course part to copy 1090 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1091 * @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. 1092 * @param initWorkflowActionId The initial workflow action id 1093 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1094 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1095 * @param copiedPrograms the id of initial programs with their copied content 1096 * @param copiedSubPrograms the id of initial subprograms with their copied content 1097 * @param copiedContainers the id of initial containers with their copied content 1098 * @param copiedCourseLists the id of initial course lists with their copied content 1099 * @param copiedCourses the id of initial courses with their copied content 1100 * @param copiedCourseParts the id of initial course parts with their copied content 1101 * @param <C> The modifiable content return type 1102 * @return The created content 1103 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1104 * @throws AmetysRepositoryException If an error occurred 1105 * @throws WorkflowException If an error occurred 1106 */ 1107 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 1108 { 1109 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1110 } 1111 1112 /** 1113 * Copy a {@link ProgramItem} 1114 * @param srcContent The program item to copy 1115 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1116 * @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. 1117 * @param initWorkflowActionId The initial workflow action id 1118 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1119 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1120 * @param copiedPrograms the id of initial programs with their copied content 1121 * @param copiedSubPrograms the id of initial subprograms with their copied content 1122 * @param copiedContainers the id of initial containers with their copied content 1123 * @param copiedCourseLists the id of initial course lists with their copied content 1124 * @param copiedCourses the id of initial courses with their copied content 1125 * @param copiedCourseParts the id of initial course parts with their copied content 1126 * @param <C> The modifiable content return type 1127 * @return The created content 1128 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1129 * @throws AmetysRepositoryException If an error occurred 1130 * @throws WorkflowException If an error occurred 1131 */ 1132 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 1133 { 1134 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1135 } 1136 1137 /** 1138 * Copy a {@link ProgramItem} 1139 * @param srcContent The program item to copy 1140 * @param catalog The catalog 1141 * @param code The odf content code 1142 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1143 * @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. 1144 * @param initWorkflowActionId The initial workflow action id 1145 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1146 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1147 * @param copiedPrograms the id of initial programs with their copied content 1148 * @param copiedSubPrograms the id of initial subprograms with their copied content 1149 * @param copiedContainers the id of initial containers with their copied content 1150 * @param copiedCourseLists the id of initial course lists with their copied content 1151 * @param copiedCourses the id of initial courses with their copied content 1152 * @param copiedCourseParts the id of initial course parts with their copied content 1153 * @param <C> The modifiable content return type 1154 * @return The created content 1155 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1156 * @throws AmetysRepositoryException If an error occurred 1157 * @throws WorkflowException If an error occurred 1158 */ 1159 @SuppressWarnings("unchecked") 1160 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 1161 { 1162 String computedTargetLanguage = targetContentLanguage; 1163 if (computedTargetLanguage == null) 1164 { 1165 computedTargetLanguage = srcContent.getLanguage(); 1166 } 1167 1168 String computeTargetName = targetContentName; 1169 if (computeTargetName == null) 1170 { 1171 // Compute content name from source content and requested language 1172 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 1173 } 1174 1175 String computeTargetCatalog = targetCatalog; 1176 if (computeTargetCatalog == null) 1177 { 1178 computeTargetCatalog = catalog; 1179 } 1180 1181 String principalContentType = srcContent.getTypes()[0]; 1182 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 1183 if (createdContent != null) 1184 { 1185 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 1186 } 1187 else 1188 { 1189 // Copy content waiting for observers to be completed and copying ACL 1190 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true); 1191 1192 if (fullCopy) 1193 { 1194 _cleanContentMetadata(createdContent); 1195 1196 if (targetCatalog != null) 1197 { 1198 boolean hasChanges = false; 1199 if (createdContent instanceof ProgramItem) 1200 { 1201 ((ProgramItem) createdContent).setCatalog(targetCatalog); 1202 hasChanges = true; 1203 } 1204 else if (createdContent instanceof CoursePart) 1205 { 1206 ((CoursePart) createdContent).setCatalog(targetCatalog); 1207 hasChanges = true; 1208 } 1209 1210 if (hasChanges) 1211 { 1212 createdContent.saveChanges(); 1213 } 1214 } 1215 1216 if (srcContent instanceof ProgramItem) 1217 { 1218 copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1219 } 1220 } 1221 1222 _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1223 } 1224 1225 return (C) createdContent; 1226 } 1227 1228 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) 1229 { 1230 if (createdContent instanceof Program) 1231 { 1232 copiedPrograms.put(srcContent.getId(), createdContent.getId()); 1233 } 1234 else if (createdContent instanceof SubProgram) 1235 { 1236 copiedSubPrograms.put(srcContent.getId(), createdContent.getId()); 1237 } 1238 else if (createdContent instanceof Container) 1239 { 1240 copiedContainers.put(srcContent.getId(), createdContent.getId()); 1241 } 1242 else if (createdContent instanceof CourseList) 1243 { 1244 copiedCourseLists.put(srcContent.getId(), createdContent.getId()); 1245 } 1246 else if (createdContent instanceof Course) 1247 { 1248 copiedCourses.put(srcContent.getId(), createdContent.getId()); 1249 } 1250 else if (createdContent instanceof CoursePart) 1251 { 1252 copiedCourseParts.put(srcContent.getId(), createdContent.getId()); 1253 } 1254 } 1255 1256 /** 1257 * Copy the structure of a {@link ProgramItem} 1258 * @param srcContent the content to copy 1259 * @param targetContent the target content 1260 * @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. 1261 * @param initWorkflowActionId The initial workflow action id 1262 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1263 * @param copiedPrograms the id of initial programs with their copied content 1264 * @param copiedSubPrograms the id of initial subprograms with their copied content 1265 * @param copiedContainers the id of initial containers with their copied content 1266 * @param copiedCourseLists the id of initial course lists with their copied content 1267 * @param copiedCourses the id of initial courses with their copied content 1268 * @param copiedCourseParts the id of initial course parts with their copied content 1269 * @throws AmetysRepositoryException If an error occurred during copy 1270 * @throws WorkflowException If an error occurred during copy 1271 */ 1272 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 1273 { 1274 List<ProgramItem> srcChildContents = new ArrayList<>(); 1275 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1276 1277 String childMetadataPath = null; 1278 String parentMetadataPath = null; 1279 1280 if (srcContent instanceof TraversableProgramPart) 1281 { 1282 childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS; 1283 parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS; 1284 srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren()); 1285 } 1286 else if (srcContent instanceof CourseList) 1287 { 1288 childMetadataPath = CourseList.CHILD_COURSES; 1289 parentMetadataPath = Course.PARENT_COURSE_LISTS; 1290 srcChildContents.addAll(((CourseList) srcContent).getCourses()); 1291 } 1292 else if (srcContent instanceof Course) 1293 { 1294 childMetadataPath = Course.CHILD_COURSE_LISTS; 1295 parentMetadataPath = CourseList.PARENT_COURSES; 1296 srcChildContents.addAll(((Course) srcContent).getCourseLists()); 1297 1298 List<String> refCoursePartIds = new ArrayList<>(); 1299 for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts()) 1300 { 1301 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1302 refCoursePartIds.add(targetChildContent.getId()); 1303 } 1304 _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds); 1305 } 1306 1307 List<String> refChildIds = new ArrayList<>(); 1308 for (ProgramItem srcChildContent : srcChildContents) 1309 { 1310 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1311 refChildIds.add(targetChildContent.getId()); 1312 } 1313 1314 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1315 1316 _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values); 1317 1318 } 1319 1320 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1321 { 1322 if (!refChildIds.isEmpty()) 1323 { 1324 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1325 } 1326 } 1327 1328 private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1329 { 1330 if (!values.isEmpty()) 1331 { 1332 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1333 { 1334 String childMetadataName = entry.getKey().getLeft(); 1335 String parentMetadataName = entry.getKey().getRight(); 1336 List<String> childContents = entry.getValue(); 1337 1338 parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()])); 1339 1340 for (String childContentId : childContents) 1341 { 1342 ModifiableContent content = _resolver.resolveById(childContentId); 1343 String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName); 1344 content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId())); 1345 } 1346 } 1347 } 1348 } 1349 1350 /** 1351 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1352 * @param createdContent The created content to clean 1353 */ 1354 protected void _cleanContentMetadata(ModifiableContent createdContent) 1355 { 1356 ModifiableCompositeMetadata metadataHolder = createdContent.getMetadataHolder(); 1357 if (createdContent instanceof ProgramPart) 1358 { 1359 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, ProgramPart.PARENT_PROGRAM_PARTS); 1360 } 1361 1362 if (createdContent instanceof TraversableProgramPart) 1363 { 1364 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, TraversableProgramPart.CHILD_PROGRAM_PARTS); 1365 } 1366 1367 if (createdContent instanceof CourseList) 1368 { 1369 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.CHILD_COURSES); 1370 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.PARENT_COURSES); 1371 } 1372 1373 if (createdContent instanceof Course) 1374 { 1375 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.CHILD_COURSE_LISTS); 1376 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.PARENT_COURSE_LISTS); 1377 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.CHILD_COURSE_PARTS); 1378 } 1379 1380 if (createdContent instanceof CoursePart) 1381 { 1382 ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CoursePart.PARENT_COURSES); 1383 } 1384 } 1385 1386 /** 1387 * Switch the ametys object to Live version if it has one 1388 * @param ao the Ametys object 1389 * @throws NoLiveVersionException if the content has no live version 1390 */ 1391 public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException 1392 { 1393 // Switch to the Live label if exists 1394 String[] allLabels = ao.getAllLabels(); 1395 String[] currentLabels = ao.getLabels(); 1396 1397 boolean hasLiveVersion = Arrays.asList(allLabels).contains(ValidateODFContentFunction.VALID_LABEL); 1398 boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(ValidateODFContentFunction.VALID_LABEL); 1399 1400 if (hasLiveVersion && !currentVersionIsLive) 1401 { 1402 ao.switchToLabel(ValidateODFContentFunction.VALID_LABEL); 1403 } 1404 else if (!hasLiveVersion) 1405 { 1406 throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version"); 1407 } 1408 } 1409 1410 /** 1411 * Switch to Live version if is required 1412 * @param ao the Ametys object 1413 * @throws NoLiveVersionException if the Live version is required but not exist 1414 */ 1415 public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException 1416 { 1417 Request request = _getRequest(); 1418 if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null) 1419 { 1420 switchToLiveVersion(ao); 1421 } 1422 } 1423 1424 /** 1425 * Count the hours accumulation in the {@link ProgramItem} 1426 * @param programItem The program item on which we compute the total number of hours 1427 * @return The hours accumulation 1428 */ 1429 public Double getCumulatedHours(ProgramItem programItem) 1430 { 1431 // Ignore optional course list and avoid useless expensive calls 1432 if (programItem instanceof CourseList && ChoiceType.OPTIONAL.equals(((CourseList) programItem).getType())) 1433 { 1434 return 0.0; 1435 } 1436 1437 List<ProgramItem> children = getChildProgramItems(programItem); 1438 1439 Double coef = 1.0; 1440 Double countNbHours = 0.0; 1441 1442 // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total) 1443 if (programItem instanceof CourseList) 1444 { 1445 // If there is no children, compute the coef is useless 1446 // Also choice list can throw an exception while dividing by zero 1447 if (children.isEmpty()) 1448 { 1449 return 0.0; 1450 } 1451 1452 CourseList courseList = (CourseList) programItem; 1453 switch (courseList.getType()) 1454 { 1455 case CHOICE: 1456 // Apply the average of number of EC from children multiply by the minimum ELP to select 1457 coef = ((double) courseList.getMinNumberOfCourses()) / children.size(); 1458 break; 1459 case MANDATORY: 1460 default: 1461 // Add all ECTS from children 1462 break; 1463 } 1464 } 1465 1466 // If it's a course and we have a value for the number of hours 1467 // Then get the value 1468 if (programItem instanceof Course && ((Course) programItem).hasNonEmptyValue(Course.NUMBER_OF_HOURS)) 1469 { 1470 countNbHours += ((Course) programItem).<Double>getValue(Course.NUMBER_OF_HOURS); 1471 } 1472 // Else if there are program item children on the item 1473 // Then compute on children 1474 else if (children.size() > 0) 1475 { 1476 for (ProgramItem child : children) 1477 { 1478 countNbHours += getCumulatedHours(child); 1479 } 1480 } 1481 // Else, it's a course but there is no value for the number of hours and we don't have program item children 1482 // Then compute on course parts 1483 else if (programItem instanceof Course) 1484 { 1485 countNbHours += ((Course) programItem).getCourseParts() 1486 .stream() 1487 .mapToDouble(CoursePart::getNumberOfHours) 1488 .sum(); 1489 } 1490 1491 return coef * countNbHours; 1492 } 1493 1494 /** 1495 * Get the request 1496 * @return the request 1497 */ 1498 protected Request _getRequest() 1499 { 1500 return ContextHelper.getRequest(_context); 1501 } 1502}