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