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