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