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