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