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.Optional; 027import java.util.Set; 028import java.util.function.Function; 029import java.util.function.Predicate; 030import java.util.function.Supplier; 031import java.util.stream.Collectors; 032import java.util.stream.Stream; 033 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.context.Context; 036import org.apache.avalon.framework.context.ContextException; 037import org.apache.avalon.framework.context.Contextualizable; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.components.ContextHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.commons.lang3.ArrayUtils; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.commons.lang3.tuple.Pair; 046 047import org.ametys.cms.CmsConstants; 048import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 049import org.ametys.cms.data.ContentDataHelper; 050import org.ametys.cms.repository.Content; 051import org.ametys.cms.repository.ContentQueryHelper; 052import org.ametys.cms.repository.ContentTypeExpression; 053import org.ametys.cms.repository.DefaultContent; 054import org.ametys.cms.repository.LanguageExpression; 055import org.ametys.cms.repository.ModifiableContent; 056import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 057import org.ametys.cms.rights.ContentRightAssignmentContext; 058import org.ametys.core.ui.Callable; 059import org.ametys.odf.course.Course; 060import org.ametys.odf.course.CourseContainer; 061import org.ametys.odf.course.ShareableCourseHelper; 062import org.ametys.odf.courselist.CourseList; 063import org.ametys.odf.courselist.CourseList.ChoiceType; 064import org.ametys.odf.courselist.CourseListContainer; 065import org.ametys.odf.coursepart.CoursePart; 066import org.ametys.odf.coursepart.CoursePartFactory; 067import org.ametys.odf.data.EducationalPath; 068import org.ametys.odf.data.type.EducationalPathRepositoryElementType; 069import org.ametys.odf.orgunit.OrgUnit; 070import org.ametys.odf.orgunit.OrgUnitFactory; 071import org.ametys.odf.orgunit.RootOrgUnitProvider; 072import org.ametys.odf.program.AbstractProgram; 073import org.ametys.odf.program.AbstractTraversableProgramPart; 074import org.ametys.odf.program.Container; 075import org.ametys.odf.program.Program; 076import org.ametys.odf.program.ProgramFactory; 077import org.ametys.odf.program.ProgramPart; 078import org.ametys.odf.program.SubProgram; 079import org.ametys.odf.program.TraversableProgramPart; 080import org.ametys.plugins.repository.AmetysObject; 081import org.ametys.plugins.repository.AmetysObjectExistsException; 082import org.ametys.plugins.repository.AmetysObjectIterable; 083import org.ametys.plugins.repository.AmetysObjectIterator; 084import org.ametys.plugins.repository.AmetysObjectResolver; 085import org.ametys.plugins.repository.AmetysRepositoryException; 086import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 087import org.ametys.plugins.repository.RepositoryConstants; 088import org.ametys.plugins.repository.UnknownAmetysObjectException; 089import org.ametys.plugins.repository.collection.AmetysObjectCollection; 090import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 091import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 092import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 093import org.ametys.plugins.repository.model.RepositoryDataContext; 094import org.ametys.plugins.repository.query.QueryHelper; 095import org.ametys.plugins.repository.query.SortCriteria; 096import org.ametys.plugins.repository.query.expression.AndExpression; 097import org.ametys.plugins.repository.query.expression.Expression; 098import org.ametys.plugins.repository.query.expression.Expression.Operator; 099import org.ametys.plugins.repository.query.expression.OrExpression; 100import org.ametys.plugins.repository.query.expression.StringExpression; 101import org.ametys.runtime.i18n.I18nizableText; 102import org.ametys.runtime.model.ModelHelper; 103import org.ametys.runtime.model.ModelItem; 104import org.ametys.runtime.model.type.DataContext; 105import org.ametys.runtime.plugin.component.AbstractLogEnabled; 106import org.ametys.runtime.plugin.component.PluginAware; 107 108import com.opensymphony.workflow.WorkflowException; 109 110/** 111 * Helper for ODF contents 112 * 113 */ 114public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware, Contextualizable 115{ 116 /** The component role. */ 117 public static final String ROLE = ODFHelper.class.getName(); 118 119 /** Request attribute to get the "Live" version of contents */ 120 public static final String REQUEST_ATTRIBUTE_VALID_LABEL = "live-version"; 121 122 /** The default id of initial workflow action */ 123 protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0; 124 125 /** Ametys object resolver */ 126 protected AmetysObjectResolver _resolver; 127 /** The content types manager */ 128 protected ContentTypeExtensionPoint _cTypeEP; 129 /** Root orgunit */ 130 protected RootOrgUnitProvider _ouRootProvider; 131 /** Helper for shareable course */ 132 protected ShareableCourseHelper _shareableCourseHelper; 133 /** The Avalon context */ 134 protected Context _context; 135 136 private String _pluginName; 137 138 public void contextualize(Context context) throws ContextException 139 { 140 _context = context; 141 } 142 143 @Override 144 public void service(ServiceManager manager) throws ServiceException 145 { 146 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 147 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 148 _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 149 _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE); 150 } 151 152 @Override 153 public void setPluginInfo(String pluginName, String featureName, String id) 154 { 155 _pluginName = pluginName; 156 } 157 158 /** 159 * Gets the root for ODF contents 160 * @return the root for ODF contents 161 */ 162 public AmetysObjectCollection getRootContent() 163 { 164 return getRootContent(false); 165 } 166 167 /** 168 * Gets the root for ODF contents 169 * @param create <code>true</code> to create automatically the root when missing. 170 * @return the root for ODF contents 171 */ 172 public AmetysObjectCollection getRootContent(boolean create) 173 { 174 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/"); 175 176 boolean needSave = false; 177 if (!pluginsNode.hasChild(_pluginName)) 178 { 179 if (create) 180 { 181 pluginsNode.createChild(_pluginName, "ametys:unstructured"); 182 needSave = true; 183 } 184 else 185 { 186 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing"); 187 } 188 } 189 190 ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName); 191 if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents")) 192 { 193 if (create) 194 { 195 pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection"); 196 needSave = true; 197 } 198 else 199 { 200 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing"); 201 } 202 } 203 204 if (needSave) 205 { 206 pluginsNode.saveChanges(); 207 } 208 209 return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 210 } 211 212 /** 213 * Get the {@link ProgramItem}s matching the given arguments 214 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 215 * @param code The code. Can be null to get program's items regardless of their code 216 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 217 * @param lang The search language. Can be null to get program's items regardless of their language 218 * @param <C> The content return type 219 * @return The matching program items 220 */ 221 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang) 222 { 223 return getProgramItems(cTypeId, code, catalogName, lang, null, null); 224 } 225 226 /** 227 * Get the {@link ProgramItem}s matching the given arguments 228 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 229 * @param code The code. Can be null to get program's items regardless of their code 230 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 231 * @param lang The search language. Can be null to get program's items regardless of their language 232 * @param <C> The content return type 233 * @return The matching program items 234 */ 235 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang) 236 { 237 return getProgramItems(cTypeIds, code, catalogName, lang, null, null); 238 } 239 240 /** 241 * Get the {@link ProgramItem}s matching the given arguments 242 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 243 * @param code The code. Can be null to get program's items regardless of their code 244 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 245 * @param lang The search language. Can be null to get program's items regardless of their language 246 * @param additionnalExpr An additional expression for filtering result. Can be null 247 * @param sortCriteria criteria for sorting results 248 * @param <C> The content return type 249 * @return The matching program items 250 */ 251 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 252 { 253 return getProgramItems(cTypeId != null ? Collections.singletonList(cTypeId) : Collections.EMPTY_LIST, code, catalogName, lang, additionnalExpr, sortCriteria); 254 } 255 256 /** 257 * Get the {@link ProgramItem}s matching the given arguments 258 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 259 * @param code The code. Can be null to get program's items regardless of their code 260 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 261 * @param lang The search language. Can be null to get program's items regardless of their language 262 * @param additionnalExpr An additional expression for filtering result. Can be null 263 * @param sortCriteria criteria for sorting results 264 * @param <C> The content return type 265 * @return The matching program items 266 */ 267 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 268 { 269 List<Expression> exprs = new ArrayList<>(); 270 271 if (!cTypeIds.isEmpty()) 272 { 273 exprs.add(new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()]))); 274 } 275 if (StringUtils.isNotEmpty(code)) 276 { 277 exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code)); 278 } 279 if (StringUtils.isNotEmpty(catalogName)) 280 { 281 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName)); 282 } 283 if (StringUtils.isNotEmpty(lang)) 284 { 285 exprs.add(new LanguageExpression(Operator.EQ, lang)); 286 } 287 if (additionnalExpr != null) 288 { 289 exprs.add(additionnalExpr); 290 } 291 292 Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 293 294 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria); 295 return _resolver.query(xpathQuery); 296 } 297 298 /** 299 * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language 300 * @param srcCoursePart The source course part 301 * @param catalogName The name of catalog to search into 302 * @param lang The search language 303 * @return The equivalent program item or <code>null</code> if not exists 304 */ 305 public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang) 306 { 307 return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang); 308 } 309 310 /** 311 * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 312 * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem} 313 * @param srcProgramItem The source program item 314 * @param catalogName The name of catalog to search into 315 * @param lang The search language 316 * @return The equivalent program item or <code>null</code> if not exists 317 */ 318 public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang) 319 { 320 return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang); 321 } 322 323 /** 324 * Get the equivalent {@link Content} having the same code in given catalog and language 325 * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject} 326 * @param contentType The content type to search for 327 * @param odfContentCode The code of the ODF content 328 * @param catalogName The name of catalog to search into 329 * @param lang The search language 330 * @return The equivalent content or <code>null</code> if not exists 331 */ 332 public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang) 333 { 334 Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType); 335 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 336 Expression catalogExpr = new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName); 337 Expression codeExpr = new StringExpression(ProgramItem.CODE, Operator.EQ, odfContentCode); 338 339 Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr); 340 341 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 342 AmetysObjectIterable<T> contents = _resolver.query(xpathQuery); 343 AmetysObjectIterator<T> contentsIt = contents.iterator(); 344 if (contentsIt.hasNext()) 345 { 346 return contentsIt.next(); 347 } 348 349 return null; 350 } 351 352 /** 353 * Get the child program items of a {@link ProgramItem} 354 * @param programItem The program item 355 * @return The child program items 356 */ 357 public List<ProgramItem> getChildProgramItems(ProgramItem programItem) 358 { 359 List<ProgramItem> children = new ArrayList<>(); 360 361 if (programItem instanceof TraversableProgramPart programPart) 362 { 363 children.addAll(programPart.getProgramPartChildren()); 364 } 365 366 if (programItem instanceof CourseContainer courseContainer) 367 { 368 children.addAll(courseContainer.getCourses()); 369 } 370 371 if (programItem instanceof Course course) 372 { 373 children.addAll(course.getCourseLists()); 374 } 375 376 return children; 377 } 378 379 /** 380 * Get the child subprograms of a {@link ProgramPart} 381 * @param programPart The program part 382 * @return The child subprograms 383 */ 384 public Set<SubProgram> getChildSubPrograms(ProgramPart programPart) 385 { 386 Set<SubProgram> subPrograms = new HashSet<>(); 387 388 if (programPart instanceof TraversableProgramPart traversableProgram) 389 { 390 if (programPart instanceof SubProgram subProgram) 391 { 392 subPrograms.add(subProgram); 393 } 394 traversableProgram.getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child))); 395 } 396 397 return subPrograms; 398 } 399 400 /** 401 * Gets (recursively) parent containers of this program item. 402 * @param programItem The program item 403 * @return parent containers of this program item. 404 */ 405 public Set<Container> getParentContainers(ProgramItem programItem) 406 { 407 return getParentContainers(programItem, false); 408 } 409 410 /** 411 * Gets (recursively) parent containers of this program item. 412 * @param programItem The program item 413 * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned. 414 * @return parent containers of this program item. 415 */ 416 public Set<Container> getParentContainers(ProgramItem programItem, boolean continueIfFound) 417 { 418 return _getParentsOfType(programItem, Container.class, continueIfFound); 419 } 420 421 /** 422 * Gets (recursively) parent programs of this course part. 423 * @param coursePart The course part 424 * @return parent programs of this course part. 425 */ 426 public Set<Program> getParentPrograms(CoursePart coursePart) 427 { 428 Set<Program> programs = new HashSet<>(); 429 for (Course course : coursePart.getCourses()) 430 { 431 programs.addAll(getParentPrograms(course)); 432 } 433 return programs; 434 } 435 436 /** 437 * Gets (recursively) parent programs of this program item. 438 * @param programItem The program item 439 * @return parent programs of this program item. 440 */ 441 public Set<Program> getParentPrograms(ProgramItem programItem) 442 { 443 return _getParentsOfType(programItem, Program.class, false); 444 } 445 446 /** 447 * Gets (recursively) parent subprograms of this course part. 448 * @param coursePart The course part 449 * @return parent subprograms of this course part. 450 */ 451 public Set<SubProgram> getParentSubPrograms(CoursePart coursePart) 452 { 453 return getParentSubPrograms(coursePart, false); 454 } 455 456 /** 457 * Gets (recursively) parent subprograms of this course part. 458 * @param coursePart The course part 459 * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned. 460 * @return parent subprograms of this course part. 461 */ 462 public Set<SubProgram> getParentSubPrograms(CoursePart coursePart, boolean continueIfFound) 463 { 464 Set<SubProgram> abstractPrograms = new HashSet<>(); 465 for (Course course : coursePart.getCourses()) 466 { 467 abstractPrograms.addAll(getParentSubPrograms(course, continueIfFound)); 468 } 469 return abstractPrograms; 470 } 471 472 /** 473 * Gets (recursively) parent subprograms of this program item. 474 * @param programItem The program item 475 * @return parent subprograms of this program item. 476 */ 477 public Set<SubProgram> getParentSubPrograms(ProgramItem programItem) 478 { 479 return getParentSubPrograms(programItem, false); 480 } 481 482 /** 483 * Gets (recursively) parent subprograms of this program item. 484 * @param programItem The program item 485 * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned. 486 * @return parent subprograms of this program item. 487 */ 488 public Set<SubProgram> getParentSubPrograms(ProgramItem programItem, boolean continueIfFound) 489 { 490 return _getParentsOfType(programItem, SubProgram.class, continueIfFound); 491 } 492 493 /** 494 * Gets (recursively) parent abstract programs of this course part. 495 * @param coursePart The course part 496 * @return parent abstract programs of this course part. 497 */ 498 public Set<AbstractProgram> getParentAbstractPrograms(CoursePart coursePart) 499 { 500 return getParentAbstractPrograms(coursePart, false); 501 } 502 503 /** 504 * Gets (recursively) parent abstract programs of this course part. 505 * @param coursePart The course part 506 * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned. 507 * @return parent abstract programs of this course part. 508 */ 509 public Set<AbstractProgram> getParentAbstractPrograms(CoursePart coursePart, boolean continueIfFound) 510 { 511 Set<AbstractProgram> abstractPrograms = new HashSet<>(); 512 for (Course course : coursePart.getCourses()) 513 { 514 abstractPrograms.addAll(getParentAbstractPrograms(course, continueIfFound)); 515 } 516 return abstractPrograms; 517 } 518 519 /** 520 * Gets (recursively) parent abstract programs of this program item. 521 * @param programItem The program item 522 * @return parent abstract programs of this program item. 523 */ 524 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem) 525 { 526 return getParentAbstractPrograms(programItem, false); 527 } 528 529 /** 530 * Gets (recursively) parent abstract programs of this program item. 531 * @param programItem The program item 532 * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned. 533 * @return parent abstract programs of this program item. 534 */ 535 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem, boolean continueIfFound) 536 { 537 return _getParentsOfType(programItem, AbstractProgram.class, continueIfFound); 538 } 539 540 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest, boolean continueIfFound) 541 { 542 Set<ProgramItem> visitedProgramItems = new HashSet<>(); 543 visitedProgramItems.add(programItem); 544 return _getParentsOfType(programItem, visitedProgramItems, classToTest, continueIfFound); 545 } 546 547 @SuppressWarnings("unchecked") 548 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest, boolean continueIfFound) 549 { 550 Set<T> parentsOfType = new HashSet<>(); 551 List<ProgramItem> parents = getParentProgramItems(programItem); 552 553 for (ProgramItem parent : parents) 554 { 555 // Only parents not already visited 556 if (visitedProgramItems.add(parent)) 557 { 558 // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram) 559 boolean found = false; 560 if (classToTest.isInstance(parent)) 561 { 562 parentsOfType.add((T) parent); 563 found = true; 564 } 565 566 if (!found || continueIfFound) 567 { 568 parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest, continueIfFound)); 569 } 570 } 571 } 572 573 return parentsOfType; 574 } 575 576 /** 577 * Get the child programs of an {@link OrgUnit} 578 * @param orgUnit the orgUnit, can be null 579 * @param catalog the catalog 580 * @param lang the lang 581 * @return The child programs 582 */ 583 public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang) 584 { 585 // Common expressions 586 AndExpression programExpression = new AndExpression(); 587 programExpression.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 588 programExpression.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 589 programExpression.add(new LanguageExpression(Operator.EQ, lang)); 590 591 // Can be null, it means that all programs for catalog and lang are selected 592 if (orgUnit != null) 593 { 594 OrExpression orgUnitExpression = new OrExpression(); 595 for (String orgUnitId : getSubOrgUnitIds(orgUnit)) 596 { 597 orgUnitExpression.add(new StringExpression(ProgramItem.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId)); 598 } 599 programExpression.add(orgUnitExpression); 600 } 601 602 // Execute the query 603 String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programExpression); 604 return _resolver.<Program>query(programQuery).stream().toList(); 605 } 606 607 /** 608 * Get the current orgunit and its suborgunits recursively identifiers. 609 * @param orgUnit The orgunit at the top 610 * @return A {@link List} of {@link OrgUnit} ids 611 */ 612 public List<String> getSubOrgUnitIds(OrgUnit orgUnit) 613 { 614 List<String> orgUnitIds = new ArrayList<>(); 615 orgUnitIds.add(orgUnit.getId()); 616 for (String id : orgUnit.getSubOrgUnits()) 617 { 618 OrgUnit childOrgUnit = _resolver.resolveById(id); 619 orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit)); 620 } 621 622 return orgUnitIds; 623 } 624 625 /** 626 * Determines if the {@link ProgramItem} has parent program items 627 * @param programItem The program item 628 * @return true if has parent program items 629 */ 630 public boolean hasParentProgramItems(ProgramItem programItem) 631 { 632 boolean hasParent = false; 633 634 if (programItem instanceof ProgramPart programPart) 635 { 636 hasParent = !programPart.getProgramPartParents().isEmpty() || hasParent; 637 } 638 639 if (programItem instanceof CourseList courseList) 640 { 641 hasParent = !courseList.getParentCourses().isEmpty() || hasParent; 642 } 643 644 if (programItem instanceof Course course) 645 { 646 hasParent = !course.getParentCourseLists().isEmpty() || hasParent; 647 } 648 649 return hasParent; 650 } 651 652 /** 653 * Get the parent program items of a {@link ProgramItem} 654 * @param programItem The program item 655 * @return The parent program items 656 */ 657 public List<ProgramItem> getParentProgramItems(ProgramItem programItem) 658 { 659 return getParentProgramItems(programItem, null); 660 } 661 662 /** 663 * Get the program item parents into the given ancestor {@link ProgramPart} 664 * @param programItem The program item 665 * @param parentProgramPart The parent program, subprogram or container. If null, all parents program items will be returned. 666 * @return The parent program items which have given parent program part has an ancestor 667 */ 668 public List<ProgramItem> getParentProgramItems(ProgramItem programItem, ProgramPart parentProgramPart) 669 { 670 List<ProgramItem> parents = new ArrayList<>(); 671 672 if (programItem instanceof Program) 673 { 674 return parents; 675 } 676 677 if (programItem instanceof ProgramPart programPart) 678 { 679 List<ProgramPart> allParents = programPart.getProgramPartParents(); 680 681 for (ProgramPart parent : allParents) 682 { 683 if (parentProgramPart == null || parent.equals(parentProgramPart)) 684 { 685 parents.add(parent); 686 } 687 else if (!getParentProgramItems(parent, parentProgramPart).isEmpty()) 688 { 689 parents.add(parent); 690 } 691 } 692 } 693 694 if (programItem instanceof CourseList courseList) 695 { 696 for (Course parentCourse : courseList.getParentCourses()) 697 { 698 if (!getParentProgramItems(parentCourse, parentProgramPart).isEmpty()) 699 { 700 parents.add(parentCourse); 701 } 702 } 703 } 704 705 if (programItem instanceof Course course) 706 { 707 for (CourseList cl : course.getParentCourseLists()) 708 { 709 if (!getParentProgramItems(cl, parentProgramPart).isEmpty()) 710 { 711 parents.add(cl); 712 } 713 714 } 715 } 716 717 return parents; 718 } 719 720 /** 721 * Get the first nearest program item parent into the given parent {@link AbstractProgram} 722 * @param programItem The program item 723 * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned. 724 * @return The parent program item or null if not found. 725 */ 726 public ProgramItem getParentProgramItem(ProgramItem programItem, AbstractProgram parentProgram) 727 { 728 List<ProgramItem> parentProgramItems = getParentProgramItems(programItem, parentProgram); 729 return parentProgramItems.isEmpty() ? null : parentProgramItems.get(0); 730 } 731 732 /** 733 * Get information of the program item 734 * @param programItemId the program item id 735 * @param programItemPathIds the list of program item ids containing in the path of the program item ... starting with itself. Can be null or empty 736 * @return a map of information 737 */ 738 @Callable 739 public Map<String, Object> getProgramItemInfo(String programItemId, List<String> programItemPathIds) 740 { 741 Map<String, Object> results = new HashMap<>(); 742 ProgramItem programItem = _resolver.resolveById(programItemId); 743 744 // Get catalog 745 String catalog = programItem.getCatalog(); 746 if (StringUtils.isNotBlank(catalog)) 747 { 748 results.put("catalog", catalog); 749 } 750 751 // Get the orgunits 752 List<String> orgUnits = programItem.getOrgUnits(); 753 if (programItemPathIds == null || programItemPathIds.isEmpty()) 754 { 755 // The programItemPathIds is null or empty because we do not know the program item context. 756 // so get the information in the parent structure if unique. 757 while (programItem != null && orgUnits.isEmpty()) 758 { 759 orgUnits = programItem.getOrgUnits(); 760 List<ProgramItem> parentProgramItems = getParentProgramItems(programItem); 761 programItem = parentProgramItems.size() == 1 ? parentProgramItems.get(0) : null; 762 } 763 } 764 else // We have the program item context: parent structure is known ... 765 { 766 // ... the first element of the programItemPathIds is the programItem itself, so begin to index 1 767 int position = 1; 768 int size = programItemPathIds.size(); 769 while (position < size && orgUnits.isEmpty()) 770 { 771 programItem = _resolver.resolveById(programItemPathIds.get(position)); 772 orgUnits = programItem.getOrgUnits(); 773 position++; 774 } 775 } 776 results.put("orgUnits", orgUnits); 777 778 return results; 779 } 780 781 /** 782 * Get information of the program item structure (type, if program has children) or orgunit (no structure for now) 783 * @param contentId the content id 784 * @return a map of information 785 */ 786 @Callable 787 public Map<String, Object> getStructureInfo(String contentId) 788 { 789 Map<String, Object> results = new HashMap<>(); 790 791 if (StringUtils.isNotBlank(contentId)) 792 { 793 Content content = _resolver.resolveById(contentId); 794 if (content instanceof ProgramItem programItem) 795 { 796 results.put("id", contentId); 797 results.put("title", content.getTitle()); 798 results.put("code", programItem.getCode()); 799 800 List<ProgramItem> childProgramItems = getChildProgramItems(programItem); 801 results.put("hasChildren", !childProgramItems.isEmpty()); 802 803 List<ProgramItem> parentProgramItems = getParentProgramItems(programItem); 804 results.put("hasParent", !parentProgramItems.isEmpty()); 805 806 results.put("paths", getPaths(programItem, " > ")); 807 } 808 else if (content instanceof OrgUnit orgunit) 809 { 810 results.put("id", contentId); 811 results.put("title", content.getTitle()); 812 results.put("code", orgunit.getUAICode()); 813 814 // Always to false, we don't manage complete copy with children 815 results.put("hasChildren", false); 816 817 results.put("hasParent", orgunit.getParentOrgUnit() != null); 818 819 results.put("paths", List.of(getOrgUnitPath(orgunit, " > "))); 820 } 821 } 822 823 return results; 824 } 825 826 /** 827 * Get information of the program item structure (type, if program has children) 828 * @param programItemIds the list of program item id 829 * @return a map of information 830 */ 831 @Callable 832 public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds) 833 { 834 Map<String, Map<String, Object>> results = new HashMap<>(); 835 836 for (String programItemId : programItemIds) 837 { 838 results.put(programItemId, getStructureInfo(programItemId)); 839 } 840 841 return results; 842 } 843 844 /** 845 * Get all the path of the orgunit.<br> 846 * The path is built with the contents' title and code 847 * @param orgunit The orgunit 848 * @param separator The path separator 849 * @return the path in parent orgunit 850 */ 851 public String getOrgUnitPath(OrgUnit orgunit, String separator) 852 { 853 String path = orgunit.getTitle() + " (" + orgunit.getUAICode() + ")"; 854 OrgUnit parent = orgunit.getParentOrgUnit(); 855 if (parent != null) 856 { 857 path = getOrgUnitPath(parent, separator) + separator + path; 858 } 859 return path; 860 } 861 862 /** 863 * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values 864 * The path is built with the contents' title and code 865 * @param item The program item 866 * @param separator The path separator 867 * @return the paths in parent program items 868 */ 869 public List<String> getPaths(ProgramItem item, String separator) 870 { 871 Function<ProgramItem, String> mapper = c -> ((Content) c).getTitle() + " (" + c.getCode() + ")"; 872 return getPaths(item, separator, mapper, true); 873 } 874 875 /** 876 * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values 877 * The path is built with the mapper function. 878 * @param item The program item 879 * @param separator The path separator 880 * @param mapper the function to apply to each program item to build the path 881 * @param includeItself set to false to not include final item in path 882 * @return the paths in parent program items 883 */ 884 public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, boolean includeItself) 885 { 886 return getPaths(item, separator, mapper, x -> true, includeItself, false); 887 } 888 889 /** 890 * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values 891 * The path is built with the mapper function. 892 * @param item The program item 893 * @param separator The path separator 894 * @param mapper the function to apply to each program item to build the path 895 * @param filterPathSegment predicate to exclude some program item of path 896 * @param includeItself set to false to not include final item in path 897 * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program 898 * @return the paths in parent program items 899 */ 900 public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, Predicate<ProgramItem> filterPathSegment, boolean includeItself, boolean ignoreOrphanPath) 901 { 902 List<EducationalPath> educationalPaths = getEducationalPaths(item, includeItself, ignoreOrphanPath); 903 904 return educationalPaths.stream() 905 .map(p -> getEducationalPathAsString(p, mapper, separator, filterPathSegment)) 906 .collect(Collectors.toList()); 907 } 908 909 /** 910 * Get the value of an attribute for a given educational path. The attribute is necessarily in a repeater composed of at least one educational path attribute and the attribute to be retrieved. 911 * @param <T> The type of the value returned by the path 912 * @param programItem The program item 913 * @param path Full or partial educational path. Cannot be null. In case of partial educational path (ie., no root program) the full possible paths will be computed. 914 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 915 * @return the value for this educational path 916 */ 917 public <T> Optional<T> getValueForPath(ProgramItem programItem, String dataPath, EducationalPath path) 918 { 919 return getValueForPath(programItem, dataPath, List.of(path)); 920 } 921 922 923 /** 924 * Get the value of an attribute for a given educational path. The attribute is necessarily in a repeater composed of at least one educational path attribute and the attribute to be retrieved. 925 * @param <T> The type of the value returned by the path 926 * @param programItem The program item 927 * @param paths Full educational paths (from root program). Cannot be null nor empty. 928 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 929 * @return the value for this educational path 930 */ 931 public <T> Optional<T> getValueForPath(ProgramItem programItem, String dataPath, List<EducationalPath> paths) 932 { 933 String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/"); 934 String attributeName = StringUtils.substringAfterLast(dataPath, "/"); 935 936 if (!((Content) programItem).hasValue(repeaterPath)) 937 { 938 return Optional.empty(); 939 } 940 941 if (paths.size() == 1) 942 { 943 ModelAwareRepeaterEntry entry = _getRepeaterEntryForPath(programItem, repeaterPath, paths.get(0)); 944 return entry != null ? Optional.ofNullable(entry.getValue(attributeName)) : Optional.empty(); 945 } 946 else 947 { 948 // For multiple full paths, can determine value only if values are equal for all full paths 949 // :( Use supplier to reuse stream for count and get (Collectors cannot be used because of possible null values) 950 @SuppressWarnings("unchecked") 951 Supplier<Stream<T>> values = () -> paths.stream() 952 .map(p -> _getRepeaterEntryForPath(programItem, repeaterPath, p)) 953 .map(e -> e != null ? (T) e.getValue(attributeName) : null) 954 .distinct(); 955 956 if (values.get().count() > 1) 957 { 958 // No same values for each path 959 getLogger().warn("Unable to determine value for '{}' attribute of content '{}'. Multiple educational paths are available for requested context with no same values", dataPath, programItem.getId()); 960 return Optional.empty(); 961 } 962 else 963 { 964 // Same value for each available paths => return this common value 965 return values.get().findFirst(); 966 } 967 } 968 } 969 970 /** 971 * Get the value of an attribute for each available educational paths 972 * @param <T> The type of the values returned by the path 973 * @param programItem The program item 974 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 975 * @param defaultValue The default value to use if the repeater contains no entry for this educational path. Can be null. 976 * @return the values for each educational paths 977 */ 978 public <T> Map<EducationalPath, T> getValuesForPaths(ProgramItem programItem, String dataPath, T defaultValue) 979 { 980 return getValuesForPaths(programItem, dataPath, getEducationalPaths(programItem), defaultValue); 981 } 982 983 /** 984 * Get the value of an attribute for each given educational paths 985 * @param <T> The type of the value returned by the path 986 * @param programItem The program item 987 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 988 * @param paths The full educational paths (from root programs) 989 * @param defaultValue The default value to use if the repeater contains no entry for this educational path. Can be null. 990 * @return the values for each educational paths 991 */ 992 @SuppressWarnings("unchecked") 993 public <T> Map<EducationalPath, T> getValuesForPaths(ProgramItem programItem, String dataPath, List<EducationalPath> paths, T defaultValue) 994 { 995 Map<EducationalPath, T> valuesByPath = new HashMap<>(); 996 997 paths.stream().forEach(path -> { 998 valuesByPath.put(path, (T) getValueForPath(programItem, dataPath, path).orElse(defaultValue)); 999 }); 1000 1001 return valuesByPath; 1002 } 1003 1004 /** 1005 * Determines if the values of an attribute depending of a educational path is the same for all available educational paths 1006 * @param programItem The program item 1007 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 1008 * @return true if the value is the same for all available educational paths 1009 */ 1010 public boolean isSameValueForAllPaths(ProgramItem programItem, String dataPath) 1011 { 1012 return isSameValueForPaths(programItem, dataPath, getEducationalPaths(programItem)); 1013 } 1014 1015 /** 1016 * Determines if the values of an attribute depending of a educational path is the same for all available educational paths 1017 * @param programItem The program item 1018 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 1019 * @param paths The full educational paths (from root programs) 1020 * @return true if the value is the same for all available educational paths 1021 */ 1022 public boolean isSameValueForPaths(ProgramItem programItem, String dataPath, List<EducationalPath> paths) 1023 { 1024 String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/"); 1025 if (!((Content) programItem).hasValue(repeaterPath)) 1026 { 1027 // Repeater is empty, the value is the default value for all educational paths 1028 return true; 1029 } 1030 1031 return getValuesForPaths(programItem, dataPath, paths, null).values().stream().distinct().count() == 1; 1032 } 1033 1034 /** 1035 * Get a position of repeater entry that match the given educational path 1036 * @param programItem The program item 1037 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 1038 * @param path The full educational path (from root program). Cannot be null. 1039 * @return the index position of the entry matching the educational path with a non-empty value for requested attribute, -1 otherwise 1040 */ 1041 public int getRepeaterEntryPositionForPath(ProgramItem programItem, String dataPath, EducationalPath path) 1042 { 1043 return getRepeaterEntryPositionForPath(programItem, dataPath, List.of(path)); 1044 } 1045 1046 /** 1047 * Get a position of repeater entry that match the given educational path 1048 * @param programItem The program item 1049 * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName) 1050 * @param paths The full educational paths (from root program). Cannot be null. 1051 * @return the index position of the entry matching the educational path with a non-empty value for requested attribute, -1 otherwise 1052 */ 1053 public int getRepeaterEntryPositionForPath(ProgramItem programItem, String dataPath, List<EducationalPath> paths) 1054 { 1055 String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/"); 1056 String attributeName = StringUtils.substringAfterLast(dataPath, "/"); 1057 1058 if (paths.size() == 1) 1059 { 1060 // Unique full path for this educational path, can determine position of entry holding the value for this path 1061 ModelAwareRepeaterEntry entry = _getRepeaterEntryForPath(programItem, repeaterPath, paths.get(0)); 1062 return entry != null && entry.hasValue(attributeName) ? entry.getPosition() : -1; 1063 } 1064 else 1065 { 1066 // For multiple educational paths, can determine value only if values are equal for all given paths 1067 // :( Use supplier to reuse stream for count and get (Collectors cannot be used because of possible null values) 1068 Supplier<Stream<Pair<Integer, Object>>> values = () -> paths.stream() 1069 .map(p -> _getRepeaterEntryForPath(programItem, repeaterPath, p)) 1070 .map(e -> Pair.of(e != null ? e.getPosition() : -1, e != null ? e.getValue(attributeName) : null)); 1071 1072 boolean isSameValues = values.get().map(Pair::getRight).distinct().count() == 1; 1073 if (!isSameValues) 1074 { 1075 // No same values for each path 1076 getLogger().warn("Unable to determine repeater entry for '{}' attribute of content '{}'. Multiple educational paths are available for requested context with no same values", dataPath, programItem.getId()); 1077 return -1; 1078 } 1079 else 1080 { 1081 // Same value for each available paths => return position of any entry 1082 Optional<Pair<Integer, Object>> first = values.get().findFirst(); 1083 return first.isPresent() ? first.get().getLeft() : -1; 1084 } 1085 } 1086 } 1087 1088 private ModelAwareRepeaterEntry _getRepeaterEntryForPath(ProgramItem programItem, String repeaterPath, EducationalPath path) 1089 { 1090 if (((Content) programItem).hasValue(repeaterPath)) 1091 { 1092 ModelAwareRepeater repeater = ((Content) programItem).getRepeater(repeaterPath); 1093 1094 // Get the name of attribute holding the education path in repeater's entries 1095 List<ModelItem> educationalPathModelItem = ModelHelper.findModelItemsByType(repeater.getModel(), EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID); 1096 if (educationalPathModelItem.isEmpty() || educationalPathModelItem.size() > 1) 1097 { 1098 getLogger().error("Unable to determine repeater entry matching an education path. No attribute or several attributes of type '{}' found.", EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID, repeaterPath); 1099 } 1100 else 1101 { 1102 String pathAttributeName = educationalPathModelItem.get(0).getName(); 1103 1104 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 1105 { 1106 if (path.equals(entry.getValue(pathAttributeName))) 1107 { 1108 return entry; 1109 } 1110 } 1111 } 1112 } 1113 1114 return null; 1115 } 1116 1117 /** 1118 * Get the full educations paths (from a root {@link Program}) from a full or partial path 1119 * @param path A full or partial path composed by program item ancestors 1120 * @return the full educational paths 1121 */ 1122 public List<EducationalPath> getEducationPathFromPath(List<ProgramItem> path) 1123 { 1124 return getEducationPathFromPaths(List.of(path)); 1125 } 1126 1127 /** 1128 * Get the full educations paths (from a root {@link Program}) from full or partial paths 1129 * @param paths full or partial paths composed by program item ancestors 1130 * @return the full educational paths 1131 */ 1132 public List<EducationalPath> getEducationPathFromPaths(List<List<ProgramItem>> paths) 1133 { 1134 return getEducationPathFromPaths(paths, null); 1135 } 1136 1137 /** 1138 * Get the full educations paths (from a root {@link Program})from full or partial paths 1139 * @param paths full or partial paths composed by program item ancestors 1140 * @param withAncestor filter the educational paths that contains this ancestor. Can be null. 1141 * @return the full educational paths 1142 */ 1143 public List<EducationalPath> getEducationPathFromPaths(List<List<ProgramItem>> paths, ProgramItem withAncestor) 1144 { 1145 List<EducationalPath> fullPaths = new ArrayList<>(); 1146 1147 for (List<ProgramItem> partialPath : paths) 1148 { 1149 ProgramItem firstProgramItem = partialPath.get(0); 1150 if (!(firstProgramItem instanceof Program)) 1151 { 1152 // First program item of path is not a root program => computed the available full paths from this first item ancestors 1153 List<EducationalPath> parentEducationalPaths = getEducationalPaths(firstProgramItem, false); 1154 1155 fullPaths.addAll(parentEducationalPaths.stream() 1156 .filter(p -> withAncestor == null || p.getProgramItems(_resolver).contains(withAncestor)) // filter educational paths that is not composed by the required ancestor 1157 .map(p -> EducationalPath.of(p, partialPath.toArray(ProgramItem[]::new))) // concat path 1158 .toList()); 1159 } 1160 else if (withAncestor == null || partialPath.contains(withAncestor)) 1161 { 1162 // The path is already a full path 1163 fullPaths.add(EducationalPath.of(partialPath.toArray(ProgramItem[]::new))); 1164 } 1165 } 1166 1167 return fullPaths; 1168 } 1169 1170 /** 1171 * Get all {@link EducationalPath} of a {@link ProgramItem} 1172 * The path is built with the mapper function. 1173 * @param programItem The program item 1174 * @return the paths in parent program items 1175 */ 1176 public List<EducationalPath> getEducationalPaths(ProgramItem programItem) 1177 { 1178 return getEducationalPaths(programItem, true); 1179 } 1180 1181 /** 1182 * Get all {@link EducationalPath} of a {@link ProgramItem} 1183 * The path is built with the mapper function. 1184 * @param programItem The program item 1185 * @param includeItself set to false to not include final item in path 1186 * @return the paths in parent program items 1187 */ 1188 public List<EducationalPath> getEducationalPaths(ProgramItem programItem, boolean includeItself) 1189 { 1190 return getEducationalPaths(programItem, includeItself, false); 1191 } 1192 1193 /** 1194 * Get all {@link EducationalPath} of a {@link ProgramItem} 1195 * The path is built with the mapper function. 1196 * @param programItem The program item 1197 * @param includeItself set to false to not include final item in path 1198 * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program 1199 * @return the paths in parent program items 1200 */ 1201 public List<EducationalPath> getEducationalPaths(ProgramItem programItem, boolean includeItself, boolean ignoreOrphanPath) 1202 { 1203 List<EducationalPath> paths = new ArrayList<>(); 1204 1205 List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(programItem); 1206 for (List<ProgramItem> ancestorPath : ancestorPaths) 1207 { 1208 if (!ignoreOrphanPath || ancestorPath.get(0) instanceof Program) // ignore paths that is not part of a Program if ignoreOrphanPath is true 1209 { 1210 if (!includeItself) 1211 { 1212 ancestorPath.remove(programItem); 1213 } 1214 1215 if (!ancestorPath.isEmpty()) 1216 { 1217 paths.add(EducationalPath.of(ancestorPath.toArray(new ProgramItem[ancestorPath.size()]))); 1218 } 1219 } 1220 } 1221 1222 return paths; 1223 } 1224 1225 /** 1226 * Get a readable value of a {@link EducationalPath} 1227 * @param path the educational path 1228 * @return a String representing the path with program item's title separated by '>' 1229 */ 1230 public String getEducationalPathAsString(EducationalPath path) 1231 { 1232 return getEducationalPathAsString(path, pi -> ((Content) pi).getTitle(), " > "); 1233 } 1234 1235 /** 1236 * Get a readable value of a {@link EducationalPath} 1237 * @param path the educational path 1238 * @param mapper the function to use for the readable value of a program item 1239 * @param separator the separator to use 1240 * @return a String representing the path with program item's readable value given by mapper function and separated by given separator 1241 */ 1242 public String getEducationalPathAsString(EducationalPath path, Function<ProgramItem, String> mapper, CharSequence separator) 1243 { 1244 return getEducationalPathAsString(path, mapper, separator, x -> true); 1245 } 1246 1247 /** 1248 * Get a readable value of a {@link EducationalPath} 1249 * @param path the educational path 1250 * @param mapper the function to use for the readable value of a program item 1251 * @param separator the separator to use 1252 * @param filterPathSegment predicate to exclude some program item of path 1253 * @return a String representing the path with program item's readable value given by mapper function and separated by given separator 1254 */ 1255 public String getEducationalPathAsString(EducationalPath path, Function<ProgramItem, String> mapper, CharSequence separator, Predicate<ProgramItem> filterPathSegment) 1256 { 1257 return path.resolveProgramItems(_resolver) 1258 .filter(filterPathSegment) 1259 .map(mapper) 1260 .collect(Collectors.joining(separator)); 1261 } 1262 1263 1264 /** 1265 * Get the full path to program item for highest ancestors. The path includes this final item. 1266 * @param item the program item 1267 * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item. 1268 */ 1269 protected List<List<ProgramItem>> getPathOfAncestors(ProgramItem item) 1270 { 1271 List<List<ProgramItem>> ancestors = new ArrayList<>(); 1272 1273 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 1274 if (parentProgramItems.isEmpty()) 1275 { 1276 List<ProgramItem> items = new ArrayList<>(); 1277 items.add(item); 1278 ancestors.add(items); 1279 return ancestors; 1280 } 1281 1282 for (ProgramItem parentProgramItem : parentProgramItems) 1283 { 1284 for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem)) 1285 { 1286 ancestorPaths.add(item); 1287 ancestors.add(ancestorPaths); 1288 } 1289 } 1290 1291 return ancestors; 1292 } 1293 1294 /** 1295 * Get the enumeration of educational paths for a program item for highest ancestors. The paths does not includes this final item. 1296 * @param programItemId the id of program item 1297 * @return a list of educational paths with paths' label (composed by ancestors' title separated by '>') and paths' id (composed by ancestors' id seperated by coma) 1298 */ 1299 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 1300 public List<Map<String, String>> getEducationalPathsEnumeration(String programItemId) 1301 { 1302 List<Map<String, String>> paths = new ArrayList<>(); 1303 1304 ProgramItem programItem = _resolver.resolveById(programItemId); 1305 1306 getEducationalPaths(programItem) 1307 .stream() 1308 .forEach(p -> paths.add(Map.of("id", p.toString(), "title", getEducationalPathAsString(p, c -> ((Content) c).getTitle(), " > ")))); 1309 1310 return paths; 1311 } 1312 1313 1314 /** 1315 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 1316 * The path is construct with the contents' names and the used separator is '/'. 1317 * @param programItemId The id of the program item 1318 * @param programId The id of program. Can not be null. 1319 * @return the path into the parent program or null if the item is not part of this program. 1320 */ 1321 @Callable 1322 public String getPathInProgram (String programItemId, String programId) 1323 { 1324 ProgramItem item = _resolver.resolveById(programItemId); 1325 Program program = _resolver.resolveById(programId); 1326 1327 return getPathInProgram(item, program); 1328 } 1329 1330 /** 1331 * Get the path of a ODF content into a {@link Program}.<br> 1332 * The path is construct with the contents' names and the used separator is '/'. 1333 * @param item The program item 1334 * @param parentProgram The parent root (sub)program. Can not be null. 1335 * @return the path from the parent program 1336 */ 1337 public String getPathInProgram (ProgramItem item, Program parentProgram) 1338 { 1339 if (item instanceof Program) 1340 { 1341 // The program item is already the program it self or another program 1342 return item.equals(parentProgram) ? "" : null; 1343 } 1344 1345 List<EducationalPath> paths = getEducationalPaths(item, true, true); 1346 1347 for (EducationalPath path : paths) 1348 { 1349 if (path.getProgramItemIds().contains(parentProgram.getId())) 1350 { 1351 // Find a path that match the given parent program 1352 Stream<ProgramItem> resolvedPath = path.resolveProgramItems(_resolver); 1353 return resolvedPath.map(ProgramItem::getName).collect(Collectors.joining("/")); 1354 } 1355 } 1356 1357 return null; 1358 } 1359 1360 /** 1361 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 1362 * The path is construct with the contents' names and the used separator is '/'. 1363 * @param contentId The id of the content 1364 * @param parentCourseId The id of parent course. Can not be null. 1365 * @return the path into the parent course or null if the item is not part of this course. 1366 */ 1367 @Callable 1368 public String getPathInCourse (String contentId, String parentCourseId) 1369 { 1370 Content content = _resolver.resolveById(contentId); 1371 Course parentCourse = _resolver.resolveById(parentCourseId); 1372 1373 return getPathInCourse(content, parentCourse); 1374 } 1375 1376 /** 1377 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 1378 * The path is construct with the contents' names and the used separator is '/'. 1379 * @param courseOrList The course or the course list 1380 * @param parentCourse The parent course. Can not be null. 1381 * @return the path into the parent course or null if the item is not part of this course. 1382 */ 1383 public String getPathInCourse(Content courseOrList, Course parentCourse) 1384 { 1385 if (courseOrList.equals(parentCourse)) 1386 { 1387 return ""; 1388 } 1389 1390 String path = _getPathInCourse(courseOrList, parentCourse); 1391 1392 return path; 1393 } 1394 1395 private String _getPathInCourse(Content content, Content parentContent) 1396 { 1397 if (content.equals(parentContent)) 1398 { 1399 return content.getName(); 1400 } 1401 1402 List<? extends Content> parents; 1403 1404 if (content instanceof Course course) 1405 { 1406 parents = course.getParentCourseLists(); 1407 } 1408 else if (content instanceof CourseList courseList) 1409 { 1410 parents = courseList.getParentCourses(); 1411 } 1412 else 1413 { 1414 throw new IllegalStateException(); 1415 } 1416 1417 for (Content parent : parents) 1418 { 1419 String path = _getPathInCourse(parent, parentContent); 1420 if (path != null) 1421 { 1422 return path + '/' + content.getName(); 1423 } 1424 } 1425 return null; 1426 } 1427 1428 /** 1429 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 1430 * The path is construct with the contents' names and the used separator is '/'. 1431 * @param orgUnitId The id of the orgunit 1432 * @param rootOrgUnitId The root orgunit id 1433 * @return the path into the parent program or null if the item is not part of this program. 1434 */ 1435 @Callable 1436 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 1437 { 1438 OrgUnit rootOU = null; 1439 if (StringUtils.isNotBlank(rootOrgUnitId)) 1440 { 1441 rootOU = _resolver.resolveById(rootOrgUnitId); 1442 } 1443 else 1444 { 1445 rootOU = _ouRootProvider.getRoot(); 1446 } 1447 1448 if (orgUnitId.equals(rootOU.getId())) 1449 { 1450 // The orgunit is already the root orgunit 1451 return rootOU.getName(); 1452 } 1453 1454 OrgUnit ou = _resolver.resolveById(orgUnitId); 1455 1456 List<String> paths = new ArrayList<>(); 1457 paths.add(ou.getName()); 1458 1459 OrgUnit parent = ou.getParentOrgUnit(); 1460 while (parent != null && !parent.getId().equals(rootOU.getId())) 1461 { 1462 paths.add(parent.getName()); 1463 parent = parent.getParentOrgUnit(); 1464 } 1465 1466 if (parent != null) 1467 { 1468 paths.add(rootOU.getName()); 1469 Collections.reverse(paths); 1470 return StringUtils.join(paths, "/"); 1471 } 1472 1473 return null; 1474 } 1475 1476 /** 1477 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 1478 * The path is construct with the contents' names and the used separator is '/'. 1479 * @param orgUnitId The id of the orgunit 1480 * @return the path into the parent program or null if the item is not part of this program. 1481 */ 1482 @Callable 1483 public String getOrgUnitPath(String orgUnitId) 1484 { 1485 return getOrgUnitPath(orgUnitId, null); 1486 } 1487 1488 /** 1489 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 1490 * @param part The program part 1491 * @param parentId The ancestor id 1492 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 1493 */ 1494 public boolean hasAncestor (ProgramPart part, String parentId) 1495 { 1496 List<ProgramPart> parents = part.getProgramPartParents(); 1497 1498 for (ProgramPart parent : parents) 1499 { 1500 if (parent.getId().equals(parentId)) 1501 { 1502 return true; 1503 } 1504 else if (hasAncestor(parent, parentId)) 1505 { 1506 return true; 1507 } 1508 } 1509 1510 return false; 1511 } 1512 1513 /** 1514 * Determines if a program item is shared 1515 * @param programItem the program item 1516 * @return true if the program item is shared 1517 */ 1518 public boolean isShared(ProgramItem programItem) 1519 { 1520 List<ProgramItem> parents = getParentProgramItems(programItem); 1521 if (parents.size() > 1) 1522 { 1523 return true; 1524 } 1525 else 1526 { 1527 return parents.isEmpty() ? false : isShared(parents.get(0)); 1528 } 1529 } 1530 1531 /** 1532 * Check if a relation can be establish between two ODF contents 1533 * @param srcContent The source content (copied or moved) 1534 * @param targetContent The target content 1535 * @param errors The list of error messages 1536 * @param contextualParameters the contextual parameters 1537 * @return true if the relation is valid, false otherwise 1538 */ 1539 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 1540 { 1541 boolean isCompatible = true; 1542 1543 if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit) 1544 { 1545 if (!_isContentTypeCompatible(srcContent, targetContent)) 1546 { 1547 // Invalid relations between content types 1548 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 1549 isCompatible = false; 1550 } 1551 else if (!_isCatalogCompatible(srcContent, targetContent)) 1552 { 1553 // Catalog is invalid 1554 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 1555 isCompatible = false; 1556 } 1557 else if (!_isLanguageCompatible(srcContent, targetContent)) 1558 { 1559 // Language is invalid 1560 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 1561 isCompatible = false; 1562 } 1563 else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters)) 1564 { 1565 // Shareable fields don't match 1566 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 1567 isCompatible = false; 1568 } 1569 } 1570 else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit) 1571 { 1572 // If the target isn't ODF related but the source is, the relation is not compatible. 1573 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 1574 isCompatible = false; 1575 } 1576 1577 return isCompatible; 1578 } 1579 1580 /** 1581 * Get the name of attribute holding the relation between a parent content and its children 1582 * @param parentProgramItem the parent content 1583 * @param childProgramItem the child content 1584 * @return the name of attribute the child relation 1585 */ 1586 public String getDescendantRelationAttributeName(ProgramItem parentProgramItem, ProgramItem childProgramItem) 1587 { 1588 if (parentProgramItem instanceof CourseList && childProgramItem instanceof Course) 1589 { 1590 return CourseList.CHILD_COURSES; 1591 } 1592 else if (parentProgramItem instanceof Course && childProgramItem instanceof CourseList) 1593 { 1594 return Course.CHILD_COURSE_LISTS; 1595 } 1596 else if (parentProgramItem instanceof Course && childProgramItem instanceof CoursePart) 1597 { 1598 return Course.CHILD_COURSE_PARTS; 1599 } 1600 else if (parentProgramItem instanceof TraversableProgramPart && childProgramItem instanceof ProgramPart) 1601 { 1602 return TraversableProgramPart.CHILD_PROGRAM_PARTS; 1603 } 1604 1605 return null; 1606 } 1607 1608 private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList) 1609 { 1610 return courseList.getCourses().contains(course); 1611 } 1612 1613 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 1614 { 1615 if (srcContent instanceof Container || srcContent instanceof SubProgram) 1616 { 1617 return targetContent instanceof AbstractTraversableProgramPart; 1618 } 1619 else if (srcContent instanceof CourseList) 1620 { 1621 return targetContent instanceof CourseListContainer; 1622 } 1623 else if (srcContent instanceof Course) 1624 { 1625 return targetContent instanceof CourseList; 1626 } 1627 else if (srcContent instanceof OrgUnit) 1628 { 1629 return targetContent instanceof OrgUnit; 1630 } 1631 1632 return false; 1633 } 1634 1635 private boolean _isCatalogCompatible(Content srcContent, Content targetContent) 1636 { 1637 if (srcContent instanceof ProgramItem srcProgramItem && targetContent instanceof ProgramItem targetProgramItem) 1638 { 1639 return srcProgramItem.getCatalog().equals(targetProgramItem.getCatalog()); 1640 } 1641 return true; 1642 } 1643 1644 private boolean _isLanguageCompatible(Content srcContent, Content targetContent) 1645 { 1646 return srcContent.getLanguage().equals(targetContent.getLanguage()); 1647 } 1648 1649 private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters) 1650 { 1651 // We check shareable fields only if the course content is not created (or created by copy) and not moved 1652 if (srcContent instanceof Course srcCourse 1653 && targetContent instanceof CourseList targetCourseList 1654 && _shareableCourseHelper.handleShareableCourse() 1655 && !"create".equals(contextualParameters.get("mode")) 1656 && !"copy".equals(contextualParameters.get("mode")) 1657 && !"move".equals(contextualParameters.get("mode")) 1658 // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields 1659 && !_isCourseAlreadyBelongToCourseList(srcCourse, targetCourseList)) 1660 { 1661 return _shareableCourseHelper.isShareableFieldsMatch(srcCourse, targetCourseList); 1662 } 1663 1664 return true; 1665 } 1666 1667 private List<String> _getContentParameters(Content srcContent, Content targetContent) 1668 { 1669 List<String> parameters = new ArrayList<>(); 1670 parameters.add(srcContent.getTitle()); 1671 parameters.add(srcContent.getId()); 1672 parameters.add(targetContent.getTitle()); 1673 parameters.add(targetContent.getId()); 1674 return parameters; 1675 } 1676 /** 1677 * Copy a {@link ProgramItem} 1678 * @param srcContent The program item to copy 1679 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1680 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1681 * @param copiedContents the initial contents with their copied content 1682 * @return The created content 1683 * @param <C> The modifiable content return type 1684 * @throws AmetysRepositoryException If an error occurred during copy 1685 * @throws WorkflowException If an error occurred during copy 1686 */ 1687 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1688 { 1689 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents); 1690 } 1691 1692 /** 1693 * Copy a {@link ProgramItem} 1694 * @param srcContent The program item to copy 1695 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1696 * @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. 1697 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1698 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1699 * @param copiedContents the initial contents with their copied content 1700 * @param <C> The modifiable content return type 1701 * @return The created content 1702 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1703 * @throws AmetysRepositoryException If an error occurred 1704 * @throws WorkflowException If an error occurred 1705 */ 1706 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1707 { 1708 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents); 1709 } 1710 1711 /** 1712 * Copy a {@link CoursePart} 1713 * @param srcContent The course part to copy 1714 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1715 * @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. 1716 * @param initWorkflowActionId The initial workflow action id 1717 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1718 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1719 * @param copiedContents the initial contents with their copied content 1720 * @param <C> The modifiable content return type 1721 * @return The created content 1722 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1723 * @throws AmetysRepositoryException If an error occurred 1724 * @throws WorkflowException If an error occurred 1725 */ 1726 public <C extends ModifiableContent> C copyCoursePart(CoursePart srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1727 { 1728 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents); 1729 } 1730 1731 /** 1732 * Copy a {@link ProgramItem} 1733 * @param srcContent The program item to copy 1734 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1735 * @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. 1736 * @param initWorkflowActionId The initial workflow action id 1737 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1738 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1739 * @param copiedContents the initial contents with their copied content 1740 * @param <C> The modifiable content return type 1741 * @return The created content 1742 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1743 * @throws AmetysRepositoryException If an error occurred 1744 * @throws WorkflowException If an error occurred 1745 */ 1746 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1747 { 1748 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents); 1749 } 1750 1751 /** 1752 * Copy a {@link ProgramItem}. Also copy the synchronization metadata (status and alternative value) 1753 * @param srcContent The program item to copy 1754 * @param catalog The catalog 1755 * @param code The odf content code 1756 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1757 * @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. 1758 * @param initWorkflowActionId The initial workflow action id 1759 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1760 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1761 * @param copiedContents the initial contents with their copied content 1762 * @param <C> The modifiable content return type 1763 * @return The created content 1764 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1765 * @throws AmetysRepositoryException If an error occurred 1766 * @throws WorkflowException If an error occurred 1767 */ 1768 @SuppressWarnings("unchecked") 1769 private <C extends ModifiableContent> C _copyODFContent(Content srcContent, String catalog, String code, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1770 { 1771 String computedTargetLanguage = targetContentLanguage; 1772 if (computedTargetLanguage == null) 1773 { 1774 computedTargetLanguage = srcContent.getLanguage(); 1775 } 1776 1777 String computeTargetName = targetContentName; 1778 if (computeTargetName == null) 1779 { 1780 // Compute content name from source content and requested language 1781 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 1782 } 1783 1784 String computeTargetCatalog = targetCatalog; 1785 if (computeTargetCatalog == null) 1786 { 1787 computeTargetCatalog = catalog; 1788 } 1789 1790 String principalContentType = srcContent.getTypes()[0]; 1791 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 1792 if (createdContent != null) 1793 { 1794 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 1795 } 1796 else 1797 { 1798 // Copy content waiting for observers to be completed and copying ACL 1799 DataContext context = RepositoryDataContext.newInstance() 1800 .withExternalMetadataInCopy(true); 1801 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true, context); 1802 1803 if (fullCopy) 1804 { 1805 _cleanContentMetadata(createdContent); 1806 1807 if (targetCatalog != null) 1808 { 1809 if (createdContent instanceof ProgramItem programItem) 1810 { 1811 programItem.setCatalog(targetCatalog); 1812 } 1813 else if (createdContent instanceof CoursePart coursePart) 1814 { 1815 coursePart.setCatalog(targetCatalog); 1816 } 1817 1818 } 1819 1820 if (srcContent instanceof ProgramItem programItem) 1821 { 1822 copyProgramItemStructure(programItem, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedContents); 1823 } 1824 1825 createdContent.saveChanges(); 1826 } 1827 1828 copiedContents.put(srcContent, createdContent); 1829 } 1830 1831 return (C) createdContent; 1832 } 1833 1834 /** 1835 * Copy the structure of a {@link ProgramItem} 1836 * @param srcContent the content to copy 1837 * @param targetContent the target content 1838 * @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. 1839 * @param initWorkflowActionId The initial workflow action id 1840 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1841 * @param copiedContents the initial contents with their copied content 1842 * @throws AmetysRepositoryException If an error occurred during copy 1843 * @throws WorkflowException If an error occurred during copy 1844 */ 1845 protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1846 { 1847 List<ProgramItem> srcChildContents = new ArrayList<>(); 1848 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1849 1850 String childMetadataPath = null; 1851 String parentMetadataPath = null; 1852 1853 if (srcContent instanceof TraversableProgramPart programPart) 1854 { 1855 childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS; 1856 parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS; 1857 srcChildContents.addAll(programPart.getProgramPartChildren()); 1858 } 1859 else if (srcContent instanceof CourseList courseList) 1860 { 1861 childMetadataPath = CourseList.CHILD_COURSES; 1862 parentMetadataPath = Course.PARENT_COURSE_LISTS; 1863 srcChildContents.addAll(courseList.getCourses()); 1864 } 1865 else if (srcContent instanceof Course course) 1866 { 1867 childMetadataPath = Course.CHILD_COURSE_LISTS; 1868 parentMetadataPath = CourseList.PARENT_COURSES; 1869 srcChildContents.addAll(course.getCourseLists()); 1870 1871 List<String> refCoursePartIds = new ArrayList<>(); 1872 for (CoursePart srcChildContent : course.getCourseParts()) 1873 { 1874 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents); 1875 refCoursePartIds.add(targetChildContent.getId()); 1876 } 1877 _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds); 1878 } 1879 1880 List<String> refChildIds = new ArrayList<>(); 1881 for (ProgramItem srcChildContent : srcChildContents) 1882 { 1883 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents); 1884 refChildIds.add(targetChildContent.getId()); 1885 } 1886 1887 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1888 1889 _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values); 1890 } 1891 1892 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1893 { 1894 if (!refChildIds.isEmpty()) 1895 { 1896 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1897 } 1898 } 1899 1900 private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1901 { 1902 if (!values.isEmpty()) 1903 { 1904 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1905 { 1906 String childMetadataName = entry.getKey().getLeft(); 1907 String parentMetadataName = entry.getKey().getRight(); 1908 List<String> childContents = entry.getValue(); 1909 1910 parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()])); 1911 1912 for (String childContentId : childContents) 1913 { 1914 ModifiableContent content = _resolver.resolveById(childContentId); 1915 String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName); 1916 content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId())); 1917 content.saveChanges(); 1918 } 1919 } 1920 } 1921 } 1922 1923 /** 1924 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1925 * @param createdContent The created content to clean 1926 */ 1927 protected void _cleanContentMetadata(ModifiableContent createdContent) 1928 { 1929 if (createdContent instanceof ProgramPart) 1930 { 1931 _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS); 1932 } 1933 1934 if (createdContent instanceof TraversableProgramPart) 1935 { 1936 _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS); 1937 } 1938 1939 if (createdContent instanceof CourseList) 1940 { 1941 _removeFullValue(createdContent, CourseList.CHILD_COURSES); 1942 _removeFullValue(createdContent, CourseList.PARENT_COURSES); 1943 } 1944 1945 if (createdContent instanceof Course) 1946 { 1947 _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS); 1948 _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS); 1949 _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS); 1950 } 1951 1952 if (createdContent instanceof CoursePart) 1953 { 1954 _removeFullValue(createdContent, CoursePart.PARENT_COURSES); 1955 } 1956 } 1957 1958 private void _removeFullValue(ModifiableContent content, String attributeName) 1959 { 1960 content.removeValue(attributeName); 1961 content.removeExternalizableMetadataIfExists(attributeName); 1962 } 1963 1964 /** 1965 * Switch the ametys object to Live version if it has one 1966 * @param ao the Ametys object 1967 * @throws NoLiveVersionException if the content has no live version 1968 */ 1969 public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException 1970 { 1971 // Switch to the Live label if exists 1972 String[] allLabels = ao.getAllLabels(); 1973 String[] currentLabels = ao.getLabels(); 1974 1975 boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL); 1976 boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL); 1977 1978 if (hasLiveVersion && !currentVersionIsLive) 1979 { 1980 ao.switchToLabel(CmsConstants.LIVE_LABEL); 1981 } 1982 else if (!hasLiveVersion) 1983 { 1984 throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version"); 1985 } 1986 } 1987 1988 /** 1989 * Switch to Live version if is required 1990 * @param ao the Ametys object 1991 * @throws NoLiveVersionException if the Live version is required but not exist 1992 */ 1993 public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException 1994 { 1995 Request request = _getRequest(); 1996 if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null) 1997 { 1998 switchToLiveVersion(ao); 1999 } 2000 } 2001 2002 /** 2003 * Count the hours accumulation in the {@link ProgramItem} 2004 * @param programItem The program item on which we compute the total number of hours 2005 * @return The hours accumulation 2006 */ 2007 public Double getCumulatedHours(ProgramItem programItem) 2008 { 2009 // Ignore optional course list and avoid useless expensive calls 2010 if (programItem instanceof CourseList courseList && ChoiceType.OPTIONAL.equals(courseList.getType())) 2011 { 2012 return 0.0; 2013 } 2014 2015 List<ProgramItem> children = getChildProgramItems(programItem); 2016 2017 Double coef = 1.0; 2018 Double countNbHours = 0.0; 2019 2020 // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total) 2021 if (programItem instanceof CourseList courseList) 2022 { 2023 // If there is no children, compute the coef is useless 2024 // Also choice list can throw an exception while dividing by zero 2025 if (children.isEmpty()) 2026 { 2027 return 0.0; 2028 } 2029 2030 switch (courseList.getType()) 2031 { 2032 case CHOICE: 2033 // Apply the average of number of EC from children multiply by the minimum ELP to select 2034 coef = ((double) courseList.getMinNumberOfCourses()) / children.size(); 2035 break; 2036 case MANDATORY: 2037 default: 2038 // Add all ECTS from children 2039 break; 2040 } 2041 } 2042 2043 // If it's a course and we have a value for the number of hours 2044 // Then get the value 2045 if (programItem instanceof Course course && course.hasValue(Course.NUMBER_OF_HOURS)) 2046 { 2047 countNbHours += course.<Double>getValue(Course.NUMBER_OF_HOURS); 2048 } 2049 // Else if there are program item children on the item 2050 // Then compute on children 2051 else if (children.size() > 0) 2052 { 2053 for (ProgramItem child : children) 2054 { 2055 countNbHours += getCumulatedHours(child); 2056 } 2057 } 2058 // Else, it's a course but there is no value for the number of hours and we don't have program item children 2059 // Then compute on course parts 2060 else if (programItem instanceof Course course) 2061 { 2062 countNbHours += course.getCourseParts() 2063 .stream() 2064 .mapToDouble(CoursePart::getNumberOfHours) 2065 .sum(); 2066 } 2067 2068 return coef * countNbHours; 2069 } 2070 2071 /** 2072 * Get the request 2073 * @return the request 2074 */ 2075 protected Request _getRequest() 2076 { 2077 return ContextHelper.getRequest(_context); 2078 } 2079 2080 /** 2081 * Get the first orgunit matching the given UAI code 2082 * @param uaiCode the UAI code 2083 * @return the orgunit or null if not found 2084 */ 2085 public OrgUnit getOrgUnitByUAICode(String uaiCode) 2086 { 2087 Expression expr = new AndExpression( 2088 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 2089 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 2090 ); 2091 2092 String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr); 2093 AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery); 2094 2095 return orgUnits.stream() 2096 .findFirst() 2097 .orElse(null); 2098 } 2099}