001/* 002 * Copyright 2021 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 */ 016 017package org.ametys.plugins.odfsync.pegase.scc; 018 019import java.io.IOException; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.LinkedHashMap; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.Set; 030import java.util.UUID; 031import java.util.function.Predicate; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035import org.apache.avalon.framework.configuration.Configuration; 036import org.apache.avalon.framework.configuration.ConfigurationException; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.commons.lang3.StringUtils; 040import org.slf4j.Logger; 041 042import org.ametys.cms.data.ContentSynchronizationResult; 043import org.ametys.cms.data.ContentValue; 044import org.ametys.cms.repository.Content; 045import org.ametys.cms.repository.ContentQueryHelper; 046import org.ametys.cms.repository.LanguageExpression; 047import org.ametys.cms.repository.ModifiableContent; 048import org.ametys.cms.repository.WorkflowAwareContent; 049import org.ametys.core.schedule.progression.ContainerProgressionTracker; 050import org.ametys.core.util.JSONUtils; 051import org.ametys.odf.ProgramItem; 052import org.ametys.odf.catalog.CatalogsManager; 053import org.ametys.odf.cdmfr.CDMFRHandler; 054import org.ametys.odf.course.Course; 055import org.ametys.odf.course.CourseFactory; 056import org.ametys.odf.courselist.CourseList; 057import org.ametys.odf.courselist.CourseListFactory; 058import org.ametys.odf.enumeration.OdfReferenceTableEntry; 059import org.ametys.odf.enumeration.OdfReferenceTableHelper; 060import org.ametys.odf.program.Container; 061import org.ametys.odf.program.ContainerFactory; 062import org.ametys.odf.program.ProgramFactory; 063import org.ametys.odf.program.ProgramPart; 064import org.ametys.odf.program.SubProgramFactory; 065import org.ametys.odf.program.TraversableProgramPart; 066import org.ametys.odf.workflow.AbstractCreateODFContentFunction; 067import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 068import org.ametys.plugins.odfsync.pegase.ws.PegaseApiManager; 069import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription; 070import org.ametys.plugins.repository.AmetysObjectIterable; 071import org.ametys.plugins.repository.jcr.NameHelper; 072import org.ametys.plugins.repository.query.expression.AndExpression; 073import org.ametys.plugins.repository.query.expression.Expression; 074import org.ametys.plugins.repository.query.expression.Expression.Operator; 075import org.ametys.plugins.repository.query.expression.StringExpression; 076import org.ametys.runtime.config.Config; 077import org.ametys.runtime.i18n.I18nizableText; 078import org.ametys.runtime.model.View; 079 080import com.opensymphony.workflow.WorkflowException; 081 082import fr.pcscol.pegase.odf.ApiException; 083import fr.pcscol.pegase.odf.api.MaquettesExterneApi; 084import fr.pcscol.pegase.odf.api.ObjetsMaquetteExterneApi; 085import fr.pcscol.pegase.odf.externe.model.DescripteursFormation; 086import fr.pcscol.pegase.odf.externe.model.DescripteursGroupement; 087import fr.pcscol.pegase.odf.externe.model.DescripteursObjetFormation; 088import fr.pcscol.pegase.odf.externe.model.DescripteursObjetFormationSyllabus; 089import fr.pcscol.pegase.odf.externe.model.DescripteursObjetMaquette; 090import fr.pcscol.pegase.odf.externe.model.DescripteursSise; 091import fr.pcscol.pegase.odf.externe.model.DescripteursSyllabus; 092import fr.pcscol.pegase.odf.externe.model.EnfantsStructure; 093import fr.pcscol.pegase.odf.externe.model.MaquetteStructure; 094import fr.pcscol.pegase.odf.externe.model.Nomenclature; 095import fr.pcscol.pegase.odf.externe.model.ObjetMaquetteDetail; 096import fr.pcscol.pegase.odf.externe.model.ObjetMaquetteStructure; 097import fr.pcscol.pegase.odf.externe.model.ObjetMaquetteSummary; 098import fr.pcscol.pegase.odf.externe.model.Pageable; 099import fr.pcscol.pegase.odf.externe.model.PagedObjetMaquetteSummaries; 100import fr.pcscol.pegase.odf.externe.model.PlageDeChoix; 101import fr.pcscol.pegase.odf.externe.model.TypeObjetMaquette; 102 103/** 104 * SCC for Pegase (COF). 105 */ 106public class PegaseSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 107{ 108 private static final String __INSERTED_GROUPEMENT_SUFFIX = "-LIST-"; 109 110 /** The JSON utils */ 111 protected JSONUtils _jsonUtils; 112 113 /** The catalogs manager */ 114 protected CatalogsManager _catalogsManager; 115 116 /** The Pégase API manager */ 117 protected PegaseApiManager _pegaseApiManager; 118 119 /** The CDM-fr handler */ 120 protected CDMFRHandler _cdmfrHandler; 121 122 /** The PégaseSCC helper */ 123 protected PegaseSCCMappingHelper _pegaseSccMappingHelper; 124 125 /** The reference table helper */ 126 protected OdfReferenceTableHelper _refTableHelper; 127 128 /** Mapping between metadata and columns */ 129 protected Map<String, Map<String, String>> _mappingByContentType; 130 131 /** Search fields to display */ 132 protected Set<String> _searchFields; 133 134 /** List of imported contents */ 135 protected Map<String, Integer> _importedContents; 136 137 /** List of synchronized contents having differences */ 138 protected Set<String> _synchronizedContents; 139 140 /** List of updated contents by relation */ 141 protected Set<String> _updatedRelationContents; 142 143 /** Map to link contents to its children at the end of the process */ 144 protected Map<String, Set<String>> _contentsChildren; 145 146 /** Default language configured for ODF */ 147 protected String _odfLang; 148 149 /** The structure code for Pégase */ 150 protected String _structureCode; 151 152 @Override 153 public void service(ServiceManager manager) throws ServiceException 154 { 155 super.service(manager); 156 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 157 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 158 _pegaseApiManager = (PegaseApiManager) manager.lookup(PegaseApiManager.ROLE); 159 _cdmfrHandler = (CDMFRHandler) manager.lookup(CDMFRHandler.ROLE); 160 _pegaseSccMappingHelper = (PegaseSCCMappingHelper) manager.lookup(PegaseSCCMappingHelper.ROLE); 161 _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 162 } 163 164 @Override 165 public String getIdField() 166 { 167 return "pegaseSyncCode"; 168 } 169 170 /** 171 * Get the identifier JSON field. 172 * @return the column id 173 */ 174 protected String getIdColumn() 175 { 176 return "id"; 177 } 178 179 @SuppressWarnings("unchecked") 180 @Override 181 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 182 { 183 return Optional.ofNullable(additionalParameters) 184 .map(params -> params.get("contentTypes")) 185 .filter(List.class::isInstance) 186 .map(cTypes -> (List<String>) cTypes) 187 .filter(Predicate.not(List::isEmpty)) 188 .map(l -> l.get(0)) 189 .map(_mappingByContentType::get) 190 .map(Map::keySet) 191 .orElse(Set.of()); 192 } 193 194 @Override 195 protected Map<String, Object> putIdParameter(String idValue) 196 { 197 Map<String, Object> parameters = new HashMap<>(); 198 parameters.put(getIdColumn(), List.of(idValue)); 199 return parameters; 200 } 201 202 @Override 203 protected void configureDataSource(Configuration configuration) throws ConfigurationException 204 { 205 _odfLang = Config.getInstance().getValue("odf.programs.lang"); 206 _searchFields = new HashSet<>(); 207 208 // Mapping by content type Map<String(Type), Map<String(Ametys field), String(Pégase field)>> 209 _mappingByContentType = new HashMap<>(); 210 211 if (Config.getInstance().getValue("pegase.activate", true, false)) 212 { 213 _structureCode = Config.getInstance().getValue("pegase.structure.code"); 214 215 // Champs affichés dans la recherche 216 _searchFields.add("libelle"); 217 _searchFields.add("code"); 218 _searchFields.add("espace"); 219 _searchFields.add("id"); 220 221 // Program 222 Map<String, String> contentTypeMapping = new HashMap<>(); 223 contentTypeMapping.put("title", "libelle"); 224 contentTypeMapping.put("pegaseCode", "code"); 225 contentTypeMapping.put("ects", "ects"); 226 contentTypeMapping.put("educationKind", "codeNatureDiplomeCode"); 227 contentTypeMapping.put("presentation", "description"); 228 contentTypeMapping.put("objectives", "objectif"); 229 contentTypeMapping.put("neededPrerequisite", "prerequisPedagogique"); 230 contentTypeMapping.put("mention", "codeMention"); 231 contentTypeMapping.put("domain", "codeDomaineFormation"); 232 contentTypeMapping.put("programField", "codeChampFormation"); 233 contentTypeMapping.put("educationLanguage", "langueEnseignement"); 234 contentTypeMapping.put("educationLevel", "codeNiveauFormation"); 235 contentTypeMapping.put("rncpLevel", "codeNiveauDiplome"); 236 contentTypeMapping.put("degree", "codeTypeDiplome"); 237 contentTypeMapping.put("orgUnit", "orgUnit"); 238 contentTypeMapping.put("additionalInformations", "autresInformations"); 239 _mappingByContentType.put(ProgramFactory.PROGRAM_CONTENT_TYPE, contentTypeMapping); 240 241 // SubProgram 242 contentTypeMapping = new HashMap<>(); 243 contentTypeMapping.put("title", "libelle"); 244 contentTypeMapping.put("pegaseCode", "code"); 245 contentTypeMapping.put("ects", "ects"); 246 contentTypeMapping.put("presentation", "description"); 247 contentTypeMapping.put("objectives", "objectif"); 248 contentTypeMapping.put("neededPrerequisite", "prerequisPedagogique"); 249 contentTypeMapping.put("orgUnit", "orgUnit"); 250 contentTypeMapping.put("numberOfStudents", "capaciteAccueil"); 251 contentTypeMapping.put("additionalInformations", "autresInformations"); 252 253 _mappingByContentType.put(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, contentTypeMapping); 254 255 // Container 256 contentTypeMapping = new HashMap<>(); 257 contentTypeMapping.put("title", "libelle"); 258 contentTypeMapping.put("pegaseCode", "code"); 259 contentTypeMapping.put("ects", "ects"); 260 contentTypeMapping.put("nature", "typeObjetFormation"); 261 contentTypeMapping.put("period", "typeObjetFormation"); 262 contentTypeMapping.put("description", "description"); 263 _mappingByContentType.put(ContainerFactory.CONTAINER_CONTENT_TYPE, contentTypeMapping); 264 265 // CourseList 266 contentTypeMapping = new HashMap<>(); 267 contentTypeMapping.put("title", "libelle"); 268 contentTypeMapping.put("pegaseCode", "code"); 269 contentTypeMapping.put("plageDeChoix", "plageDeChoix"); 270 contentTypeMapping.put("obligatoire", "obligatoire"); 271 contentTypeMapping.put("min", "plageMin"); 272 contentTypeMapping.put("max", "plageMax"); 273 _mappingByContentType.put(CourseListFactory.COURSE_LIST_CONTENT_TYPE, contentTypeMapping); 274 275 // Course 276 contentTypeMapping = new HashMap<>(); 277 contentTypeMapping.put("title", "libelle"); 278 contentTypeMapping.put("pegaseCode", "code"); 279 contentTypeMapping.put("ects", "ects"); 280 contentTypeMapping.put("description", "description"); 281 contentTypeMapping.put("courseType", "typeObjetFormation"); 282 contentTypeMapping.put("objectives", "objectif"); 283 contentTypeMapping.put("neededPrerequisite", "prerequisPedagogique"); 284 contentTypeMapping.put("bibliography", "bibliographie"); 285 contentTypeMapping.put("additionalInformations", "autresInformations"); 286 contentTypeMapping.put("openToExchangeStudents", "ouvertureALaMobiliteEntrante"); 287 contentTypeMapping.put("formOfAssessment", "modalitesEvaluation"); 288 contentTypeMapping.put("teachingLanguage", "langueEnseignement"); 289 290 _mappingByContentType.put(CourseFactory.COURSE_CONTENT_TYPE, contentTypeMapping); 291 } 292 } 293 294 @Override 295 protected void configureSearchModel() 296 { 297 List<String> sortableColumns = List.of("code", "libelle"); 298 299 _searchModelConfiguration.addCriterion("libelle", new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_PEGASE_CRITERION_LIBELLE"), "string"); 300 _searchModelConfiguration.addCriterion("validee", new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_PEGASE_CRITERION_VALIDEE"), "boolean", "edition.boolean-combobox"); 301 302 for (String keptField : _searchFields) 303 { 304 if (sortableColumns.contains(keptField)) 305 { 306 _searchModelConfiguration.addColumn(keptField, new I18nizableText(keptField), true); 307 } 308 else 309 { 310 _searchModelConfiguration.addColumn(keptField, new I18nizableText(keptField), false); 311 } 312 } 313 } 314 315 /** 316 * Get the catalog for import. 317 * @return the catalog 318 */ 319 protected String getCatalog() 320 { 321 return Optional.of(getParameterValues()) 322 .map(params -> params.get("catalog")) 323 .map(String.class::cast) 324 .filter(StringUtils::isNotBlank) 325 .orElseGet(() -> _catalogsManager.getDefaultCatalogName()); 326 } 327 328 @Override 329 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 330 { 331 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 332 333 try 334 { 335 ObjetsMaquetteExterneApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteExterneApi(); 336 337 // Get details for all requested programs by objetsMaquetteApi 338 PagedObjetMaquetteSummaries pagedPrograms = _getPagedProgramsSummaries(parameters, offset, limit, sort, objetsMaquetteApi); 339 Long total = pagedPrograms.getTotalElements(); 340 341 if (total != null) 342 { 343 // if total is provided, store it so that getTotalCount won't do a useless full search 344 parameters.put("totalCount", total.longValue()); 345 } 346 347 List<ObjetMaquetteSummary> programs = pagedPrograms.getItems(); 348 if (programs != null) 349 { 350 for (ObjetMaquetteSummary programSummary : programs) 351 { 352 String pegaseSyncCode = programSummary.getId().toString(); 353 354 // If not an import, fill the results with Pégase synchronization code 355 Map<String, Object> result = results.computeIfAbsent(pegaseSyncCode, __ -> new HashMap<>()); 356 Map<String, String> fields = _getSummaryFields(programSummary); 357 358 for (String keptField : _searchFields) 359 { 360 String field = fields.get(keptField); 361 result.put(keptField, field); 362 } 363 364 result.put(SCC_UNIQUE_ID, pegaseSyncCode); 365 } 366 } 367 } 368 catch (ApiException | IOException e) 369 { 370 throw new RuntimeException("Error while getting remote values", e); 371 } 372 373 return results; 374 } 375 376 @Override 377 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 378 { 379 // avoid to relaunch a full search if already done 380 Long totalCount = (Long) searchParameters.get("totalCount"); 381 382 if (totalCount != null) 383 { 384 return totalCount.intValue(); 385 } 386 387 return super.getTotalCount(searchParameters, logger); 388 } 389 390 private PagedObjetMaquetteSummaries _getPagedProgramsSummaries(Map<String, Object> parameters, int offset, int limit, List<Object> sort, ObjetsMaquetteExterneApi objetsMaquetteApi) throws ApiException 391 { 392 Pageable pageable = new Pageable(); 393 int pageNumber = offset / 50; 394 pageable.setPage(pageNumber); 395 pageable.setTaille(limit); 396 397 if (sort == null) 398 { 399 pageable.setTri(List.of()); 400 } 401 else 402 { 403 // Convert the sort parameter from Object to JSON 404 String jsonSortParameters = _jsonUtils.convertObjectToJson(sort.get(0)); 405 // Convert the sort parameter from JSON to Map<String, Object> 406 Map<String, Object> sortParameters = _jsonUtils.convertJsonToMap(jsonSortParameters); 407 // Create the list containing the sort result; it is going to be of the form : ["field,direction"] 408 List<String> sortParametersArray = new ArrayList<>(); 409 // Create the sort parameter that is going to be sent in the list; it is going to be of the form : "field,direction" 410 StringBuilder stringBuilder = new StringBuilder(); 411 // Get the parameter "property" which is the column on which the sorting is meant to be made 412 String property = (String) sortParameters.get("property"); 413 414 if (!"code".equals(property) && !"libelle".equals(property)) 415 { 416 if ("libelle".equals(property)) 417 { 418 stringBuilder.append("libelle,").append((String) sortParameters.get("direction")); 419 sortParametersArray.add(stringBuilder.toString()); 420 } 421 } 422 else 423 { 424 stringBuilder.append(property).append(",").append((String) sortParameters.get("direction")); 425 sortParametersArray.add(stringBuilder.toString()); 426 } 427 428 pageable.setTri(sortParametersArray); 429 } 430 431 String searchLabel = (String) parameters.get("libelle"); 432 List<TypeObjetMaquette> typeObjets = List.of(TypeObjetMaquette.FORMATION); 433 Boolean validated = (Boolean) parameters.get("validee"); 434 return objetsMaquetteApi.rechercherObjetMaquette(_structureCode, pageable, searchLabel, null, typeObjets, null, null, null, null, null, validated, null); 435 } 436 437 private Map<String, String> _getSummaryFields(ObjetMaquetteSummary objectSummary) 438 { 439 return Map.of("id", objectSummary.getId().toString(), 440 "code", StringUtils.defaultString(objectSummary.getCode()), 441 "libelle", StringUtils.defaultString(objectSummary.getLibelle()), 442 "espace", StringUtils.defaultString(objectSummary.getEspaceLibelle())); 443 } 444 445 @SuppressWarnings("unchecked") 446 @Override 447 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 448 { 449 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 450 451 List<String> pegaseSyncCodeValues = (List<String>) parameters.getOrDefault(getIdColumn(), new ArrayList<>()); 452 453 List<String> idValues = new ArrayList<>(); 454 455 try 456 { 457 ObjetsMaquetteExterneApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteExterneApi(); 458 MaquettesExterneApi maquettesApi = _pegaseApiManager.getMaquettesExterneApi(); 459 460 if (!pegaseSyncCodeValues.isEmpty()) 461 { 462 // import or synchronize single items 463 idValues.addAll(pegaseSyncCodeValues); 464 } 465 else 466 { 467 // global synchronization 468 PagedObjetMaquetteSummaries pagedPrograms = _getPagedProgramsSummaries(parameters, 0, Integer.MAX_VALUE, null, objetsMaquetteApi); 469 for (ObjetMaquetteSummary programDetails : pagedPrograms.getItems()) 470 { 471 idValues.add(programDetails.getId().toString()); 472 } 473 } 474 475 results.putAll(_getObjectDetailsForImport(idValues, objetsMaquetteApi, maquettesApi, logger)); 476 } 477 catch (ApiException | IOException e) 478 { 479 throw new RuntimeException("Error while getting remote values", e); 480 } 481 482 return results; 483 } 484 485 Map<String, Map<String, List<Object>>> _getObjectDetailsForImport(List<String> idValues, ObjetsMaquetteExterneApi objetsMaquetteApi, MaquettesExterneApi maquettesApi, Logger logger) throws ApiException 486 { 487 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 488 Set<UUID> alreadyHandledObjects = new HashSet<>(); 489 490 for (String idValue : idValues) 491 { 492 // Get the program tree structure from the idValue 493 MaquetteStructure maquette = maquettesApi.lireStructureMaquette(_structureCode, UUID.fromString(idValue)); 494 ObjetMaquetteStructure racine = maquette.getRacine(); 495 496 results.putAll(_getObjectDetailsForImport(racine, null, alreadyHandledObjects, objetsMaquetteApi, logger)); 497 } 498 499 return results; 500 } 501 502 private Map<String, Map<String, List<Object>>> _getObjectDetailsForImport(ObjetMaquetteStructure item, EnfantsStructure structure, Set<UUID> alreadyHandledObjects, ObjetsMaquetteExterneApi objetsMaquetteApi, Logger logger) throws ApiException 503 { 504 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 505 506 UUID id = item.getId(); 507 508 if (alreadyHandledObjects.add(id)) 509 { 510 ObjetMaquetteDetail detail = objetsMaquetteApi.lireObjetMaquette(_structureCode, id); 511 512 // get data for this particular Pegase object 513 Map<String, List<Object>> objectData = _getObjectFields(detail, structure, item); 514 515 // then compute Ametys children from the Pegase structure 516 ComputedChildren children = _computeChildren(item); 517 518 // link to Ametys children 519 objectData.put("children", children.childrenIds()); 520 521 results.put(id.toString(), objectData); 522 523 // add the newly created intermediary objects, if any 524 results.putAll(children.newObjects()); 525 526 // then finally import recursively next objects 527 for (EnfantsStructure child : children.nextObjects()) 528 { 529 results.putAll(_getObjectDetailsForImport(child.getObjetMaquette(), child, alreadyHandledObjects, objetsMaquetteApi, logger)); 530 } 531 } 532 533 return results; 534 } 535 536 private ComputedChildren _computeChildren(ObjetMaquetteStructure item) 537 { 538 List<EnfantsStructure> enfants = item.getEnfants(); 539 540 List<Object> childrenIds = new ArrayList<>(); 541 List<EnfantsStructure> nextObjects = new ArrayList<>(); 542 Map<String, Map<String, List<Object>>> newObjects = new HashMap<>(); 543 544 if (_isProgramPart(item)) 545 { 546 // item is a programpart, we allow other programparts as direct children 547 List<EnfantsStructure> programPartChildren = enfants.stream().filter(e -> _isProgramPart(e.getObjetMaquette())).toList(); 548 childrenIds.addAll(programPartChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 549 nextObjects.addAll(programPartChildren); 550 551 // if there are direct courses, we should inject an intermediary list 552 _computeIntermediateLists(item, enfants, childrenIds, nextObjects, newObjects); 553 554 // add groups 555 List<EnfantsStructure> listChildren = enfants.stream().filter(e -> _isCourseList(e.getObjetMaquette())).toList(); 556 childrenIds.addAll(listChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 557 nextObjects.addAll(listChildren); 558 } 559 else if (_isCourseList(item)) 560 { 561 List<EnfantsStructure> courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette())).toList(); 562 childrenIds.addAll(courseChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 563 nextObjects.addAll(courseChildren); 564 } 565 else if (_isCourse(item)) 566 { 567 // if there are direct courses, we should inject an intermediary list 568 _computeIntermediateLists(item, enfants, childrenIds, nextObjects, newObjects); 569 570 // add groups 571 List<EnfantsStructure> listChildren = enfants.stream().filter(e -> _isCourseList(e.getObjetMaquette())).toList(); 572 childrenIds.addAll(listChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 573 nextObjects.addAll(listChildren); 574 } 575 576 return new ComputedChildren(childrenIds, newObjects, nextObjects); 577 } 578 579 private String _getContentType(ObjetMaquetteStructure item) 580 { 581 String type = item.getType(); 582 if (item.getClasse().equals("F")) 583 { 584 type = "FORMATION"; 585 } 586 else if (item.getClasse().equals("G")) 587 { 588 type = "GROUPEMENT"; 589 } 590 591 return Optional.ofNullable(type) 592 .map(_pegaseSccMappingHelper::getAmetysType) 593 .orElse(CourseFactory.COURSE_CONTENT_TYPE); 594 } 595 596 private boolean _isProgramPart(ObjetMaquetteStructure item) 597 { 598 String contentType = _getContentType(item); 599 600 if (contentType.equals(ProgramFactory.PROGRAM_CONTENT_TYPE) || contentType.equals(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE) || contentType.equals(ContainerFactory.CONTAINER_CONTENT_TYPE)) 601 { 602 return true; 603 } 604 605 return false; 606 } 607 608 private boolean _isCourse(ObjetMaquetteStructure item) 609 { 610 return _getContentType(item).equals(CourseFactory.COURSE_CONTENT_TYPE); 611 } 612 613 private boolean _isCourseList(ObjetMaquetteStructure item) 614 { 615 return _getContentType(item).equals(CourseListFactory.COURSE_LIST_CONTENT_TYPE); 616 } 617 618 private void _computeIntermediateLists(ObjetMaquetteStructure item, List<EnfantsStructure> enfants, List<Object> childrenIds, List<EnfantsStructure> nextObjects, Map<String, Map<String, List<Object>>> newObjects) 619 { 620 List<EnfantsStructure> courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette()) && e.getObligatoire()).toList(); 621 _computeIntermediateList(item, courseChildren, true, childrenIds, nextObjects, newObjects); 622 623 courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette()) && !e.getObligatoire()).toList(); 624 _computeIntermediateList(item, courseChildren, false, childrenIds, nextObjects, newObjects); 625 } 626 627 private void _computeIntermediateList(ObjetMaquetteStructure item, List<EnfantsStructure> courseChildren, boolean mandatory, List<Object> childrenIds, List<EnfantsStructure> nextObjects, Map<String, Map<String, List<Object>>> newObjects) 628 { 629 if (!courseChildren.isEmpty()) 630 { 631 String listId = item.getId() + __INSERTED_GROUPEMENT_SUFFIX + (mandatory ? "O" : "F"); 632 childrenIds.add(listId); 633 634 Map<String, List<Object>> listData = new HashMap<>(); 635 listData.put("workflowDescription", List.of(ContentWorkflowDescription.COURSELIST_WF_DESCRIPTION)); 636 listData.put(getIdField(), List.of(listId)); 637 listData.put("title", List.of(item.getLibelle() + " - Liste " + (mandatory ? "O" : "F"))); 638 listData.put("obligatoire", List.of(String.valueOf(mandatory))); 639 listData.put("plageDeChoix", List.of("false")); 640 listData.put("children", courseChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).collect(Collectors.toList())); 641 642 newObjects.put(listId, listData); 643 nextObjects.addAll(courseChildren); 644 } 645 } 646 647 private Map<String, List<Object>> _getObjectFields(ObjetMaquetteDetail objectDetails, EnfantsStructure structure, ObjetMaquetteStructure item) 648 { 649 Map<String, List<Object>> result = new HashMap<>(); 650 651 String contentTypeId = _getContentType(item); 652 ContentWorkflowDescription wfDescription = ContentWorkflowDescription.getByContentType(contentTypeId); 653 654 result.put("workflowDescription", List.of(wfDescription)); 655 656 Map<String, String> contentTypeMapping = _mappingByContentType.get(contentTypeId); 657 658 Map<String, Object> fields = _getFields(objectDetails, structure); 659 660 for (String attribute : contentTypeMapping.keySet()) 661 { 662 String jsonKey = contentTypeMapping.get(attribute); 663 Object value = fields.get(jsonKey); 664 665 result.put(attribute, value != null ? List.of(value) : null); // Add the retrieved metadata values list to the contentResult 666 } 667 668 // Add the ID field 669 result.put(getIdField(), List.of(objectDetails.getId().toString())); 670 671 return result; 672 } 673 674 private Map<String, Object> _getFields(ObjetMaquetteDetail objectDetails, EnfantsStructure structure) 675 { 676 Map<String, Object> fields = new HashMap<>(); 677 678 fields.put("id", objectDetails.getId().toString()); 679 fields.put("code", StringUtils.trimToNull(objectDetails.getCode())); 680 681 DescripteursObjetMaquette descripteursObjetMaquette = objectDetails.getDescripteursObjetMaquette(); 682 String libelle = StringUtils.trimToNull(descripteursObjetMaquette.getLibelle()); 683 fields.put("libelle", libelle); 684 fields.put("libelleLong", StringUtils.trimToNull(descripteursObjetMaquette.getLibelleLong())); 685 686 if (descripteursObjetMaquette instanceof DescripteursFormation descripteursFormation) 687 { 688 fields.put("ects", descripteursFormation.getEcts()); 689 fields.put("orgUnit", StringUtils.trimToNull(descripteursFormation.getStructurePrincipale())); 690 } 691 else if (descripteursObjetMaquette instanceof DescripteursGroupement descripteursGroupement) 692 { 693 fields.put("obligatoire", String.valueOf(structure.getObligatoire())); 694 695 PlageDeChoix plageDeChoix = descripteursGroupement.getPlageDeChoix(); 696 if (plageDeChoix != null) 697 { 698 fields.put("plageDeChoix", "true"); 699 fields.put("plageMin", descripteursGroupement.getPlageDeChoix().getMin()); 700 fields.put("plageMax", descripteursGroupement.getPlageDeChoix().getMax()); 701 } 702 } 703 else if (descripteursObjetMaquette instanceof DescripteursObjetFormation descripteursObjetFormation) 704 { 705 fields.put("ects", descripteursObjetFormation.getEcts()); 706 fields.put("orgUnit", StringUtils.trimToNull(descripteursObjetFormation.getStructurePrincipale())); 707 fields.put("capaciteAccueil", descripteursObjetFormation.getCapaciteAccueil()); 708 709 String type = _getNomenclature(descripteursObjetFormation.getType()); 710 fields.put("typeObjetFormation", type); 711 } 712 713 DescripteursSyllabus syllabus = objectDetails.getDescripteursSyllabus(); 714 if (syllabus != null) 715 { 716 fields.put("langueEnseignement", StringUtils.trimToNull(syllabus.getLangueEnseignement())); 717 fields.put("description", StringUtils.trimToNull(syllabus.getDescription())); 718 fields.put("bibliographie", StringUtils.trimToNull(syllabus.getBibliographie())); 719 fields.put("autresInformations", StringUtils.trimToNull(syllabus.getAutresInformations())); 720 fields.put("ouvertureALaMobiliteEntrante", syllabus.getOuvertureALaMobiliteEntrante()); 721 fields.put("objectif", StringUtils.trimToNull(syllabus.getObjectif())); 722 fields.put("prerequisPedagogique", StringUtils.trimToNull(syllabus.getPrerequisPedagogique())); 723 724 if (syllabus instanceof DescripteursObjetFormationSyllabus objetFormationSyllabus) 725 { 726 fields.put("modalitesEvaluation", objetFormationSyllabus.getModalitesEvaluation()); 727 } 728 } 729 730 DescripteursSise sise = objectDetails.getDescripteursEnquete().getDescripteursSise(); 731 if (sise != null) 732 { 733 fields.put("codeTypeDiplome", _getNomenclature(sise.getTypeDiplome())); 734 fields.put("codeMention", _getNomenclature(sise.getMention())); 735 fields.put("codeNiveauDiplome", _getNomenclature(sise.getNiveauDiplome())); 736 fields.put("codeChampFormation", _getNomenclature(sise.getChampFormation())); 737 fields.put("codeDomaineFormation", _getNomenclature(sise.getDomaineFormation())); 738 } 739 740 return fields; 741 } 742 743 private String _getNomenclature(Nomenclature nomenclature) 744 { 745 return nomenclature != null ? StringUtils.trimToNull(nomenclature.getCode()) : null; 746 } 747 748 @Override 749 public List<ModifiableContent> importContent(String idValue, Map<String, Object> importParams, Logger logger) throws Exception 750 { 751 Map<String, Object> parameters = putIdParameter(idValue); 752 return _importOrSynchronizeContents(parameters, true, logger); 753 } 754 755 @Override 756 public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception 757 { 758 String idValue = content.getValue(getIdField()); 759 Map<String, Object> parameters = putIdParameter(idValue); 760 _importOrSynchronizeContents(parameters, true, logger); 761 } 762 763 @Override 764 protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParams, boolean forceImport, Logger logger, ContainerProgressionTracker progressionTracker) 765 { 766 _importedContents = new HashMap<>(); 767 _synchronizedContents = new HashSet<>(); 768 _updatedRelationContents = new HashSet<>(); 769 _contentsChildren = new HashMap<>(); 770 771 try 772 { 773 _cdmfrHandler.suspendCDMFRObserver(); 774 775 List<ModifiableContent> contents = super._importOrSynchronizeContents(searchParams, forceImport, logger, progressionTracker); 776 _updateRelations(logger); 777 _updateWorkflowStatus(logger); 778 return contents; 779 } 780 finally 781 { 782 _cdmfrHandler.unsuspendCDMFRObserver(_synchronizedContents); 783 784 _importedContents = null; 785 _synchronizedContents = null; 786 _updatedRelationContents = null; 787 _contentsChildren = null; 788 } 789 } 790 791 /** 792 * Get or create the content defined by the given parameters. 793 * @param lang The content language 794 * @param idValue The synchronization code 795 * @param remoteValues The remote values 796 * @param forceImport <code>true</code> to force import (only on single import or unlimited global synchronization) 797 * @param logger The logger 798 * @return the content 799 * @throws Exception if an error occurs 800 */ 801 protected ModifiableContent _getOrCreateContent(String lang, String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) throws Exception 802 { 803 ContentWorkflowDescription wfDescription = (ContentWorkflowDescription) remoteValues.get("workflowDescription").get(0); 804 ModifiableContent content = _getContent(lang, idValue, wfDescription.getContentType()); 805 if (content == null && (forceImport || !synchronizeExistingContentsOnly())) 806 { 807 // Calculate contentTitle 808 String contentTitle = Optional.of(remoteValues) 809 .map(v -> v.get("title")) 810 .map(List::stream) 811 .orElseGet(Stream::empty) 812 .filter(String.class::isInstance) 813 .map(String.class::cast) 814 .filter(StringUtils::isNotEmpty) 815 .findFirst() 816 .orElse(idValue); 817 818 String contentName = NameHelper.filterName(_contentPrefix + "-" + contentTitle + "-" + lang); 819 820 Map<String, Object> inputs = new HashMap<>(); 821 String catalog = getCatalog(); 822 if (catalog != null) 823 { 824 inputs.put(AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY, catalog); 825 } 826 827 Map<String, Object> resultMap = _contentWorkflowHelper.createContent( 828 wfDescription.getWorkflowName(), 829 wfDescription.getInitialActionId(), 830 contentName, 831 contentTitle, 832 new String[] {wfDescription.getContentType()}, 833 null, 834 lang, 835 inputs); 836 837 if ((boolean) resultMap.getOrDefault("error", false)) 838 { 839 _nbError++; 840 } 841 842 content = (ModifiableContent) resultMap.get(Content.class.getName()); 843 844 if (content != null) 845 { 846 _sccHelper.updateSCCProperty(content, getId()); 847 848 // Set sync code 849 content.setValue(getIdField(), idValue); 850 851 content.saveChanges(); 852 _importedContents.put(content.getId(), wfDescription.getValidationActionId()); 853 _nbCreatedContents++; 854 } 855 } 856 return content; 857 } 858 859 /** 860 * Get the content from the synchronization code, the lang, the catalog and the content type. 861 * @param lang The lang 862 * @param syncCode The synchronization code 863 * @param contentType The content type 864 * @return the retrieved content 865 */ 866 protected ModifiableContent _getContent(String lang, String syncCode, String contentType) 867 { 868 String xPathQuery = _getContentPathQuery(lang, syncCode, contentType, false); 869 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery); 870 871 if (contents.getSize() > 0) 872 { 873 return contents.iterator().next(); 874 } 875 876 return null; 877 } 878 879 @Override 880 protected Optional<ModifiableContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 881 { 882 try 883 { 884 ModifiableContent content = _getOrCreateContent(lang, idValue, remoteValues, forceImport, logger); 885 if (content != null) 886 { 887 return Optional.of(_synchronizeContent(content, remoteValues, logger)); 888 } 889 } 890 catch (Exception e) 891 { 892 _nbError++; 893 logger.error("An error occurred while importing or synchronizing content", e); 894 } 895 896 return Optional.empty(); 897 } 898 899 @SuppressWarnings("unchecked") 900 @Override 901 protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 902 { 903 super._synchronizeContent(content, remoteValues, logger); 904 // Add children to the list to handle later to add relations 905 if (remoteValues.containsKey("children")) 906 { 907 Set<String> children = _contentsChildren.computeIfAbsent(content.getId(), __ -> new LinkedHashSet<>()); 908 children.addAll((List<String>) (Object) remoteValues.get("children")); 909 } 910 return content; 911 } 912 913 @Override 914 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableContent content, Map<String, Object> additionalParameters, boolean create, Logger logger) throws Exception 915 { 916 _synchronizedContents.add(content.getId()); 917 return super._fillContent(remoteValues, content, additionalParameters, create, logger); 918 } 919 920 @Override 921 public List<String> getLanguages() 922 { 923 return List.of(_odfLang); 924 } 925 926 @Override 927 protected List<Expression> _getExpressionsList(String lang, String idValue, String contentType, boolean forceStrictCheck) 928 { 929 List<Expression> expList = super._getExpressionsList(lang, idValue, contentType, forceStrictCheck); 930 String catalog = getCatalog(); 931 if (catalog != null) 932 { 933 expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 934 } 935 return expList; 936 } 937 938 @Override 939 protected Map<String, Object> _transformRemoteValuesCardinality(Map<String, List<Object>> remoteValues, String obsoleteContentTypeId) 940 { 941 String realContentTypeId = Optional.of(remoteValues) 942 .map(v -> v.get("workflowDescription")) 943 .map(l -> l.get(0)) 944 .map(ContentWorkflowDescription.class::cast) 945 .map(ContentWorkflowDescription::getContentType) 946 .orElse(null); 947 return super._transformRemoteValuesCardinality(remoteValues, realContentTypeId); 948 } 949 950 private void _updateRelations(Logger logger) 951 { 952 for (String contentId : _contentsChildren.keySet()) 953 { 954 WorkflowAwareContent content = _resolver.resolveById(contentId); 955 Set<String> childrenCodes = _contentsChildren.get(contentId); 956 String contentLanguage = content.getLanguage(); 957 String contentCatalog = content.getValue("catalog"); 958 Map<String, Set<String>> childrenByAttributeName = new HashMap<>(); 959 960 for (String childCode : childrenCodes) 961 { 962 Expression expression = new AndExpression( 963 _sccHelper.getCollectionExpression(getId()), 964 new StringExpression(getIdField(), Operator.EQ, childCode), 965 new StringExpression("catalog", Operator.EQ, contentCatalog), 966 new LanguageExpression(Operator.EQ, contentLanguage) 967 ); 968 969 ModifiableContent childContent = _resolver.<ModifiableContent>query(ContentQueryHelper.getContentXPathQuery(expression)) 970 .stream() 971 .findFirst() 972 .orElse(null); 973 974 if (childContent == null) 975 { 976 logger.warn("Content with code '{}' in {} on catalog '{}' was not found in the repository to update relations with content [{}] '{}' ({}).", childCode, contentLanguage, contentCatalog, content.getValue(getIdField()), content.getTitle(), contentCatalog); 977 } 978 else 979 { 980 String attributesName = _getChildAttributeName(content, childContent); 981 if (attributesName != null) 982 { 983 Set<String> children = childrenByAttributeName.computeIfAbsent(attributesName, __ -> new LinkedHashSet<>()); 984 children.add(childContent.getId()); 985 } 986 else 987 { 988 logger.warn("The child content [{}] '{}' of type '{}' is not compatible with parent content [{}] '{}' of type '{}'.", childCode, childContent.getTitle(), childContent.getTypes()[0], content.getValue(getIdField()), content.getTitle(), content.getTypes()[0]); 989 } 990 } 991 } 992 _updateRelations(content, childrenByAttributeName, logger); 993 } 994 } 995 996 private String _getChildAttributeName(Content parentContent, Content childContent) 997 { 998 if (childContent instanceof Course && parentContent instanceof CourseList) 999 { 1000 return CourseList.CHILD_COURSES; 1001 } 1002 1003 if (parentContent instanceof Course && childContent instanceof CourseList) 1004 { 1005 return Course.CHILD_COURSE_LISTS; 1006 } 1007 1008 if (parentContent instanceof TraversableProgramPart && childContent instanceof ProgramPart) 1009 { 1010 return TraversableProgramPart.CHILD_PROGRAM_PARTS; 1011 } 1012 1013 return null; 1014 } 1015 1016 private void _updateRelations(WorkflowAwareContent content, Map<String, Set<String>> contentRelationsByAttribute, Logger logger) 1017 { 1018 // Compute the view 1019 View view = View.of(content.getModel(), contentRelationsByAttribute.keySet().toArray(new String[contentRelationsByAttribute.size()])); 1020 1021 // Compute values 1022 Map<String, Object> values = new HashMap<>(); 1023 for (String attributeName : contentRelationsByAttribute.keySet()) 1024 { 1025 // Add the content relations to the existing ones 1026 List<String> attributeValue = _getContentAttributeValue(content, attributeName); 1027 contentRelationsByAttribute.get(attributeName) 1028 .stream() 1029 .filter(id -> !attributeValue.contains(id)) 1030 .forEach(attributeValue::add); 1031 1032 values.put(attributeName, attributeValue.toArray(new String[attributeValue.size()])); 1033 } 1034 1035 // Compute not synchronized contents 1036 Set<String> notSynchronizedContentIds = contentRelationsByAttribute.values() 1037 .stream() 1038 .flatMap(Collection::stream) 1039 .filter(id -> !_synchronizedContents.contains(id)) 1040 .collect(Collectors.toSet()); 1041 1042 try 1043 { 1044 _editContent(content, Optional.of(view), values, Map.of(), false, notSynchronizedContentIds, logger); 1045 } 1046 catch (WorkflowException e) 1047 { 1048 _nbError++; 1049 logger.error("The content '{}' cannot be links edited (workflow action)", content, e); 1050 } 1051 } 1052 1053 private List<String> _getContentAttributeValue(Content content, String attributeName) 1054 { 1055 return Optional.of(attributeName) 1056 .map(content::<ContentValue[]>getValue) 1057 .map(Stream::of) 1058 .orElseGet(Stream::empty) 1059 .map(ContentValue::getContentId) 1060 .collect(Collectors.toList()); 1061 } 1062 1063 private void _updateWorkflowStatus(Logger logger) 1064 { 1065 // Validate contents -> only on newly imported contents 1066 if (validateAfterImport()) 1067 { 1068 for (String contentId : _importedContents.keySet()) 1069 { 1070 WorkflowAwareContent content = _resolver.resolveById(contentId); 1071 Integer validationActionId = _importedContents.get(contentId); 1072 if (validationActionId > 0) 1073 { 1074 validateContent(content, validationActionId, logger); 1075 } 1076 } 1077 } 1078 } 1079 1080 @Override 1081 public boolean handleRightAssignmentContext() 1082 { 1083 // Rights on ODF contents are handled by ODFRightAssignmentContext 1084 return false; 1085 } 1086 1087 @Override 1088 public ContentSynchronizationResult additionalCommonOperations(ModifiableContent content, Map<String, Object> additionalParameters, Logger logger) 1089 { 1090 _setPeriod(content); 1091 return super.additionalCommonOperations(content, additionalParameters, logger); 1092 } 1093 1094 private void _setPeriod(ModifiableContent content) 1095 { 1096 if (content instanceof Container container && "semestre".equals(_resolver.<Content>resolveById(container.getNature()).getValue("code"))) 1097 { 1098 String period = _getPeriodFromTitle(content); 1099 if (period != null) 1100 { 1101 OdfReferenceTableEntry entry = _refTableHelper.getItemFromCode(OdfReferenceTableHelper.PERIOD, period); 1102 if (entry != null) 1103 { 1104 content.setExternalValue(Container.PERIOD, entry.getId()); 1105 } 1106 } 1107 } 1108 } 1109 1110 private String _getPeriodFromTitle(Content content) 1111 { 1112 return switch (content.getTitle()) 1113 { 1114 case String title when StringUtils.containsIgnoreCase(title, "Semestre 1") || StringUtils.containsIgnoreCase(title, "S1") -> "s1"; 1115 case String title when StringUtils.containsIgnoreCase(title, "Semestre 2") || StringUtils.containsIgnoreCase(title, "S2") -> "s2"; 1116 case String title when StringUtils.containsIgnoreCase(title, "Semestre 3") || StringUtils.containsIgnoreCase(title, "S3") -> "s3"; 1117 case String title when StringUtils.containsIgnoreCase(title, "Semestre 4") || StringUtils.containsIgnoreCase(title, "S4") -> "s4"; 1118 case String title when StringUtils.containsIgnoreCase(title, "Semestre 5") || StringUtils.containsIgnoreCase(title, "S5") -> "s5"; 1119 case String title when StringUtils.containsIgnoreCase(title, "Semestre 6") || StringUtils.containsIgnoreCase(title, "S6") -> "s6"; 1120 default -> null; 1121 }; 1122 } 1123 1124 private record ComputedChildren(List<Object> childrenIds, Map<String, Map<String, List<Object>>> newObjects, List<EnfantsStructure> nextObjects) { /* empty*/ } 1125}