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