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) or orgunit (no structure for now) 638 * @param contentId the content id 639 * @return a map of information 640 */ 641 @Callable 642 public Map<String, Object> getStructureInfo(String contentId) 643 { 644 Map<String, Object> results = new HashMap<>(); 645 646 if (StringUtils.isNotBlank(contentId)) 647 { 648 Content content = _resolver.resolveById(contentId); 649 if (content instanceof ProgramItem) 650 { 651 ProgramItem programItem = (ProgramItem) content; 652 653 results.put("id", contentId); 654 results.put("title", content.getTitle()); 655 results.put("code", programItem.getCode()); 656 657 List<ProgramItem> childProgramItems = getChildProgramItems(programItem); 658 results.put("hasChildren", !childProgramItems.isEmpty()); 659 660 List<ProgramItem> parentProgramItems = getParentProgramItems(programItem); 661 results.put("hasParent", !parentProgramItems.isEmpty()); 662 663 results.put("paths", getPaths(programItem, " > ")); 664 } 665 else if (content instanceof OrgUnit) 666 { 667 OrgUnit orgunit = (OrgUnit) content; 668 669 results.put("id", contentId); 670 results.put("title", content.getTitle()); 671 results.put("code", orgunit.getUAICode()); 672 673 // Always to false, we don't manage complete copy with children 674 results.put("hasChildren", false); 675 676 results.put("hasParent", orgunit.getParentOrgUnit() != null); 677 678 results.put("paths", List.of(getOrgUnitPath(orgunit, " > "))); 679 } 680 } 681 682 return results; 683 } 684 685 /** 686 * Get information of the program item structure (type, if program has children) 687 * @param programItemIds the list of program item id 688 * @return a map of information 689 */ 690 @Callable 691 public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds) 692 { 693 Map<String, Map<String, Object>> results = new HashMap<>(); 694 695 for (String programItemId : programItemIds) 696 { 697 results.put(programItemId, getStructureInfo(programItemId)); 698 } 699 700 return results; 701 } 702 703 /** 704 * Get all the path of the orgunit.<br> 705 * The path is built with the contents' title and code 706 * @param orgunit The orgunit 707 * @param separator The path separator 708 * @return the path in parent orgunit 709 */ 710 public String getOrgUnitPath(OrgUnit orgunit, String separator) 711 { 712 String path = orgunit.getTitle() + " (" + orgunit.getUAICode() + ")"; 713 OrgUnit parent = orgunit.getParentOrgUnit(); 714 if (parent != null) 715 { 716 path = getOrgUnitPath(parent, separator) + separator + path; 717 } 718 return path; 719 } 720 721 /** 722 * Get all the paths of a ODF content.<br> 723 * The path is construct with the contents' title 724 * @param separator The path separator 725 * @param item The program item 726 * @return the paths in parent program items 727 */ 728 protected List<String> getPaths(ProgramItem item, String separator) 729 { 730 List<String> paths = new ArrayList<>(); 731 732 List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(item); 733 for (List<ProgramItem> ancestorPath : ancestorPaths) 734 { 735 List<String> titles = ancestorPath.stream().map(p -> ((Content) p).getTitle() + " (" + p.getCode() + ")").collect(Collectors.toList()); 736 paths.add(String.join(separator, titles)); 737 } 738 739 return paths; 740 } 741 742 /** 743 * Get the full path to program item for highest ancestors. The path includes this final item. 744 * @param item the program item 745 * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item. 746 */ 747 public List<List<ProgramItem>> getPathOfAncestors(ProgramItem item) 748 { 749 List<List<ProgramItem>> ancestors = new ArrayList<>(); 750 751 List<ProgramItem> parentProgramItems = getParentProgramItems(item); 752 if (parentProgramItems.isEmpty()) 753 { 754 List<ProgramItem> items = new ArrayList<>(); 755 items.add(item); 756 ancestors.add(items); 757 return ancestors; 758 } 759 760 for (ProgramItem parentProgramItem : parentProgramItems) 761 { 762 for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem)) 763 { 764 ancestorPaths.add(item); 765 ancestors.add(ancestorPaths); 766 } 767 } 768 769 return ancestors; 770 } 771 772 /** 773 * Get the path of a {@link ProgramItem} into a {@link Program}<br> 774 * The path is construct with the contents' names and the used separator is '/'. 775 * @param programItemId The id of the program item 776 * @param programId The id of program. Can not be null. 777 * @return the path into the parent program or null if the item is not part of this program. 778 */ 779 @Callable 780 public String getPathInProgram (String programItemId, String programId) 781 { 782 ProgramItem item = _resolver.resolveById(programItemId); 783 Program program = _resolver.resolveById(programId); 784 785 return getPathInProgram(item, program); 786 } 787 788 /** 789 * Get the path of a ODF content into a {@link Program}.<br> 790 * The path is construct with the contents' names and the used separator is '/'. 791 * @param item The program item 792 * @param parentProgram The parent root (sub)program. Can not be null. 793 * @return the path from the parent program 794 */ 795 public String getPathInProgram (ProgramItem item, Program parentProgram) 796 { 797 if (item instanceof Program) 798 { 799 // The program item is already the program it self or another program 800 return item.equals(parentProgram) ? "" : null; 801 } 802 803 List<String> paths = new ArrayList<>(); 804 paths.add(item.getName()); 805 806 ProgramItem parent = getParentProgramItem(item, parentProgram); 807 while (parent != null && !(parent instanceof Program)) 808 { 809 paths.add(parent.getName()); 810 parent = getParentProgramItem(parent, parentProgram); 811 } 812 813 if (parent != null) 814 { 815 paths.add(parent.getName()); 816 Collections.reverse(paths); 817 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 818 } 819 820 return null; 821 } 822 823 /** 824 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 825 * The path is construct with the contents' names and the used separator is '/'. 826 * @param contentId The id of the content 827 * @param parentCourseId The id of parent course. Can not be null. 828 * @return the path into the parent course or null if the item is not part of this course. 829 */ 830 @Callable 831 public String getPathInCourse (String contentId, String parentCourseId) 832 { 833 Content content = _resolver.resolveById(contentId); 834 Course parentCourse = _resolver.resolveById(parentCourseId); 835 836 return getPathInCourse(content, parentCourse); 837 } 838 839 /** 840 * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br> 841 * The path is construct with the contents' names and the used separator is '/'. 842 * @param courseOrList The course or the course list 843 * @param parentCourse The parent course. Can not be null. 844 * @return the path into the parent course or null if the item is not part of this course. 845 */ 846 public String getPathInCourse(Content courseOrList, Course parentCourse) 847 { 848 if (courseOrList.equals(parentCourse)) 849 { 850 return ""; 851 } 852 853 String path = _getPathInCourse(courseOrList, parentCourse); 854 855 return path; 856 } 857 858 private String _getPathInCourse(Content content, Content parentContent) 859 { 860 if (content.equals(parentContent)) 861 { 862 return content.getName(); 863 } 864 865 List<? extends Content> parents; 866 867 if (content instanceof Course) 868 { 869 parents = ((Course) content).getParentCourseLists(); 870 } 871 else if (content instanceof CourseList) 872 { 873 parents = ((CourseList) content).getParentCourses(); 874 } 875 else 876 { 877 throw new IllegalStateException(); 878 } 879 880 for (Content parent : parents) 881 { 882 String path = _getPathInCourse(parent, parentContent); 883 if (path != null) 884 { 885 return path + '/' + content.getName(); 886 } 887 } 888 return null; 889 } 890 891 /** 892 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br> 893 * The path is construct with the contents' names and the used separator is '/'. 894 * @param orgUnitId The id of the orgunit 895 * @param rootOrgUnitId The root orgunit id 896 * @return the path into the parent program or null if the item is not part of this program. 897 */ 898 @Callable 899 public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId) 900 { 901 OrgUnit rootOU = null; 902 if (StringUtils.isNotBlank(rootOrgUnitId)) 903 { 904 rootOU = _resolver.resolveById(rootOrgUnitId); 905 } 906 else 907 { 908 rootOU = _ouRootProvider.getRoot(); 909 } 910 911 if (orgUnitId.equals(rootOU.getId())) 912 { 913 // The orgunit is already the root orgunit 914 return rootOU.getName(); 915 } 916 917 OrgUnit ou = _resolver.resolveById(orgUnitId); 918 919 List<String> paths = new ArrayList<>(); 920 paths.add(ou.getName()); 921 922 OrgUnit parent = ou.getParentOrgUnit(); 923 while (parent != null && !parent.getId().equals(rootOU.getId())) 924 { 925 paths.add(parent.getName()); 926 parent = parent.getParentOrgUnit(); 927 } 928 929 if (parent != null) 930 { 931 paths.add(rootOU.getName()); 932 Collections.reverse(paths); 933 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 934 } 935 936 return null; 937 } 938 939 /** 940 * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br> 941 * The path is construct with the contents' names and the used separator is '/'. 942 * @param orgUnitId The id of the orgunit 943 * @return the path into the parent program or null if the item is not part of this program. 944 */ 945 @Callable 946 public String getOrgUnitPath(String orgUnitId) 947 { 948 return getOrgUnitPath(orgUnitId, null); 949 } 950 951 /** 952 * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 953 * @param part The program part 954 * @param parentId The ancestor id 955 * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id 956 */ 957 public boolean hasAncestor (ProgramPart part, String parentId) 958 { 959 List<ProgramPart> parents = part.getProgramPartParents(); 960 961 for (ProgramPart parent : parents) 962 { 963 if (parent.getId().equals(parentId)) 964 { 965 return true; 966 } 967 else if (hasAncestor(parent, parentId)) 968 { 969 return true; 970 } 971 } 972 973 return false; 974 } 975 976 /** 977 * Check if a relation can be establish between two ODF contents 978 * @param srcContent The source content (copied or moved) 979 * @param targetContent The target content 980 * @param errors The list of error messages 981 * @param contextualParameters the contextual parameters 982 * @return true if the relation is valid, false otherwise 983 */ 984 public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters) 985 { 986 boolean isCompatible = true; 987 988 if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit) 989 { 990 if (!_isContentTypeCompatible(srcContent, targetContent)) 991 { 992 // Invalid relations between content types 993 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent))); 994 isCompatible = false; 995 } 996 else if (!_isCatalogCompatible(srcContent, targetContent)) 997 { 998 // Catalog is invalid 999 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent))); 1000 isCompatible = false; 1001 } 1002 else if (!_isLanguageCompatible(srcContent, targetContent)) 1003 { 1004 // Language is invalid 1005 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent))); 1006 isCompatible = false; 1007 } 1008 else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters)) 1009 { 1010 // Shareable fields don't match 1011 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent))); 1012 isCompatible = false; 1013 } 1014 } 1015 else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit) 1016 { 1017 // If the target isn't ODF related but the source is, the relation is not compatible. 1018 errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent))); 1019 isCompatible = false; 1020 } 1021 1022 return isCompatible; 1023 } 1024 1025 private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList) 1026 { 1027 return courseList.getCourses().contains(course); 1028 } 1029 1030 private boolean _isContentTypeCompatible(Content srcContent, Content targetContent) 1031 { 1032 if (srcContent instanceof Container || srcContent instanceof SubProgram) 1033 { 1034 return targetContent instanceof AbstractTraversableProgramPart; 1035 } 1036 else if (srcContent instanceof CourseList) 1037 { 1038 return targetContent instanceof CourseListContainer; 1039 } 1040 else if (srcContent instanceof Course) 1041 { 1042 return targetContent instanceof CourseList; 1043 } 1044 else if (srcContent instanceof OrgUnit) 1045 { 1046 return targetContent instanceof OrgUnit; 1047 } 1048 1049 return false; 1050 } 1051 1052 private boolean _isCatalogCompatible(Content srcContent, Content targetContent) 1053 { 1054 if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem) 1055 { 1056 return ((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog()); 1057 } 1058 return true; 1059 } 1060 1061 private boolean _isLanguageCompatible(Content srcContent, Content targetContent) 1062 { 1063 return srcContent.getLanguage().equals(targetContent.getLanguage()); 1064 } 1065 1066 private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters) 1067 { 1068 // We check shareable fields only if the course content is not created (or created by copy) and not moved 1069 if (srcContent instanceof Course 1070 && targetContent instanceof CourseList 1071 && _shareableCourseHelper.handleShareableCourse() 1072 && !"create".equals(contextualParameters.get("mode")) 1073 && !"copy".equals(contextualParameters.get("mode")) 1074 && !"move".equals(contextualParameters.get("mode")) 1075 // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields 1076 && !_isCourseAlreadyBelongToCourseList((Course) srcContent, (CourseList) targetContent)) 1077 { 1078 return _shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent); 1079 } 1080 1081 return true; 1082 } 1083 1084 private List<String> _getContentParameters(Content srcContent, Content targetContent) 1085 { 1086 List<String> parameters = new ArrayList<>(); 1087 parameters.add(srcContent.getTitle()); 1088 parameters.add(srcContent.getId()); 1089 parameters.add(targetContent.getTitle()); 1090 parameters.add(targetContent.getId()); 1091 return parameters; 1092 } 1093 /** 1094 * Copy a {@link ProgramItem} 1095 * @param srcContent The program item to copy 1096 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1097 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1098 * @param copiedPrograms the id of initial programs with their copied content 1099 * @param copiedSubPrograms the id of initial subprograms with their copied content 1100 * @param copiedContainers the id of initial containers with their copied content 1101 * @param copiedCourseLists the id of initial course lists with their copied content 1102 * @param copiedCourses the id of initial courses with their copied content 1103 * @param copiedCourseParts the id of initial course parts with their copied content 1104 * @return The created content 1105 * @param <C> The modifiable content return type 1106 * @throws AmetysRepositoryException If an error occurred during copy 1107 * @throws WorkflowException If an error occurred during copy 1108 */ 1109 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 1110 { 1111 return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1112 } 1113 1114 /** 1115 * Copy a {@link ProgramItem} 1116 * @param srcContent The program item to copy 1117 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1118 * @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. 1119 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1120 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1121 * @param copiedPrograms the id of initial programs with their copied content 1122 * @param copiedSubPrograms the id of initial subprograms with their copied content 1123 * @param copiedContainers the id of initial containers with their copied content 1124 * @param copiedCourseLists the id of initial course lists with their copied content 1125 * @param copiedCourses the id of initial courses with their copied content 1126 * @param copiedCourseParts the id of initial course parts with their copied content 1127 * @param <C> The modifiable content return type 1128 * @return The created content 1129 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1130 * @throws AmetysRepositoryException If an error occurred 1131 * @throws WorkflowException If an error occurred 1132 */ 1133 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 1134 { 1135 return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1136 } 1137 1138 /** 1139 * Copy a {@link CoursePart} 1140 * @param srcContent The course part to copy 1141 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1142 * @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. 1143 * @param initWorkflowActionId The initial workflow action id 1144 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1145 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1146 * @param copiedPrograms the id of initial programs with their copied content 1147 * @param copiedSubPrograms the id of initial subprograms with their copied content 1148 * @param copiedContainers the id of initial containers with their copied content 1149 * @param copiedCourseLists the id of initial course lists with their copied content 1150 * @param copiedCourses the id of initial courses with their copied content 1151 * @param copiedCourseParts the id of initial course parts with their copied content 1152 * @param <C> The modifiable content return type 1153 * @return The created content 1154 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1155 * @throws AmetysRepositoryException If an error occurred 1156 * @throws WorkflowException If an error occurred 1157 */ 1158 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 1159 { 1160 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1161 } 1162 1163 /** 1164 * Copy a {@link ProgramItem} 1165 * @param srcContent The program item to copy 1166 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1167 * @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. 1168 * @param initWorkflowActionId The initial workflow action id 1169 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1170 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1171 * @param copiedPrograms the id of initial programs with their copied content 1172 * @param copiedSubPrograms the id of initial subprograms with their copied content 1173 * @param copiedContainers the id of initial containers with their copied content 1174 * @param copiedCourseLists the id of initial course lists with their copied content 1175 * @param copiedCourses the id of initial courses with their copied content 1176 * @param copiedCourseParts the id of initial course parts with their copied content 1177 * @param <C> The modifiable content return type 1178 * @return The created content 1179 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1180 * @throws AmetysRepositoryException If an error occurred 1181 * @throws WorkflowException If an error occurred 1182 */ 1183 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 1184 { 1185 return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1186 } 1187 1188 /** 1189 * Copy a {@link ProgramItem} 1190 * @param srcContent The program item to copy 1191 * @param catalog The catalog 1192 * @param code The odf content code 1193 * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object. 1194 * @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. 1195 * @param initWorkflowActionId The initial workflow action id 1196 * @param fullCopy Set to <code>true</code> to copy the sub-structure 1197 * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object. 1198 * @param copiedPrograms the id of initial programs with their copied content 1199 * @param copiedSubPrograms the id of initial subprograms with their copied content 1200 * @param copiedContainers the id of initial containers with their copied content 1201 * @param copiedCourseLists the id of initial course lists with their copied content 1202 * @param copiedCourses the id of initial courses with their copied content 1203 * @param copiedCourseParts the id of initial course parts with their copied content 1204 * @param <C> The modifiable content return type 1205 * @return The created content 1206 * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists 1207 * @throws AmetysRepositoryException If an error occurred 1208 * @throws WorkflowException If an error occurred 1209 */ 1210 @SuppressWarnings("unchecked") 1211 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 1212 { 1213 String computedTargetLanguage = targetContentLanguage; 1214 if (computedTargetLanguage == null) 1215 { 1216 computedTargetLanguage = srcContent.getLanguage(); 1217 } 1218 1219 String computeTargetName = targetContentName; 1220 if (computeTargetName == null) 1221 { 1222 // Compute content name from source content and requested language 1223 computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : ""); 1224 } 1225 1226 String computeTargetCatalog = targetCatalog; 1227 if (computeTargetCatalog == null) 1228 { 1229 computeTargetCatalog = catalog; 1230 } 1231 1232 String principalContentType = srcContent.getTypes()[0]; 1233 ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage); 1234 if (createdContent != null) 1235 { 1236 getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage); 1237 } 1238 else 1239 { 1240 // Copy content waiting for observers to be completed and copying ACL 1241 createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true); 1242 1243 if (fullCopy) 1244 { 1245 _cleanContentMetadata(createdContent); 1246 1247 if (targetCatalog != null) 1248 { 1249 if (createdContent instanceof ProgramItem) 1250 { 1251 ((ProgramItem) createdContent).setCatalog(targetCatalog); 1252 } 1253 else if (createdContent instanceof CoursePart) 1254 { 1255 ((CoursePart) createdContent).setCatalog(targetCatalog); 1256 } 1257 1258 } 1259 1260 if (srcContent instanceof ProgramItem) 1261 { 1262 copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1263 } 1264 1265 createdContent.saveChanges(); 1266 } 1267 1268 _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1269 } 1270 1271 return (C) createdContent; 1272 } 1273 1274 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) 1275 { 1276 if (createdContent instanceof Program) 1277 { 1278 copiedPrograms.put(srcContent.getId(), createdContent.getId()); 1279 } 1280 else if (createdContent instanceof SubProgram) 1281 { 1282 copiedSubPrograms.put(srcContent.getId(), createdContent.getId()); 1283 } 1284 else if (createdContent instanceof Container) 1285 { 1286 copiedContainers.put(srcContent.getId(), createdContent.getId()); 1287 } 1288 else if (createdContent instanceof CourseList) 1289 { 1290 copiedCourseLists.put(srcContent.getId(), createdContent.getId()); 1291 } 1292 else if (createdContent instanceof Course) 1293 { 1294 copiedCourses.put(srcContent.getId(), createdContent.getId()); 1295 } 1296 else if (createdContent instanceof CoursePart) 1297 { 1298 copiedCourseParts.put(srcContent.getId(), createdContent.getId()); 1299 } 1300 } 1301 1302 /** 1303 * Copy the structure of a {@link ProgramItem} 1304 * @param srcContent the content to copy 1305 * @param targetContent the target content 1306 * @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. 1307 * @param initWorkflowActionId The initial workflow action id 1308 * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object. 1309 * @param copiedPrograms the id of initial programs with their copied content 1310 * @param copiedSubPrograms the id of initial subprograms with their copied content 1311 * @param copiedContainers the id of initial containers with their copied content 1312 * @param copiedCourseLists the id of initial course lists with their copied content 1313 * @param copiedCourses the id of initial courses with their copied content 1314 * @param copiedCourseParts the id of initial course parts with their copied content 1315 * @throws AmetysRepositoryException If an error occurred during copy 1316 * @throws WorkflowException If an error occurred during copy 1317 */ 1318 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 1319 { 1320 List<ProgramItem> srcChildContents = new ArrayList<>(); 1321 Map<Pair<String, String>, List<String>> values = new HashMap<>(); 1322 1323 String childMetadataPath = null; 1324 String parentMetadataPath = null; 1325 1326 if (srcContent instanceof TraversableProgramPart) 1327 { 1328 childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS; 1329 parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS; 1330 srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren()); 1331 } 1332 else if (srcContent instanceof CourseList) 1333 { 1334 childMetadataPath = CourseList.CHILD_COURSES; 1335 parentMetadataPath = Course.PARENT_COURSE_LISTS; 1336 srcChildContents.addAll(((CourseList) srcContent).getCourses()); 1337 } 1338 else if (srcContent instanceof Course) 1339 { 1340 childMetadataPath = Course.CHILD_COURSE_LISTS; 1341 parentMetadataPath = CourseList.PARENT_COURSES; 1342 srcChildContents.addAll(((Course) srcContent).getCourseLists()); 1343 1344 List<String> refCoursePartIds = new ArrayList<>(); 1345 for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts()) 1346 { 1347 CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1348 refCoursePartIds.add(targetChildContent.getId()); 1349 } 1350 _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds); 1351 } 1352 1353 List<String> refChildIds = new ArrayList<>(); 1354 for (ProgramItem srcChildContent : srcChildContents) 1355 { 1356 ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts); 1357 refChildIds.add(targetChildContent.getId()); 1358 } 1359 1360 _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds); 1361 1362 _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values); 1363 } 1364 1365 private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds) 1366 { 1367 if (!refChildIds.isEmpty()) 1368 { 1369 values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds); 1370 } 1371 } 1372 1373 private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException 1374 { 1375 if (!values.isEmpty()) 1376 { 1377 for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet()) 1378 { 1379 String childMetadataName = entry.getKey().getLeft(); 1380 String parentMetadataName = entry.getKey().getRight(); 1381 List<String> childContents = entry.getValue(); 1382 1383 parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()])); 1384 1385 for (String childContentId : childContents) 1386 { 1387 ModifiableContent content = _resolver.resolveById(childContentId); 1388 String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName); 1389 content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId())); 1390 content.saveChanges(); 1391 } 1392 } 1393 } 1394 } 1395 1396 /** 1397 * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure 1398 * @param createdContent The created content to clean 1399 */ 1400 protected void _cleanContentMetadata(ModifiableContent createdContent) 1401 { 1402 if (createdContent instanceof ProgramPart) 1403 { 1404 _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS); 1405 } 1406 1407 if (createdContent instanceof TraversableProgramPart) 1408 { 1409 _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS); 1410 } 1411 1412 if (createdContent instanceof CourseList) 1413 { 1414 _removeFullValue(createdContent, CourseList.CHILD_COURSES); 1415 _removeFullValue(createdContent, CourseList.PARENT_COURSES); 1416 } 1417 1418 if (createdContent instanceof Course) 1419 { 1420 _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS); 1421 _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS); 1422 _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS); 1423 } 1424 1425 if (createdContent instanceof CoursePart) 1426 { 1427 _removeFullValue(createdContent, CoursePart.PARENT_COURSES); 1428 } 1429 } 1430 1431 private void _removeFullValue(ModifiableContent content, String attributeName) 1432 { 1433 content.removeValue(attributeName); 1434 content.removeExternalizableMetadataIfExists(attributeName); 1435 } 1436 1437 /** 1438 * Switch the ametys object to Live version if it has one 1439 * @param ao the Ametys object 1440 * @throws NoLiveVersionException if the content has no live version 1441 */ 1442 public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException 1443 { 1444 // Switch to the Live label if exists 1445 String[] allLabels = ao.getAllLabels(); 1446 String[] currentLabels = ao.getLabels(); 1447 1448 boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL); 1449 boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL); 1450 1451 if (hasLiveVersion && !currentVersionIsLive) 1452 { 1453 ao.switchToLabel(CmsConstants.LIVE_LABEL); 1454 } 1455 else if (!hasLiveVersion) 1456 { 1457 throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version"); 1458 } 1459 } 1460 1461 /** 1462 * Switch to Live version if is required 1463 * @param ao the Ametys object 1464 * @throws NoLiveVersionException if the Live version is required but not exist 1465 */ 1466 public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException 1467 { 1468 Request request = _getRequest(); 1469 if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null) 1470 { 1471 switchToLiveVersion(ao); 1472 } 1473 } 1474 1475 /** 1476 * Count the hours accumulation in the {@link ProgramItem} 1477 * @param programItem The program item on which we compute the total number of hours 1478 * @return The hours accumulation 1479 */ 1480 public Double getCumulatedHours(ProgramItem programItem) 1481 { 1482 // Ignore optional course list and avoid useless expensive calls 1483 if (programItem instanceof CourseList && ChoiceType.OPTIONAL.equals(((CourseList) programItem).getType())) 1484 { 1485 return 0.0; 1486 } 1487 1488 List<ProgramItem> children = getChildProgramItems(programItem); 1489 1490 Double coef = 1.0; 1491 Double countNbHours = 0.0; 1492 1493 // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total) 1494 if (programItem instanceof CourseList) 1495 { 1496 // If there is no children, compute the coef is useless 1497 // Also choice list can throw an exception while dividing by zero 1498 if (children.isEmpty()) 1499 { 1500 return 0.0; 1501 } 1502 1503 CourseList courseList = (CourseList) programItem; 1504 switch (courseList.getType()) 1505 { 1506 case CHOICE: 1507 // Apply the average of number of EC from children multiply by the minimum ELP to select 1508 coef = ((double) courseList.getMinNumberOfCourses()) / children.size(); 1509 break; 1510 case MANDATORY: 1511 default: 1512 // Add all ECTS from children 1513 break; 1514 } 1515 } 1516 1517 // If it's a course and we have a value for the number of hours 1518 // Then get the value 1519 if (programItem instanceof Course && ((Course) programItem).hasValue(Course.NUMBER_OF_HOURS)) 1520 { 1521 countNbHours += ((Course) programItem).<Double>getValue(Course.NUMBER_OF_HOURS); 1522 } 1523 // Else if there are program item children on the item 1524 // Then compute on children 1525 else if (children.size() > 0) 1526 { 1527 for (ProgramItem child : children) 1528 { 1529 countNbHours += getCumulatedHours(child); 1530 } 1531 } 1532 // Else, it's a course but there is no value for the number of hours and we don't have program item children 1533 // Then compute on course parts 1534 else if (programItem instanceof Course) 1535 { 1536 countNbHours += ((Course) programItem).getCourseParts() 1537 .stream() 1538 .mapToDouble(CoursePart::getNumberOfHours) 1539 .sum(); 1540 } 1541 1542 return coef * countNbHours; 1543 } 1544 1545 /** 1546 * Get the request 1547 * @return the request 1548 */ 1549 protected Request _getRequest() 1550 { 1551 return ContextHelper.getRequest(_context); 1552 } 1553 1554 /** 1555 * Get the first orgunit matching the given UAI code 1556 * @param uaiCode the UAI code 1557 * @return the orgunit or null if not found 1558 */ 1559 public OrgUnit getOrgUnitByUAICode(String uaiCode) 1560 { 1561 Expression expr = new AndExpression( 1562 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 1563 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 1564 ); 1565 1566 String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr); 1567 AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery); 1568 1569 return orgUnits.stream() 1570 .findFirst() 1571 .orElse(null); 1572 } 1573}