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.Set; 027import java.util.function.Function; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.commons.lang.StringUtils; 040import org.apache.commons.lang3.ArrayUtils; 041import org.apache.commons.lang3.tuple.Pair; 042 043import org.ametys.cms.CmsConstants; 044import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 045import org.ametys.cms.data.ContentDataHelper; 046import org.ametys.cms.repository.Content; 047import org.ametys.cms.repository.ContentQueryHelper; 048import org.ametys.cms.repository.ContentTypeExpression; 049import org.ametys.cms.repository.DefaultContent; 050import org.ametys.cms.repository.LanguageExpression; 051import org.ametys.cms.repository.ModifiableContent; 052import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 053import org.ametys.cms.workflow.ContentWorkflowHelper; 054import org.ametys.core.observation.ObservationManager; 055import org.ametys.core.ui.Callable; 056import org.ametys.core.user.CurrentUserProvider; 057import org.ametys.odf.course.Course; 058import org.ametys.odf.course.CourseContainer; 059import org.ametys.odf.course.ShareableCourseHelper; 060import org.ametys.odf.courselist.CourseList; 061import org.ametys.odf.courselist.CourseList.ChoiceType; 062import org.ametys.odf.courselist.CourseListContainer; 063import org.ametys.odf.coursepart.CoursePart; 064import org.ametys.odf.coursepart.CoursePartFactory; 065import org.ametys.odf.orgunit.OrgUnit; 066import org.ametys.odf.orgunit.OrgUnitFactory; 067import org.ametys.odf.orgunit.RootOrgUnitProvider; 068import org.ametys.odf.program.AbstractProgram; 069import org.ametys.odf.program.AbstractTraversableProgramPart; 070import org.ametys.odf.program.Container; 071import org.ametys.odf.program.Program; 072import org.ametys.odf.program.ProgramFactory; 073import org.ametys.odf.program.ProgramPart; 074import org.ametys.odf.program.SubProgram; 075import org.ametys.odf.program.TraversableProgramPart; 076import org.ametys.plugins.repository.AmetysObject; 077import org.ametys.plugins.repository.AmetysObjectExistsException; 078import org.ametys.plugins.repository.AmetysObjectIterable; 079import org.ametys.plugins.repository.AmetysObjectIterator; 080import org.ametys.plugins.repository.AmetysObjectResolver; 081import org.ametys.plugins.repository.AmetysRepositoryException; 082import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 083import org.ametys.plugins.repository.RepositoryConstants; 084import org.ametys.plugins.repository.UnknownAmetysObjectException; 085import org.ametys.plugins.repository.collection.AmetysObjectCollection; 086import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 087import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 088import org.ametys.plugins.repository.query.QueryHelper; 089import org.ametys.plugins.repository.query.SortCriteria; 090import org.ametys.plugins.repository.query.expression.AndExpression; 091import org.ametys.plugins.repository.query.expression.Expression; 092import org.ametys.plugins.repository.query.expression.Expression.Operator; 093import org.ametys.plugins.repository.query.expression.OrExpression; 094import org.ametys.plugins.repository.query.expression.StringExpression; 095import org.ametys.runtime.i18n.I18nizableText; 096import org.ametys.runtime.plugin.component.AbstractLogEnabled; 097import org.ametys.runtime.plugin.component.PluginAware; 098 099import com.opensymphony.workflow.WorkflowException; 100 101/** 102 * Helper for ODF contents 103 * 104 */ 105public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware, Contextualizable 106{ 107 /** The component role. */ 108 public static final String ROLE = ODFHelper.class.getName(); 109 110 /** Request attribute to get the "Live" version of contents */ 111 public static final String REQUEST_ATTRIBUTE_VALID_LABEL = "live-version"; 112 113 /** The default id of initial workflow action */ 114 protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0; 115 116 /** Ametys object resolver */ 117 protected AmetysObjectResolver _resolver; 118 /** The content workflow helper */ 119 protected ContentWorkflowHelper _contentWorkflowHelper; 120 /** The content types manager */ 121 protected ContentTypeExtensionPoint _cTypeEP; 122 /** The observation manager */ 123 protected ObservationManager _observationManager; 124 /** The current user provider */ 125 protected CurrentUserProvider _currentUserProvider; 126 /** Root orgunit */ 127 protected RootOrgUnitProvider _ouRootProvider; 128 /** Provider for externalizable metadata */ 129 protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 130 /** Helper for shareable course */ 131 protected ShareableCourseHelper _shareableCourseHelper; 132 /** The Avalon context */ 133 protected Context _context; 134 135 private String _pluginName; 136 137 public void contextualize(Context context) throws ContextException 138 { 139 _context = context; 140 } 141 142 @Override 143 public void service(ServiceManager manager) throws ServiceException 144 { 145 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 146 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 147 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 148 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 149 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 150 _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 151 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 152 _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE); 153 } 154 155 @Override 156 public void setPluginInfo(String pluginName, String featureName, String id) 157 { 158 _pluginName = pluginName; 159 } 160 161 /** 162 * Gets the root for ODF contents 163 * @return the root for ODF contents 164 */ 165 public AmetysObjectCollection getRootContent() 166 { 167 return getRootContent(false); 168 } 169 170 /** 171 * Gets the root for ODF contents 172 * @param create <code>true</code> to create automatically the root when missing. 173 * @return the root for ODF contents 174 */ 175 public AmetysObjectCollection getRootContent(boolean create) 176 { 177 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/"); 178 179 boolean needSave = false; 180 if (!pluginsNode.hasChild(_pluginName)) 181 { 182 if (create) 183 { 184 pluginsNode.createChild(_pluginName, "ametys:unstructured"); 185 needSave = true; 186 } 187 else 188 { 189 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing"); 190 } 191 } 192 193 ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName); 194 if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents")) 195 { 196 if (create) 197 { 198 pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection"); 199 needSave = true; 200 } 201 else 202 { 203 throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing"); 204 } 205 } 206 207 if (needSave) 208 { 209 pluginsNode.saveChanges(); 210 } 211 212 return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 213 } 214 215 /** 216 * Get the {@link ProgramItem}s matching the given arguments 217 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 218 * @param code The code. Can be null to get program's items regardless of their code 219 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 220 * @param lang The search language. Can be null to get program's items regardless of their language 221 * @param <C> The content return type 222 * @return The matching program items 223 */ 224 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang) 225 { 226 return getProgramItems(cTypeId, code, catalogName, lang, null, null); 227 } 228 229 /** 230 * Get the {@link ProgramItem}s matching the given arguments 231 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 232 * @param code The code. Can be null to get program's items regardless of their code 233 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 234 * @param lang The search language. Can be null to get program's items regardless of their language 235 * @param <C> The content return type 236 * @return The matching program items 237 */ 238 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang) 239 { 240 return getProgramItems(cTypeIds, code, catalogName, lang, null, null); 241 } 242 243 /** 244 * Get the {@link ProgramItem}s matching the given arguments 245 * @param cTypeId The id of content type. Can be null to get program's items whatever their content type. 246 * @param code The code. Can be null to get program's items regardless of their code 247 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 248 * @param lang The search language. Can be null to get program's items regardless of their language 249 * @param additionnalExpr An additional expression for filtering result. Can be null 250 * @param sortCriteria criteria for sorting results 251 * @param <C> The content return type 252 * @return The matching program items 253 */ 254 public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 255 { 256 return getProgramItems(cTypeId != null ? Collections.singletonList(cTypeId) : Collections.EMPTY_LIST, code, catalogName, lang, additionnalExpr, sortCriteria); 257 } 258 259 /** 260 * Get the {@link ProgramItem}s matching the given arguments 261 * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type. 262 * @param code The code. Can be null to get program's items regardless of their code 263 * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to. 264 * @param lang The search language. Can be null to get program's items regardless of their language 265 * @param additionnalExpr An additional expression for filtering result. Can be null 266 * @param sortCriteria criteria for sorting results 267 * @param <C> The content return type 268 * @return The matching program items 269 */ 270 public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria) 271 { 272 List<Expression> exprs = new ArrayList<>(); 273 274 if (!cTypeIds.isEmpty()) 275 { 276 exprs.add(new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()]))); 277 } 278 if (StringUtils.isNotEmpty(code)) 279 { 280 exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code)); 281 } 282 if (StringUtils.isNotEmpty(catalogName)) 283 { 284 exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName)); 285 } 286 if (StringUtils.isNotEmpty(lang)) 287 { 288 exprs.add(new LanguageExpression(Operator.EQ, lang)); 289 } 290 if (additionnalExpr != null) 291 { 292 exprs.add(additionnalExpr); 293 } 294 295 Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()])); 296 297 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria); 298 return _resolver.query(xpathQuery); 299 } 300 301 /** 302 * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language 303 * @param srcCoursePart The source course part 304 * @param catalogName The name of catalog to search into 305 * @param lang The search language 306 * @return The equivalent program item or <code>null</code> if not exists 307 */ 308 public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang) 309 { 310 return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang); 311 } 312 313 /** 314 * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 315 * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem} 316 * @param srcProgramItem The source program item 317 * @param catalogName The name of catalog to search into 318 * @param lang The search language 319 * @return The equivalent program item or <code>null</code> if not exists 320 */ 321 public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang) 322 { 323 return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang); 324 } 325 326 /** 327 * Get the equivalent {@link Content} having the same code in given catalog and language 328 * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject} 329 * @param contentType The content type to search for 330 * @param odfContentCode The code of the ODF content 331 * @param catalogName The name of catalog to search into 332 * @param lang The search language 333 * @return The equivalent content or <code>null</code> if not exists 334 */ 335 public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang) 336 { 337 Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType); 338 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 339 Expression catalogExpr = new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName); 340 Expression codeExpr = new StringExpression(ProgramItem.CODE, Operator.EQ, odfContentCode); 341 342 Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr); 343 344 String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr); 345 AmetysObjectIterable<T> contents = _resolver.query(xpathQuery); 346 AmetysObjectIterator<T> contentsIt = contents.iterator(); 347 if (contentsIt.hasNext()) 348 { 349 return contentsIt.next(); 350 } 351 352 return null; 353 } 354 355 /** 356 * Get the child program items of a {@link ProgramItem} 357 * @param programItem The program item 358 * @return The child program items 359 */ 360 public List<ProgramItem> getChildProgramItems(ProgramItem programItem) 361 { 362 List<ProgramItem> children = new ArrayList<>(); 363 364 if (programItem instanceof TraversableProgramPart) 365 { 366 children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren()); 367 } 368 369 if (programItem instanceof CourseContainer) 370 { 371 children.addAll(((CourseContainer) programItem).getCourses()); 372 } 373 374 if (programItem instanceof Course) 375 { 376 children.addAll(((Course) programItem).getCourseLists()); 377 } 378 379 return children; 380 } 381 382 /** 383 * Get the child subprograms of a {@link ProgramPart} 384 * @param programPart The program part 385 * @return The child subprograms 386 */ 387 public Set<SubProgram> getChildSubPrograms(ProgramPart programPart) 388 { 389 Set<SubProgram> subPrograms = new HashSet<>(); 390 391 if (programPart instanceof TraversableProgramPart) 392 { 393 if (programPart instanceof SubProgram) 394 { 395 subPrograms.add((SubProgram) programPart); 396 } 397 ((TraversableProgramPart) programPart).getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child))); 398 } 399 400 return subPrograms; 401 } 402 403 /** 404 * Gets (recursively) parent containers of this program item. 405 * @param programItem The program item 406 * @return parent containers of this program item. 407 */ 408 public Set<Container> getParentContainers(ProgramItem programItem) 409 { 410 return _getParentsOfType(programItem, Container.class); 411 } 412 413 /** 414 * Gets (recursively) parent programs of this program item. 415 * @param programItem The program item 416 * @return parent programs of this program item. 417 */ 418 public Set<Program> getParentPrograms(ProgramItem programItem) 419 { 420 return _getParentsOfType(programItem, Program.class); 421 } 422 423 /** 424 * Gets (recursively) parent abstract programs of this program item. 425 * @param programItem The program item 426 * @return parent abstract programs of this program item. 427 */ 428 public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem) 429 { 430 return _getParentsOfType(programItem, AbstractProgram.class); 431 } 432 433 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest) 434 { 435 Set<ProgramItem> visitedProgramItems = new HashSet<>(); 436 visitedProgramItems.add(programItem); 437 return _getParentsOfType(programItem, visitedProgramItems, classToTest); 438 } 439 440 @SuppressWarnings("unchecked") 441 private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest) 442 { 443 Set<T> parentsOfType = new HashSet<>(); 444 List<ProgramItem> parents = getParentProgramItems(programItem); 445 446 for (ProgramItem parent : parents) 447 { 448 // Only parents not already visited 449 if (visitedProgramItems.add(parent)) 450 { 451 // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram) 452 if (classToTest.isInstance(parent)) 453 { 454 parentsOfType.add((T) parent); 455 } 456 else 457 { 458 parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest)); 459 } 460 } 461 } 462 463 return parentsOfType; 464 } 465 466 /** 467 * Get the child programs of an {@link OrgUnit} 468 * @param orgUnit the orgUnit, can be null 469 * @param catalog the catalog 470 * @param lang the lang 471 * @return The child programs 472 */ 473 public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang) 474 { 475 List<Program> programs = new ArrayList<>(); 476 List<Expression> programExpressions = new ArrayList<>(); 477 programExpressions.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 478 479 programExpressions.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 480 programExpressions.add(new LanguageExpression(Operator.EQ, lang)); 481 482 // Can be null, it means that all programs for catalog and lang are selected 483 if (orgUnit != null) 484 { 485 List<Expression> expressions = new ArrayList<>(); 486 for (String orgUnitId : getSubOrgUnitIds(orgUnit)) 487 { 488 expressions.add(new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId)); 489 } 490 491 programExpressions.add(new OrExpression(expressions.toArray(new Expression[0]))); 492 } 493 494 String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0]))); 495 AmetysObjectIterable<Program> programsIterable = _resolver.query(programQuery); 496 AmetysObjectIterator<Program> programsIterator = programsIterable.iterator(); 497 while (programsIterator.hasNext()) 498 { 499 programs.add(programsIterator.next()); 500 } 501 return programs; 502 } 503 504 /** 505 * Get the current orgunit and its suborgunits recursively identifiers. 506 * @param orgUnit The orgunit at the top 507 * @return A {@link List} of {@link OrgUnit} ids 508 */ 509 public List<String> getSubOrgUnitIds(OrgUnit orgUnit) 510 { 511 List<String> orgUnitIds = new ArrayList<>(); 512 orgUnitIds.add(orgUnit.getId()); 513 for (String id : orgUnit.getSubOrgUnits()) 514 { 515 OrgUnit childOrgUnit = _resolver.resolveById(id); 516 orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit)); 517 } 518 519 return orgUnitIds; 520 } 521 522 /** 523 * Determines if the {@link ProgramItem} has parent program items 524 * @param programItem The program item 525 * @return true if has parent program items 526 */ 527 public boolean hasParentProgramItems(ProgramItem programItem) 528 { 529 boolean hasParent = false; 530 531 if (programItem instanceof ProgramPart) 532 { 533 hasParent = !((ProgramPart) programItem).getProgramPartParents().isEmpty() || hasParent; 534 } 535 536 if (programItem instanceof CourseList) 537 { 538 hasParent = !((CourseList) programItem).getParentCourses().isEmpty() || hasParent; 539 } 540 541 if (programItem instanceof Course) 542 { 543 hasParent = !((Course) programItem).getParentCourseLists().isEmpty() || hasParent; 544 } 545 546 return hasParent; 547 } 548 549 /** 550 * Get the parent program items of a {@link ProgramItem} 551 * @param programItem The program item 552 * @return The parent program items 553 */ 554 public List<ProgramItem> getParentProgramItems(ProgramItem programItem) 555 { 556 List<ProgramItem> parents = new ArrayList<>(); 557 558 if (programItem instanceof ProgramPart) 559 { 560 parents.addAll(((ProgramPart) programItem).getProgramPartParents()); 561 } 562 563 if (programItem instanceof CourseList) 564 { 565 parents.addAll(((CourseList) programItem).getParentCourses()); 566 } 567 568 if (programItem instanceof Course) 569 { 570 parents.addAll(((Course) programItem).getParentCourseLists()); 571 } 572 573 return parents; 574 } 575 576 /** 577 * Get the nearest program item parent into the given parent {@link AbstractProgram} 578 * @param programItem The program item 579 * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned. 580 * @return The parent program item or null if not found. 581 */ 582 public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram) 583 { 584 if (programItem instanceof Program) 585 { 586 return null; 587 } 588 589 if (programItem instanceof ProgramPart) 590 { 591 List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents(); 592 593 for (ProgramPart parent : parents) 594 { 595 if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram))) 596 { 597 return parent; 598 } 599 else 600 { 601 ProgramItem ancestor = getParentProgramItem(parent, parentProgram); 602 if (ancestor != null) 603 { 604 return parent; 605 } 606 } 607 } 608 } 609 610 if (programItem instanceof CourseList) 611 { 612 for (Course parentCourse : ((CourseList) programItem).getParentCourses()) 613 { 614 ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram); 615 if (ancestor != null) 616 { 617 return parentCourse; 618 } 619 } 620 } 621 622 if (programItem instanceof Course) 623 { 624 for (CourseList cl : ((Course) programItem).getParentCourseLists()) 625 { 626 ProgramItem ancestor = getParentProgramItem(cl, parentProgram); 627 if (ancestor != null) 628 { 629 return cl; 630 } 631 } 632 } 633 634 return null; 635 } 636 637 /** 638 * Get information of the program item structure (type, if program has children) 639 * @param programItemId the program item id 640 * @return a map of information 641 */ 642 @Callable 643 public Map<String, Object> getStructureInfo(String programItemId) 644 { 645 Map<String, Object> results = new HashMap<>(); 646 647 if (StringUtils.isNotBlank(programItemId)) 648 { 649 Content content = _resolver.resolveById(programItemId); 650 if (content instanceof ProgramItem) 651 { 652 results.put("id", programItemId); 653 results.put("title", content.getTitle()); 654 results.put("code", ((ProgramItem) content).getCode()); 655 656 List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content); 657 results.put("hasChildren", !childProgramItems.isEmpty()); 658 659 List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content); 660 results.put("hasParent", !parentProgramItems.isEmpty()); 661 662 results.put("paths", getPaths((ProgramItem) content, " > ")); 663 } 664 } 665 666 return results; 667 } 668 669 /** 670 * Get information of the program item structure (type, if program has children) 671 * @param programItemIds the list of program item id 672 * @return a map of information 673 */ 674 @Callable 675 public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds) 676 { 677 Map<String, Map<String, Object>> results = new HashMap<>(); 678 679 for (String programItemId : programItemIds) 680 { 681 results.put(programItemId, getStructureInfo(programItemId)); 682 } 683 684 return results; 685 } 686 687 /** 688 * Get all the paths of a ODF content.<br> 689 * The path is built with the contents' title and code 690 * @param item The program item 691 * @param separator The path separator 692 * @return the paths in parent program items 693 */ 694 public List<String> getPaths(ProgramItem item, String separator) 695 { 696 Function<ProgramItem, String> mapper = c -> ((Content) c).getTitle() + " (" + c.getCode() + ")"; 697 return getPaths(item, separator, mapper, true); 698 } 699 700 /** 701 * Get all the paths of a ODF content.<br> 702 * The path is built with the mapper function. 703 * @param item The program item 704 * @param separator The path separator 705 * @param mapper the function to apply to each program item to build the path 706 * @param includeItseft set to false to not include final item in path 707 * @return the paths in parent program items 708 */ 709 public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, boolean includeItseft) 710 { 711 List<String> paths = new ArrayList<>(); 712 713 List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(item); 714 for (List<ProgramItem> ancestorPath : ancestorPaths) 715 { 716 if (!includeItseft) 717 { 718 ancestorPath.remove(item); 719 } 720 if (!ancestorPath.isEmpty()) 721 { 722 List<String> titles = ancestorPath.stream().map(mapper).collect(Collectors.toList()); 723 paths.add(String.join(separator, titles)); 724 } 725 } 726 727 return paths; 728 } 729 730 731 /** 732 * Get the full path to program item for highest ancestors. The path includes this final item. 733 * @param item the program item 734 * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item. 735 */ 736 public List<List<ProgramItem>> getPathOfAncestors(ProgramItem item) 737 { 738 List<List<ProgramItem>> ancestors = new ArrayList<>(); 739 740 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 741 if (parentProgramItems.isEmpty()) 742 { 743 List<ProgramItem> items = new ArrayList<>(); 744 items.add(item); 745 ancestors.add(items); 746 return ancestors; 747 } 748 749 for (ProgramItem parentProgramItem : parentProgramItems) 750 { 751 for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem)) 752 { 753 ancestorPaths.add(item); 754 ancestors.add(ancestorPaths); 755 } 756 } 757 758 return ancestors; 759 } 760 761 /** 762 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 763 * The path is construct with the contents' names and the used separator is '/'. 764 * @param programItemId The id of the program item 765 * @param programId The id of program. Can not be null. 766 * @return the path into the parent program or null if the item is not part of this program. 767 */ 768 @Callable 769 public String getPathInProgram (String programItemId, String programId) 770 { 771 ProgramItem item = _resolver.resolveById(programItemId); 772 Program program = _resolver.resolveById(programId); 773 774 return getPathInProgram(item, program); 775 } 776 777 /** 778 * Get the path of a ODF content into a {@link Program}.<br> 779 * The path is construct with the contents' names and the used separator is '/'. 780 * @param item The program item 781 * @param parentProgram The parent root (sub)program. Can not be null. 782 * @return the path from the parent program 783 */ 784 public String getPathInProgram (ProgramItem item, Program parentProgram) 785 { 786 if (item instanceof Program) 787 { 788 // The program item is already the program it self or another program 789 return item.equals(parentProgram) ? "" : null; 790 } 791 792 List<String> paths = new ArrayList<>(); 793 paths.add(item.getName()); 794 795 ProgramItem parent = getParentProgramItem(item, parentProgram); 796 while (parent != null && !(parent instanceof Program)) 797 { 798 paths.add(parent.getName()); 799 parent = getParentProgramItem(parent, parentProgram); 800 } 801 802 if (parent != null) 803 { 804 paths.add(parent.getName()); 805 Collections.reverse(paths); 806 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 807 } 808 809 return null; 810 } 811 812 /** 813 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 814 * The path is construct with the contents' names and the used separator is '/'. 815 * @param contentId The id of the content 816 * @param parentCourseId The id of parent course. Can not be null. 817 * @return the path into the parent course or null if the item is not part of this course. 818 */ 819 @Callable 820 public String getPathInCourse (String contentId, String parentCourseId) 821 { 822 Content content = _resolver.resolveById(contentId); 823 Course parentCourse = _resolver.resolveById(parentCourseId); 824 825 return getPathInCourse(content, parentCourse); 826 } 827 828 /** 829 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 830 * The path is construct with the contents' names and the used separator is '/'. 831 * @param courseOrList The course or the course list 832 * @param parentCourse The parent course. Can not be null. 833 * @return the path into the parent course or null if the item is not part of this course. 834 */ 835 public String getPathInCourse(Content courseOrList, Course parentCourse) 836 { 837 if (courseOrList.equals(parentCourse)) 838 { 839 return ""; 840 } 841 842 String path = _getPathInCourse(courseOrList, parentCourse); 843 844 return path; 845 } 846 847 private String _getPathInCourse(Content content, Content parentContent) 848 { 849 if (content.equals(parentContent)) 850 { 851 return content.getName(); 852 } 853 854 List<? extends Content> parents; 855 856 if (content instanceof Course) 857 { 858 parents = ((Course) content).getParentCourseLists(); 859 } 860 else if (content instanceof CourseList) 861 { 862 parents = ((CourseList) content).getParentCourses(); 863 } 864 else 865 { 866 throw new IllegalStateException(); 867 } 868 869 for (Content parent : parents) 870 { 871 String path = _getPathInCourse(parent, parentContent); 872 if (path != null) 873 { 874 return path + '/' + content.getName(); 875 } 876 } 877 return null; 878 } 879 880 /** 881 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 882 * The path is construct with the contents' names and the used separator is '/'. 883 * @param orgUnitId The id of the orgunit 884 * @param rootOrgUnitId The root orgunit id 885 * @return the path into the parent program or null if the item is not part of this program. 886 */ 887 @Callable 888 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 889 { 890 OrgUnit rootOU = null; 891 if (StringUtils.isNotBlank(rootOrgUnitId)) 892 { 893 rootOU = _resolver.resolveById(rootOrgUnitId); 894 } 895 else 896 { 897 rootOU = _ouRootProvider.getRoot(); 898 } 899 900 if (orgUnitId.equals(rootOU.getId())) 901 { 902 // The orgunit is already the root orgunit 903 return rootOU.getName(); 904 } 905 906 OrgUnit ou = _resolver.resolveById(orgUnitId); 907 908 List<String> paths = new ArrayList<>(); 909 paths.add(ou.getName()); 910 911 OrgUnit parent = ou.getParentOrgUnit(); 912 while (parent != null && !parent.getId().equals(rootOU.getId())) 913 { 914 paths.add(parent.getName()); 915 parent = parent.getParentOrgUnit(); 916 } 917 918 if (parent != null) 919 { 920 paths.add(rootOU.getName()); 921 Collections.reverse(paths); 922 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 923 } 924 925 return null; 926 } 927 928 /** 929 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 930 * The path is construct with the contents' names and the used separator is '/'. 931 * @param orgUnitId The id of the orgunit 932 * @return the path into the parent program or null if the item is not part of this program. 933 */ 934 @Callable 935 public String getOrgUnitPath(String orgUnitId) 936 { 937 return getOrgUnitPath(orgUnitId, null); 938 } 939 940 /** 941 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 942 * @param part The program part 943 * @param parentId The ancestor id 944 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 945 */ 946 public boolean hasAncestor (ProgramPart part, String parentId) 947 { 948 List<ProgramPart> parents = part.getProgramPartParents(); 949 950 for (ProgramPart parent : parents) 951 { 952 if (parent.getId().equals(parentId)) 953 { 954 return true; 955 } 956 else if (hasAncestor(parent, parentId)) 957 { 958 return true; 959 } 960 } 961 962 return false; 963 } 964 965 /** 966 * Check if a relation can be establish between two ODF contents 967 * @param srcContent The source content (copied or moved) 968 * @param targetContent The target content 969 * @param errors The list of error messages 970 * @param contextualParameters the contextual parameters 971 * @return true if the relation is valid, false otherwise 972 */ 973 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 974 { 975 boolean isCompatible = true; 976 977 if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit) 978 { 979 if (!_isContentTypeCompatible(srcContent, targetContent)) 980 { 981 // Invalid relations between content types 982 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 983 isCompatible = false; 984 } 985 else if (!_isCatalogCompatible(srcContent, targetContent)) 986 { 987 // Catalog is invalid 988 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 989 isCompatible = false; 990 } 991 else if (!_isLanguageCompatible(srcContent, targetContent)) 992 { 993 // Language is invalid 994 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 995 isCompatible = false; 996 } 997 else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters)) 998 { 999 // Shareable fields don't match 1000 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 1001 isCompatible = false; 1002 } 1003 } 1004 else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit) 1005 { 1006 // If the target isn't ODF related but the source is, the relation is not compatible. 1007 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 1008 isCompatible = false; 1009 } 1010 1011 return isCompatible; 1012 } 1013 1014 private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList) 1015 { 1016 return courseList.getCourses().contains(course); 1017 } 1018 1019 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 1020 { 1021 if (srcContent instanceof Container || srcContent instanceof SubProgram) 1022 { 1023 return targetContent instanceof AbstractTraversableProgramPart; 1024 } 1025 else if (srcContent instanceof CourseList) 1026 { 1027 return targetContent instanceof CourseListContainer; 1028 } 1029 else if (srcContent instanceof Course) 1030 { 1031 return targetContent instanceof CourseList; 1032 } 1033 else if (srcContent instanceof OrgUnit) 1034 { 1035 return targetContent instanceof OrgUnit; 1036 } 1037 1038 return false; 1039 } 1040 1041 private boolean _isCatalogCompatible(Content srcContent, Content targetContent) 1042 { 1043 if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem) 1044 { 1045 return ((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog()); 1046 } 1047 return true; 1048 } 1049 1050 private boolean _isLanguageCompatible(Content srcContent, Content targetContent) 1051 { 1052 return srcContent.getLanguage().equals(targetContent.getLanguage()); 1053 } 1054 1055 private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters) 1056 { 1057 // We check shareable fields only if the course content is not created (or created by copy) and not moved 1058 if (srcContent instanceof Course 1059 && targetContent instanceof CourseList 1060 && _shareableCourseHelper.handleShareableCourse() 1061 && !"create".equals(contextualParameters.get("mode")) 1062 && !"copy".equals(contextualParameters.get("mode")) 1063 && !"move".equals(contextualParameters.get("mode")) 1064 // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields 1065 && !_isCourseAlreadyBelongToCourseList((Course) srcContent, (CourseList) targetContent)) 1066 { 1067 return _shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent); 1068 } 1069 1070 return true; 1071 } 1072 1073 private List<String> _getContentParameters(Content srcContent, Content targetContent) 1074 { 1075 List<String> parameters = new ArrayList<>(); 1076 parameters.add(srcContent.getTitle()); 1077 parameters.add(srcContent.getId()); 1078 parameters.add(targetContent.getTitle()); 1079 parameters.add(targetContent.getId()); 1080 return parameters; 1081 } 1082 /** 1083 * Copy a {@link ProgramItem} 1084 * @param srcContent The program item to copy 1085 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1086 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1087 * @param copiedPrograms the id of initial programs with their copied content 1088 * @param copiedSubPrograms the id of initial subprograms with their copied content 1089 * @param copiedContainers the id of initial containers with their copied content 1090 * @param copiedCourseLists the id of initial course lists with their copied content 1091 * @param copiedCourses the id of initial courses with their copied content 1092 * @param copiedCourseParts the id of initial course parts with their copied content 1093 * @return The created content 1094 * @param <C> The modifiable content return type 1095 * @throws AmetysRepositoryException If an error occurred during copy 1096 * @throws WorkflowException If an error occurred during copy 1097 */ 1098 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1099 { 1100 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1101 } 1102 1103 /** 1104 * Copy a {@link ProgramItem} 1105 * @param srcContent The program item to copy 1106 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1107 * @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. 1108 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1109 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1110 * @param copiedPrograms the id of initial programs with their copied content 1111 * @param copiedSubPrograms the id of initial subprograms with their copied content 1112 * @param copiedContainers the id of initial containers with their copied content 1113 * @param copiedCourseLists the id of initial course lists with their copied content 1114 * @param copiedCourses the id of initial courses with their copied content 1115 * @param copiedCourseParts the id of initial course parts with their copied content 1116 * @param <C> The modifiable content return type 1117 * @return The created content 1118 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1119 * @throws AmetysRepositoryException If an error occurred 1120 * @throws WorkflowException If an error occurred 1121 */ 1122 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1123 { 1124 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1125 } 1126 1127 /** 1128 * Copy a {@link CoursePart} 1129 * @param srcContent The course part to copy 1130 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1131 * @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. 1132 * @param initWorkflowActionId The initial workflow action id 1133 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1134 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1135 * @param copiedPrograms the id of initial programs with their copied content 1136 * @param copiedSubPrograms the id of initial subprograms with their copied content 1137 * @param copiedContainers the id of initial containers with their copied content 1138 * @param copiedCourseLists the id of initial course lists with their copied content 1139 * @param copiedCourses the id of initial courses with their copied content 1140 * @param copiedCourseParts the id of initial course parts with their copied content 1141 * @param <C> The modifiable content return type 1142 * @return The created content 1143 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1144 * @throws AmetysRepositoryException If an error occurred 1145 * @throws WorkflowException If an error occurred 1146 */ 1147 public <C extends ModifiableContent> C copyCoursePart(CoursePart srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1148 { 1149 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1150 } 1151 1152 /** 1153 * Copy a {@link ProgramItem} 1154 * @param srcContent The program item to copy 1155 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1156 * @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. 1157 * @param initWorkflowActionId The initial workflow action id 1158 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1159 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1160 * @param copiedPrograms the id of initial programs with their copied content 1161 * @param copiedSubPrograms the id of initial subprograms with their copied content 1162 * @param copiedContainers the id of initial containers with their copied content 1163 * @param copiedCourseLists the id of initial course lists with their copied content 1164 * @param copiedCourses the id of initial courses with their copied content 1165 * @param copiedCourseParts the id of initial course parts with their copied content 1166 * @param <C> The modifiable content return type 1167 * @return The created content 1168 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1169 * @throws AmetysRepositoryException If an error occurred 1170 * @throws WorkflowException If an error occurred 1171 */ 1172 public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1173 { 1174 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1175 } 1176 1177 /** 1178 * Copy a {@link ProgramItem} 1179 * @param srcContent The program item to copy 1180 * @param catalog The catalog 1181 * @param code The odf content code 1182 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1183 * @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. 1184 * @param initWorkflowActionId The initial workflow action id 1185 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1186 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1187 * @param copiedPrograms the id of initial programs with their copied content 1188 * @param copiedSubPrograms the id of initial subprograms with their copied content 1189 * @param copiedContainers the id of initial containers with their copied content 1190 * @param copiedCourseLists the id of initial course lists with their copied content 1191 * @param copiedCourses the id of initial courses with their copied content 1192 * @param copiedCourseParts the id of initial course parts with their copied content 1193 * @param <C> The modifiable content return type 1194 * @return The created content 1195 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1196 * @throws AmetysRepositoryException If an error occurred 1197 * @throws WorkflowException If an error occurred 1198 */ 1199 @SuppressWarnings("unchecked") 1200 private <C extends ModifiableContent> C _copyODFContent(Content srcContent, String catalog, String code, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1201 { 1202 String computedTargetLanguage = targetContentLanguage; 1203 if (computedTargetLanguage == null) 1204 { 1205 computedTargetLanguage = srcContent.getLanguage(); 1206 } 1207 1208 String computeTargetName = targetContentName; 1209 if (computeTargetName == null) 1210 { 1211 // Compute content name from source content and requested language 1212 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 1213 } 1214 1215 String computeTargetCatalog = targetCatalog; 1216 if (computeTargetCatalog == null) 1217 { 1218 computeTargetCatalog = catalog; 1219 } 1220 1221 String principalContentType = srcContent.getTypes()[0]; 1222 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 1223 if (createdContent != null) 1224 { 1225 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 1226 } 1227 else 1228 { 1229 // Copy content waiting for observers to be completed and copying ACL 1230 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true); 1231 1232 if (fullCopy) 1233 { 1234 _cleanContentMetadata(createdContent); 1235 1236 if (targetCatalog != null) 1237 { 1238 if (createdContent instanceof ProgramItem) 1239 { 1240 ((ProgramItem) createdContent).setCatalog(targetCatalog); 1241 } 1242 else if (createdContent instanceof CoursePart) 1243 { 1244 ((CoursePart) createdContent).setCatalog(targetCatalog); 1245 } 1246 1247 } 1248 1249 if (srcContent instanceof ProgramItem) 1250 { 1251 copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1252 } 1253 1254 createdContent.saveChanges(); 1255 } 1256 1257 _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1258 } 1259 1260 return (C) createdContent; 1261 } 1262 1263 private void _putInCopiedMap(Content srcContent, ModifiableContent createdContent, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) 1264 { 1265 if (createdContent instanceof Program) 1266 { 1267 copiedPrograms.put(srcContent.getId(), createdContent.getId()); 1268 } 1269 else if (createdContent instanceof SubProgram) 1270 { 1271 copiedSubPrograms.put(srcContent.getId(), createdContent.getId()); 1272 } 1273 else if (createdContent instanceof Container) 1274 { 1275 copiedContainers.put(srcContent.getId(), createdContent.getId()); 1276 } 1277 else if (createdContent instanceof CourseList) 1278 { 1279 copiedCourseLists.put(srcContent.getId(), createdContent.getId()); 1280 } 1281 else if (createdContent instanceof Course) 1282 { 1283 copiedCourses.put(srcContent.getId(), createdContent.getId()); 1284 } 1285 else if (createdContent instanceof CoursePart) 1286 { 1287 copiedCourseParts.put(srcContent.getId(), createdContent.getId()); 1288 } 1289 } 1290 1291 /** 1292 * Copy the structure of a {@link ProgramItem} 1293 * @param srcContent the content to copy 1294 * @param targetContent the target content 1295 * @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. 1296 * @param initWorkflowActionId The initial workflow action id 1297 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1298 * @param copiedPrograms the id of initial programs with their copied content 1299 * @param copiedSubPrograms the id of initial subprograms with their copied content 1300 * @param copiedContainers the id of initial containers with their copied content 1301 * @param copiedCourseLists the id of initial course lists with their copied content 1302 * @param copiedCourses the id of initial courses with their copied content 1303 * @param copiedCourseParts the id of initial course parts with their copied content 1304 * @throws AmetysRepositoryException If an error occurred during copy 1305 * @throws WorkflowException If an error occurred during copy 1306 */ 1307 protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException 1308 { 1309 List<ProgramItem> srcChildContents = new ArrayList<>(); 1310 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1311 1312 String childMetadataPath = null; 1313 String parentMetadataPath = null; 1314 1315 if (srcContent instanceof TraversableProgramPart) 1316 { 1317 childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS; 1318 parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS; 1319 srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren()); 1320 } 1321 else if (srcContent instanceof CourseList) 1322 { 1323 childMetadataPath = CourseList.CHILD_COURSES; 1324 parentMetadataPath = Course.PARENT_COURSE_LISTS; 1325 srcChildContents.addAll(((CourseList) srcContent).getCourses()); 1326 } 1327 else if (srcContent instanceof Course) 1328 { 1329 childMetadataPath = Course.CHILD_COURSE_LISTS; 1330 parentMetadataPath = CourseList.PARENT_COURSES; 1331 srcChildContents.addAll(((Course) srcContent).getCourseLists()); 1332 1333 List<String> refCoursePartIds = new ArrayList<>(); 1334 for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts()) 1335 { 1336 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1337 refCoursePartIds.add(targetChildContent.getId()); 1338 } 1339 _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds); 1340 } 1341 1342 List<String> refChildIds = new ArrayList<>(); 1343 for (ProgramItem srcChildContent : srcChildContents) 1344 { 1345 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1346 refChildIds.add(targetChildContent.getId()); 1347 } 1348 1349 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1350 1351 _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values); 1352 } 1353 1354 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1355 { 1356 if (!refChildIds.isEmpty()) 1357 { 1358 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1359 } 1360 } 1361 1362 private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1363 { 1364 if (!values.isEmpty()) 1365 { 1366 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1367 { 1368 String childMetadataName = entry.getKey().getLeft(); 1369 String parentMetadataName = entry.getKey().getRight(); 1370 List<String> childContents = entry.getValue(); 1371 1372 parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()])); 1373 1374 for (String childContentId : childContents) 1375 { 1376 ModifiableContent content = _resolver.resolveById(childContentId); 1377 String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName); 1378 content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId())); 1379 content.saveChanges(); 1380 } 1381 } 1382 } 1383 } 1384 1385 /** 1386 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1387 * @param createdContent The created content to clean 1388 */ 1389 protected void _cleanContentMetadata(ModifiableContent createdContent) 1390 { 1391 if (createdContent instanceof ProgramPart) 1392 { 1393 _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS); 1394 } 1395 1396 if (createdContent instanceof TraversableProgramPart) 1397 { 1398 _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS); 1399 } 1400 1401 if (createdContent instanceof CourseList) 1402 { 1403 _removeFullValue(createdContent, CourseList.CHILD_COURSES); 1404 _removeFullValue(createdContent, CourseList.PARENT_COURSES); 1405 } 1406 1407 if (createdContent instanceof Course) 1408 { 1409 _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS); 1410 _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS); 1411 _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS); 1412 } 1413 1414 if (createdContent instanceof CoursePart) 1415 { 1416 _removeFullValue(createdContent, CoursePart.PARENT_COURSES); 1417 } 1418 } 1419 1420 private void _removeFullValue(ModifiableContent content, String attributeName) 1421 { 1422 content.removeValue(attributeName); 1423 content.removeExternalizableMetadataIfExists(attributeName); 1424 } 1425 1426 /** 1427 * Switch the ametys object to Live version if it has one 1428 * @param ao the Ametys object 1429 * @throws NoLiveVersionException if the content has no live version 1430 */ 1431 public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException 1432 { 1433 // Switch to the Live label if exists 1434 String[] allLabels = ao.getAllLabels(); 1435 String[] currentLabels = ao.getLabels(); 1436 1437 boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL); 1438 boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL); 1439 1440 if (hasLiveVersion && !currentVersionIsLive) 1441 { 1442 ao.switchToLabel(CmsConstants.LIVE_LABEL); 1443 } 1444 else if (!hasLiveVersion) 1445 { 1446 throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version"); 1447 } 1448 } 1449 1450 /** 1451 * Switch to Live version if is required 1452 * @param ao the Ametys object 1453 * @throws NoLiveVersionException if the Live version is required but not exist 1454 */ 1455 public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException 1456 { 1457 Request request = _getRequest(); 1458 if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null) 1459 { 1460 switchToLiveVersion(ao); 1461 } 1462 } 1463 1464 /** 1465 * Count the hours accumulation in the {@link ProgramItem} 1466 * @param programItem The program item on which we compute the total number of hours 1467 * @return The hours accumulation 1468 */ 1469 public Double getCumulatedHours(ProgramItem programItem) 1470 { 1471 // Ignore optional course list and avoid useless expensive calls 1472 if (programItem instanceof CourseList && ChoiceType.OPTIONAL.equals(((CourseList) programItem).getType())) 1473 { 1474 return 0.0; 1475 } 1476 1477 List<ProgramItem> children = getChildProgramItems(programItem); 1478 1479 Double coef = 1.0; 1480 Double countNbHours = 0.0; 1481 1482 // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total) 1483 if (programItem instanceof CourseList) 1484 { 1485 // If there is no children, compute the coef is useless 1486 // Also choice list can throw an exception while dividing by zero 1487 if (children.isEmpty()) 1488 { 1489 return 0.0; 1490 } 1491 1492 CourseList courseList = (CourseList) programItem; 1493 switch (courseList.getType()) 1494 { 1495 case CHOICE: 1496 // Apply the average of number of EC from children multiply by the minimum ELP to select 1497 coef = ((double) courseList.getMinNumberOfCourses()) / children.size(); 1498 break; 1499 case MANDATORY: 1500 default: 1501 // Add all ECTS from children 1502 break; 1503 } 1504 } 1505 1506 // If it's a course and we have a value for the number of hours 1507 // Then get the value 1508 if (programItem instanceof Course && ((Course) programItem).hasValue(Course.NUMBER_OF_HOURS)) 1509 { 1510 countNbHours += ((Course) programItem).<Double>getValue(Course.NUMBER_OF_HOURS); 1511 } 1512 // Else if there are program item children on the item 1513 // Then compute on children 1514 else if (children.size() > 0) 1515 { 1516 for (ProgramItem child : children) 1517 { 1518 countNbHours += getCumulatedHours(child); 1519 } 1520 } 1521 // Else, it's a course but there is no value for the number of hours and we don't have program item children 1522 // Then compute on course parts 1523 else if (programItem instanceof Course) 1524 { 1525 countNbHours += ((Course) programItem).getCourseParts() 1526 .stream() 1527 .mapToDouble(CoursePart::getNumberOfHours) 1528 .sum(); 1529 } 1530 1531 return coef * countNbHours; 1532 } 1533 1534 /** 1535 * Get the request 1536 * @return the request 1537 */ 1538 protected Request _getRequest() 1539 { 1540 return ContextHelper.getRequest(_context); 1541 } 1542 1543 /** 1544 * Get the first orgunit matching the given UAI code 1545 * @param uaiCode the UAI code 1546 * @return the orgunit or null if not found 1547 */ 1548 public OrgUnit getOrgUnitByUAICode(String uaiCode) 1549 { 1550 Expression expr = new AndExpression( 1551 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 1552 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 1553 ); 1554 1555 String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr); 1556 AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery); 1557 1558 return orgUnits.stream() 1559 .findFirst() 1560 .orElse(null); 1561 } 1562}