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