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