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