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