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