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 * Determines if a educational path is valid ODF path 1294 * @param path the educational path 1295 * @return <code>true</code> if path represents a valid ODF path 1296 */ 1297 public boolean isValid(EducationalPath path) 1298 { 1299 // Get leaf program item of this educational path 1300 List<String> programItemIds = path.getProgramItemIds(); 1301 String leafProgramItemId = programItemIds.get(programItemIds.size() - 1); 1302 if (_resolver.hasAmetysObjectForId(leafProgramItemId)) 1303 { 1304 ProgramItem leafProgramItem = _resolver.resolveById(leafProgramItemId); 1305 return isValid(path, leafProgramItem, false); 1306 } 1307 return false; 1308 } 1309 1310 1311 /** 1312 * Determines if the given educational path is a valid path for the program item 1313 * @param path the educational path. The path must include the program item itself 1314 * @param programItem the program item 1315 * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program 1316 * @return <code>true</code> if path is valid for the given program item 1317 */ 1318 public boolean isValid(EducationalPath path, ProgramItem programItem, boolean ignoreOrphanPath) 1319 { 1320 return getEducationalPaths(programItem, true, ignoreOrphanPath).contains(path); 1321 } 1322 1323 /** 1324 * Get the full path to program item for highest ancestors. The path includes this final item. 1325 * @param programItem the program item 1326 * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item. 1327 */ 1328 protected List<List<ProgramItem>> getPathOfAncestors(ProgramItem programItem) 1329 { 1330 Cache<ProgramItem, List<List<ProgramItem>>> cache = _cacheManager.get(__ANCESTORS_CACHE); 1331 1332 return cache.get(programItem, item -> { 1333 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 1334 1335 // There is no more parents, the only path is the item itself 1336 if (parentProgramItems.isEmpty()) 1337 { 1338 return List.of(List.of(item)); 1339 } 1340 1341 List<List<ProgramItem>> ancestors = new ArrayList<>(); 1342 1343 // Compute the path for each parent 1344 for (ProgramItem parentProgramItem : parentProgramItems) 1345 { 1346 for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem)) 1347 { 1348 List<ProgramItem> ancestorPathsCopy = new ArrayList<>(ancestorPaths); 1349 ancestorPathsCopy.add(item); 1350 // Add an immutable list to avoid unvoluntary modifications in cache 1351 ancestors.add(Collections.unmodifiableList(ancestorPathsCopy)); 1352 } 1353 } 1354 1355 // Add an immutable list to avoid unvoluntary modifications in cache 1356 return Collections.unmodifiableList(ancestors); 1357 }); 1358 } 1359 1360 /** 1361 * Get the enumeration of educational paths for a program item for highest ancestors. The paths does not includes this final item. 1362 * @param programItemId the id of program item 1363 * @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) 1364 */ 1365 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 1366 public List<Map<String, String>> getEducationalPathsEnumeration(String programItemId) 1367 { 1368 List<Map<String, String>> paths = new ArrayList<>(); 1369 1370 ProgramItem programItem = _resolver.resolveById(programItemId); 1371 1372 getEducationalPaths(programItem) 1373 .stream() 1374 .forEach(p -> paths.add(Map.of("id", p.toString(), "title", getEducationalPathAsString(p, c -> ((Content) c).getTitle(), " > ")))); 1375 1376 return paths; 1377 } 1378 1379 1380 /** 1381 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 1382 * The path is construct with the contents' names and the used separator is '/'. 1383 * @param programItemId The id of the program item 1384 * @param programId The id of program. Can not be null. 1385 * @return the path into the parent program or null if the item is not part of this program. 1386 */ 1387 @Callable 1388 public String getPathInProgram (String programItemId, String programId) 1389 { 1390 ProgramItem item = _resolver.resolveById(programItemId); 1391 Program program = _resolver.resolveById(programId); 1392 1393 return getPathInProgram(item, program); 1394 } 1395 1396 /** 1397 * Get the path of a ODF content into a {@link Program}.<br> 1398 * The path is construct with the contents' names and the used separator is '/'. 1399 * @param item The program item 1400 * @param parentProgram The parent root (sub)program. Can not be null. 1401 * @return the path from the parent program 1402 */ 1403 public String getPathInProgram (ProgramItem item, Program parentProgram) 1404 { 1405 if (item instanceof Program) 1406 { 1407 // The program item is already the program it self or another program 1408 return item.equals(parentProgram) ? "" : null; 1409 } 1410 1411 List<EducationalPath> paths = getEducationalPaths(item, true, true); 1412 1413 for (EducationalPath path : paths) 1414 { 1415 if (path.getProgramItemIds().contains(parentProgram.getId())) 1416 { 1417 // Find a path that match the given parent program 1418 Stream<ProgramItem> resolvedPath = path.resolveProgramItems(_resolver); 1419 return resolvedPath.map(ProgramItem::getName).collect(Collectors.joining("/")); 1420 } 1421 } 1422 1423 return null; 1424 } 1425 1426 /** 1427 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 1428 * The path is construct with the contents' names and the used separator is '/'. 1429 * @param contentId The id of the content 1430 * @param parentCourseId The id of parent course. Can not be null. 1431 * @return the path into the parent course or null if the item is not part of this course. 1432 */ 1433 @Callable 1434 public String getPathInCourse (String contentId, String parentCourseId) 1435 { 1436 Content content = _resolver.resolveById(contentId); 1437 Course parentCourse = _resolver.resolveById(parentCourseId); 1438 1439 return getPathInCourse(content, parentCourse); 1440 } 1441 1442 /** 1443 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 1444 * The path is construct with the contents' names and the used separator is '/'. 1445 * @param courseOrList The course or the course list 1446 * @param parentCourse The parent course. Can not be null. 1447 * @return the path into the parent course or null if the item is not part of this course. 1448 */ 1449 public String getPathInCourse(Content courseOrList, Course parentCourse) 1450 { 1451 if (courseOrList.equals(parentCourse)) 1452 { 1453 return ""; 1454 } 1455 1456 String path = _getPathInCourse(courseOrList, parentCourse); 1457 1458 return path; 1459 } 1460 1461 private String _getPathInCourse(Content content, Content parentContent) 1462 { 1463 if (content.equals(parentContent)) 1464 { 1465 return content.getName(); 1466 } 1467 1468 List<? extends Content> parents; 1469 1470 if (content instanceof Course course) 1471 { 1472 parents = course.getParentCourseLists(); 1473 } 1474 else if (content instanceof CourseList courseList) 1475 { 1476 parents = courseList.getParentCourses(); 1477 } 1478 else 1479 { 1480 throw new IllegalStateException(); 1481 } 1482 1483 for (Content parent : parents) 1484 { 1485 String path = _getPathInCourse(parent, parentContent); 1486 if (path != null) 1487 { 1488 return path + '/' + content.getName(); 1489 } 1490 } 1491 return null; 1492 } 1493 1494 /** 1495 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 1496 * The path is construct with the contents' names and the used separator is '/'. 1497 * @param orgUnitId The id of the orgunit 1498 * @param rootOrgUnitId The root orgunit id 1499 * @return the path into the parent program or null if the item is not part of this program. 1500 */ 1501 @Callable 1502 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 1503 { 1504 OrgUnit rootOU = null; 1505 if (StringUtils.isNotBlank(rootOrgUnitId)) 1506 { 1507 rootOU = _resolver.resolveById(rootOrgUnitId); 1508 } 1509 else 1510 { 1511 rootOU = _ouRootProvider.getRoot(); 1512 } 1513 1514 if (orgUnitId.equals(rootOU.getId())) 1515 { 1516 // The orgunit is already the root orgunit 1517 return rootOU.getName(); 1518 } 1519 1520 OrgUnit ou = _resolver.resolveById(orgUnitId); 1521 1522 List<String> paths = new ArrayList<>(); 1523 paths.add(ou.getName()); 1524 1525 OrgUnit parent = ou.getParentOrgUnit(); 1526 while (parent != null && !parent.getId().equals(rootOU.getId())) 1527 { 1528 paths.add(parent.getName()); 1529 parent = parent.getParentOrgUnit(); 1530 } 1531 1532 if (parent != null) 1533 { 1534 paths.add(rootOU.getName()); 1535 Collections.reverse(paths); 1536 return StringUtils.join(paths, "/"); 1537 } 1538 1539 return null; 1540 } 1541 1542 /** 1543 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 1544 * The path is construct with the contents' names and the used separator is '/'. 1545 * @param orgUnitId The id of the orgunit 1546 * @return the path into the parent program or null if the item is not part of this program. 1547 */ 1548 @Callable 1549 public String getOrgUnitPath(String orgUnitId) 1550 { 1551 return getOrgUnitPath(orgUnitId, null); 1552 } 1553 1554 /** 1555 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 1556 * @param part The program part 1557 * @param parentId The ancestor id 1558 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 1559 */ 1560 public boolean hasAncestor (ProgramPart part, String parentId) 1561 { 1562 List<ProgramPart> parents = part.getProgramPartParents(); 1563 1564 for (ProgramPart parent : parents) 1565 { 1566 if (parent.getId().equals(parentId)) 1567 { 1568 return true; 1569 } 1570 else if (hasAncestor(parent, parentId)) 1571 { 1572 return true; 1573 } 1574 } 1575 1576 return false; 1577 } 1578 1579 /** 1580 * Determines if a program item is shared 1581 * @param programItem the program item 1582 * @return true if the program item is shared 1583 */ 1584 public boolean isShared(ProgramItem programItem) 1585 { 1586 List<ProgramItem> parents = getParentProgramItems(programItem); 1587 if (parents.size() > 1) 1588 { 1589 return true; 1590 } 1591 else 1592 { 1593 return parents.isEmpty() ? false : isShared(parents.get(0)); 1594 } 1595 } 1596 1597 /** 1598 * Check if a relation can be establish between two ODF contents 1599 * @param srcContent The source content (copied or moved) 1600 * @param targetContent The target content 1601 * @param errors The list of error messages 1602 * @param contextualParameters the contextual parameters 1603 * @return true if the relation is valid, false otherwise 1604 */ 1605 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 1606 { 1607 boolean isCompatible = true; 1608 1609 if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit) 1610 { 1611 if (!_isContentTypeCompatible(srcContent, targetContent)) 1612 { 1613 // Invalid relations between content types 1614 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 1615 isCompatible = false; 1616 } 1617 else if (!_isCatalogCompatible(srcContent, targetContent)) 1618 { 1619 // Catalog is invalid 1620 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 1621 isCompatible = false; 1622 } 1623 else if (!_isLanguageCompatible(srcContent, targetContent)) 1624 { 1625 // Language is invalid 1626 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 1627 isCompatible = false; 1628 } 1629 else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters)) 1630 { 1631 // Shareable fields don't match 1632 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 1633 isCompatible = false; 1634 } 1635 } 1636 else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit) 1637 { 1638 // If the target isn't ODF related but the source is, the relation is not compatible. 1639 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 1640 isCompatible = false; 1641 } 1642 1643 return isCompatible; 1644 } 1645 1646 /** 1647 * Get the name of attribute holding the relation between a parent content and its children 1648 * @param parentProgramItem the parent content 1649 * @param childProgramItem the child content 1650 * @return the name of attribute the child relation 1651 */ 1652 public String getDescendantRelationAttributeName(ProgramItem parentProgramItem, ProgramItem childProgramItem) 1653 { 1654 if (parentProgramItem instanceof CourseList && childProgramItem instanceof Course) 1655 { 1656 return CourseList.CHILD_COURSES; 1657 } 1658 else if (parentProgramItem instanceof Course && childProgramItem instanceof CourseList) 1659 { 1660 return Course.CHILD_COURSE_LISTS; 1661 } 1662 else if (parentProgramItem instanceof Course && childProgramItem instanceof CoursePart) 1663 { 1664 return Course.CHILD_COURSE_PARTS; 1665 } 1666 else if (parentProgramItem instanceof TraversableProgramPart && childProgramItem instanceof ProgramPart) 1667 { 1668 return TraversableProgramPart.CHILD_PROGRAM_PARTS; 1669 } 1670 1671 return null; 1672 } 1673 1674 private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList) 1675 { 1676 return courseList.getCourses().contains(course); 1677 } 1678 1679 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 1680 { 1681 if (srcContent instanceof Container || srcContent instanceof SubProgram) 1682 { 1683 return targetContent instanceof AbstractTraversableProgramPart; 1684 } 1685 else if (srcContent instanceof CourseList) 1686 { 1687 return targetContent instanceof CourseListContainer; 1688 } 1689 else if (srcContent instanceof Course) 1690 { 1691 return targetContent instanceof CourseList; 1692 } 1693 else if (srcContent instanceof OrgUnit) 1694 { 1695 return targetContent instanceof OrgUnit; 1696 } 1697 1698 return false; 1699 } 1700 1701 private boolean _isCatalogCompatible(Content srcContent, Content targetContent) 1702 { 1703 if (srcContent instanceof ProgramItem srcProgramItem && targetContent instanceof ProgramItem targetProgramItem) 1704 { 1705 return srcProgramItem.getCatalog().equals(targetProgramItem.getCatalog()); 1706 } 1707 return true; 1708 } 1709 1710 private boolean _isLanguageCompatible(Content srcContent, Content targetContent) 1711 { 1712 return srcContent.getLanguage().equals(targetContent.getLanguage()); 1713 } 1714 1715 private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters) 1716 { 1717 // We check shareable fields only if the course content is not created (or created by copy) and not moved 1718 if (srcContent instanceof Course srcCourse 1719 && targetContent instanceof CourseList targetCourseList 1720 && _shareableCourseHelper.handleShareableCourse() 1721 && !"create".equals(contextualParameters.get("mode")) 1722 && !"copy".equals(contextualParameters.get("mode")) 1723 && !"move".equals(contextualParameters.get("mode")) 1724 // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields 1725 && !_isCourseAlreadyBelongToCourseList(srcCourse, targetCourseList)) 1726 { 1727 return _shareableCourseHelper.isShareableFieldsMatch(srcCourse, targetCourseList); 1728 } 1729 1730 return true; 1731 } 1732 1733 private List<String> _getContentParameters(Content srcContent, Content targetContent) 1734 { 1735 List<String> parameters = new ArrayList<>(); 1736 parameters.add(srcContent.getTitle()); 1737 parameters.add(srcContent.getId()); 1738 parameters.add(targetContent.getTitle()); 1739 parameters.add(targetContent.getId()); 1740 return parameters; 1741 } 1742 /** 1743 * Copy a {@link ProgramItem} 1744 * @param srcContent The program item to copy 1745 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1746 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1747 * @param copiedContents the initial contents with their copied content 1748 * @return The created content 1749 * @param <C> The modifiable content return type 1750 * @throws AmetysRepositoryException If an error occurred during copy 1751 * @throws WorkflowException If an error occurred during copy 1752 */ 1753 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1754 { 1755 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents); 1756 } 1757 1758 /** 1759 * Copy a {@link ProgramItem} 1760 * @param srcContent The program item to copy 1761 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1762 * @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. 1763 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1764 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1765 * @param copiedContents the initial contents with their copied content 1766 * @param <C> The modifiable content return type 1767 * @return The created content 1768 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1769 * @throws AmetysRepositoryException If an error occurred 1770 * @throws WorkflowException If an error occurred 1771 */ 1772 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1773 { 1774 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents); 1775 } 1776 1777 /** 1778 * Copy a {@link CoursePart} 1779 * @param srcContent The course part to copy 1780 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1781 * @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. 1782 * @param initWorkflowActionId The initial workflow action id 1783 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1784 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1785 * @param copiedContents the initial contents with their copied content 1786 * @param <C> The modifiable content return type 1787 * @return The created content 1788 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1789 * @throws AmetysRepositoryException If an error occurred 1790 * @throws WorkflowException If an error occurred 1791 */ 1792 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 1793 { 1794 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents); 1795 } 1796 1797 /** 1798 * Copy a {@link ProgramItem} 1799 * @param srcContent The program item to copy 1800 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1801 * @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. 1802 * @param initWorkflowActionId The initial workflow action id 1803 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1804 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1805 * @param copiedContents the initial contents with their copied content 1806 * @param <C> The modifiable content return type 1807 * @return The created content 1808 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1809 * @throws AmetysRepositoryException If an error occurred 1810 * @throws WorkflowException If an error occurred 1811 */ 1812 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 1813 { 1814 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents); 1815 } 1816 1817 /** 1818 * Copy a {@link ProgramItem}. Also copy the synchronization metadata (status and alternative value) 1819 * @param srcContent The program item to copy 1820 * @param catalog The catalog 1821 * @param code The odf content code 1822 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1823 * @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. 1824 * @param initWorkflowActionId The initial workflow action id 1825 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1826 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1827 * @param copiedContents the initial contents with their copied content 1828 * @param <C> The modifiable content return type 1829 * @return The created content 1830 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1831 * @throws AmetysRepositoryException If an error occurred 1832 * @throws WorkflowException If an error occurred 1833 */ 1834 @SuppressWarnings("unchecked") 1835 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 1836 { 1837 String computedTargetLanguage = targetContentLanguage; 1838 if (computedTargetLanguage == null) 1839 { 1840 computedTargetLanguage = srcContent.getLanguage(); 1841 } 1842 1843 String computeTargetName = targetContentName; 1844 if (computeTargetName == null) 1845 { 1846 // Compute content name from source content and requested language 1847 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 1848 } 1849 1850 String computeTargetCatalog = targetCatalog; 1851 if (computeTargetCatalog == null) 1852 { 1853 computeTargetCatalog = catalog; 1854 } 1855 1856 String principalContentType = srcContent.getTypes()[0]; 1857 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 1858 if (createdContent != null) 1859 { 1860 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 1861 } 1862 else 1863 { 1864 // Copy content without notifying observers (done later) and copying ACL 1865 DataContext context = RepositoryDataContext.newInstance() 1866 .withExternalMetadataInCopy(true); 1867 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true, false, true, context); 1868 1869 if (fullCopy) 1870 { 1871 _cleanContentMetadata(createdContent); 1872 1873 if (targetCatalog != null) 1874 { 1875 if (createdContent instanceof ProgramItem programItem) 1876 { 1877 programItem.setCatalog(targetCatalog); 1878 } 1879 else if (createdContent instanceof CoursePart coursePart) 1880 { 1881 coursePart.setCatalog(targetCatalog); 1882 } 1883 1884 } 1885 1886 if (srcContent instanceof ProgramItem programItem) 1887 { 1888 copyProgramItemStructure(programItem, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedContents); 1889 } 1890 1891 _extractOutgoingReferences(createdContent); 1892 1893 createdContent.saveChanges(); 1894 } 1895 1896 // Notify observers after all structure has been copied 1897 _contentDAO.notifyContentCopied(createdContent, false); 1898 1899 copiedContents.put(srcContent, createdContent); 1900 } 1901 1902 return (C) createdContent; 1903 } 1904 1905 /** 1906 * Copy the structure of a {@link ProgramItem} 1907 * @param srcContent the content to copy 1908 * @param targetContent the target content 1909 * @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. 1910 * @param initWorkflowActionId The initial workflow action id 1911 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1912 * @param copiedContents the initial contents with their copied content 1913 * @throws AmetysRepositoryException If an error occurred during copy 1914 * @throws WorkflowException If an error occurred during copy 1915 */ 1916 protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException 1917 { 1918 List<ProgramItem> srcChildContents = new ArrayList<>(); 1919 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1920 1921 String childMetadataPath = null; 1922 String parentMetadataPath = null; 1923 1924 if (srcContent instanceof TraversableProgramPart programPart) 1925 { 1926 childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS; 1927 parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS; 1928 srcChildContents.addAll(programPart.getProgramPartChildren()); 1929 } 1930 else if (srcContent instanceof CourseList courseList) 1931 { 1932 childMetadataPath = CourseList.CHILD_COURSES; 1933 parentMetadataPath = Course.PARENT_COURSE_LISTS; 1934 srcChildContents.addAll(courseList.getCourses()); 1935 } 1936 else if (srcContent instanceof Course course) 1937 { 1938 childMetadataPath = Course.CHILD_COURSE_LISTS; 1939 parentMetadataPath = CourseList.PARENT_COURSES; 1940 srcChildContents.addAll(course.getCourseLists()); 1941 1942 List<String> refCoursePartIds = new ArrayList<>(); 1943 for (CoursePart srcChildContent : course.getCourseParts()) 1944 { 1945 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents); 1946 refCoursePartIds.add(targetChildContent.getId()); 1947 } 1948 _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds); 1949 } 1950 1951 List<String> refChildIds = new ArrayList<>(); 1952 for (ProgramItem srcChildContent : srcChildContents) 1953 { 1954 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents); 1955 refChildIds.add(targetChildContent.getId()); 1956 } 1957 1958 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1959 1960 _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values); 1961 } 1962 1963 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1964 { 1965 if (!refChildIds.isEmpty()) 1966 { 1967 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1968 } 1969 } 1970 1971 private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1972 { 1973 if (!values.isEmpty()) 1974 { 1975 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1976 { 1977 String childMetadataName = entry.getKey().getLeft(); 1978 String parentMetadataName = entry.getKey().getRight(); 1979 List<String> childContents = entry.getValue(); 1980 1981 parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()])); 1982 1983 for (String childContentId : childContents) 1984 { 1985 ModifiableContent content = _resolver.resolveById(childContentId); 1986 String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName); 1987 content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId())); 1988 content.saveChanges(); 1989 } 1990 } 1991 } 1992 } 1993 1994 /** 1995 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1996 * @param createdContent The created content to clean 1997 */ 1998 protected void _cleanContentMetadata(ModifiableContent createdContent) 1999 { 2000 if (createdContent instanceof ProgramPart) 2001 { 2002 _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS); 2003 } 2004 2005 if (createdContent instanceof TraversableProgramPart) 2006 { 2007 _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS); 2008 } 2009 2010 if (createdContent instanceof CourseList) 2011 { 2012 _removeFullValue(createdContent, CourseList.CHILD_COURSES); 2013 _removeFullValue(createdContent, CourseList.PARENT_COURSES); 2014 } 2015 2016 if (createdContent instanceof Course) 2017 { 2018 _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS); 2019 _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS); 2020 _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS); 2021 } 2022 2023 if (createdContent instanceof CoursePart) 2024 { 2025 _removeFullValue(createdContent, CoursePart.PARENT_COURSES); 2026 } 2027 } 2028 2029 private void _removeFullValue(ModifiableContent content, String attributeName) 2030 { 2031 content.removeValue(attributeName); 2032 content.removeExternalizableMetadataIfExists(attributeName); 2033 } 2034 2035 private void _extractOutgoingReferences(ModifiableContent content) 2036 { 2037 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content); 2038 content.setOutgoingReferences(outgoingReferencesByPath); 2039 } 2040 2041 /** 2042 * Switch the ametys object to Live version if it has one 2043 * @param ao the Ametys object 2044 * @throws NoLiveVersionException if the content has no live version 2045 */ 2046 public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException 2047 { 2048 // Switch to the Live label if exists 2049 String[] allLabels = ao.getAllLabels(); 2050 String[] currentLabels = ao.getLabels(); 2051 2052 boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL); 2053 boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL); 2054 2055 if (hasLiveVersion && !currentVersionIsLive) 2056 { 2057 ao.switchToLabel(CmsConstants.LIVE_LABEL); 2058 } 2059 else if (!hasLiveVersion) 2060 { 2061 throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version"); 2062 } 2063 } 2064 2065 /** 2066 * Switch to Live version if is required 2067 * @param ao the Ametys object 2068 * @throws NoLiveVersionException if the Live version is required but not exist 2069 */ 2070 public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException 2071 { 2072 Request request = _getRequest(); 2073 if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null) 2074 { 2075 switchToLiveVersion(ao); 2076 } 2077 } 2078 2079 /** 2080 * Count the hours accumulation in the {@link ProgramItem} 2081 * @param programItem The program item on which we compute the total number of hours 2082 * @return The hours accumulation 2083 */ 2084 public Double getCumulatedHours(ProgramItem programItem) 2085 { 2086 // Ignore optional course list and avoid useless expensive calls 2087 if (programItem instanceof CourseList courseList && ChoiceType.OPTIONAL.equals(courseList.getType())) 2088 { 2089 return 0.0; 2090 } 2091 2092 List<ProgramItem> children = getChildProgramItems(programItem); 2093 2094 Double coef = 1.0; 2095 Double countNbHours = 0.0; 2096 2097 // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total) 2098 if (programItem instanceof CourseList courseList) 2099 { 2100 // If there is no children, compute the coef is useless 2101 // Also choice list can throw an exception while dividing by zero 2102 if (children.isEmpty()) 2103 { 2104 return 0.0; 2105 } 2106 2107 switch (courseList.getType()) 2108 { 2109 case CHOICE: 2110 // Apply the average of number of EC from children multiply by the minimum ELP to select 2111 coef = ((double) courseList.getMinNumberOfCourses()) / children.size(); 2112 break; 2113 case MANDATORY: 2114 default: 2115 // Add all ECTS from children 2116 break; 2117 } 2118 } 2119 2120 // If it's a course and we have a value for the number of hours 2121 // Then get the value 2122 if (programItem instanceof Course course && course.hasValue(Course.NUMBER_OF_HOURS)) 2123 { 2124 countNbHours += course.<Double>getValue(Course.NUMBER_OF_HOURS); 2125 } 2126 // Else if there are program item children on the item 2127 // Then compute on children 2128 else if (children.size() > 0) 2129 { 2130 for (ProgramItem child : children) 2131 { 2132 countNbHours += getCumulatedHours(child); 2133 } 2134 } 2135 // Else, it's a course but there is no value for the number of hours and we don't have program item children 2136 // Then compute on course parts 2137 else if (programItem instanceof Course course) 2138 { 2139 countNbHours += course.getCourseParts() 2140 .stream() 2141 .mapToDouble(CoursePart::getNumberOfHours) 2142 .sum(); 2143 } 2144 2145 return coef * countNbHours; 2146 } 2147 2148 /** 2149 * Get the request 2150 * @return the request 2151 */ 2152 protected Request _getRequest() 2153 { 2154 return ContextHelper.getRequest(_context); 2155 } 2156 2157 /** 2158 * Get the first orgunit matching the given UAI code 2159 * @param uaiCode the UAI code 2160 * @return the orgunit or null if not found 2161 */ 2162 public OrgUnit getOrgUnitByUAICode(String uaiCode) 2163 { 2164 Expression expr = new AndExpression( 2165 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 2166 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 2167 ); 2168 2169 String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr); 2170 AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery); 2171 2172 return orgUnits.stream() 2173 .findFirst() 2174 .orElse(null); 2175 } 2176}