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