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