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