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.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.LinkedHashMap; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Set; 033import java.util.UUID; 034import java.util.function.Predicate; 035import java.util.stream.Collectors; 036import java.util.stream.Stream; 037 038import org.apache.avalon.framework.configuration.Configuration; 039import org.apache.avalon.framework.configuration.ConfigurationException; 040import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 041import org.apache.avalon.framework.context.Context; 042import org.apache.avalon.framework.context.ContextException; 043import org.apache.avalon.framework.context.Contextualizable; 044import org.apache.avalon.framework.service.ServiceException; 045import org.apache.avalon.framework.service.ServiceManager; 046import org.apache.cocoon.Constants; 047import org.apache.commons.lang3.StringUtils; 048import org.apache.commons.lang3.Strings; 049import org.slf4j.Logger; 050 051import org.ametys.cms.data.ContentSynchronizationResult; 052import org.ametys.cms.data.ContentValue; 053import org.ametys.cms.repository.Content; 054import org.ametys.cms.repository.ContentQueryHelper; 055import org.ametys.cms.repository.LanguageExpression; 056import org.ametys.cms.repository.ModifiableContent; 057import org.ametys.cms.repository.WorkflowAwareContent; 058import org.ametys.core.schedule.progression.ContainerProgressionTracker; 059import org.ametys.core.util.JSONUtils; 060import org.ametys.odf.ProgramItem; 061import org.ametys.odf.catalog.CatalogsManager; 062import org.ametys.odf.cdmfr.CDMFRHandler; 063import org.ametys.odf.course.Course; 064import org.ametys.odf.course.CourseFactory; 065import org.ametys.odf.courselist.CourseList; 066import org.ametys.odf.courselist.CourseListFactory; 067import org.ametys.odf.enumeration.OdfReferenceTableEntry; 068import org.ametys.odf.enumeration.OdfReferenceTableHelper; 069import org.ametys.odf.program.Container; 070import org.ametys.odf.program.ContainerFactory; 071import org.ametys.odf.program.ProgramFactory; 072import org.ametys.odf.program.ProgramPart; 073import org.ametys.odf.program.SubProgramFactory; 074import org.ametys.odf.program.TraversableProgramPart; 075import org.ametys.odf.workflow.AbstractCreateODFContentFunction; 076import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 077import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionMappingHelper; 078import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionMappingHelper.MappingEntry; 079import org.ametys.plugins.odfsync.pegase.ws.PegaseApiManager; 080import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription; 081import org.ametys.plugins.repository.AmetysObjectIterable; 082import org.ametys.plugins.repository.jcr.NameHelper; 083import org.ametys.plugins.repository.query.expression.AndExpression; 084import org.ametys.plugins.repository.query.expression.Expression; 085import org.ametys.plugins.repository.query.expression.Expression.Operator; 086import org.ametys.plugins.repository.query.expression.StringExpression; 087import org.ametys.runtime.config.Config; 088import org.ametys.runtime.i18n.I18nizableText; 089import org.ametys.runtime.model.View; 090 091import com.opensymphony.workflow.WorkflowException; 092 093import fr.pcscol.pegase.odf.ApiException; 094import fr.pcscol.pegase.odf.api.MaquettesExterneApi; 095import fr.pcscol.pegase.odf.api.ObjetsMaquetteExterneApi; 096import fr.pcscol.pegase.odf.externe.model.EnfantsStructure; 097import fr.pcscol.pegase.odf.externe.model.MaquetteStructure; 098import fr.pcscol.pegase.odf.externe.model.ObjetMaquetteStructure; 099import fr.pcscol.pegase.odf.externe.model.ObjetMaquetteSummary; 100import fr.pcscol.pegase.odf.externe.model.Pageable; 101import fr.pcscol.pegase.odf.externe.model.PagedObjetMaquetteSummaries; 102import fr.pcscol.pegase.odf.externe.model.TypeObjetMaquette; 103 104/** 105 * SCC for Pegase (COF). 106 */ 107public class PegaseSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable 108{ 109 /** Name of parameter holding the fields mapping */ 110 protected static final String __PARAM_MAPPING = "mapping"; 111 /** Name of parameter holding the field ID column */ 112 protected static final String __PARAM_ID_COLUMN = "idColumn"; 113 /** Name of paramter holding columns */ 114 protected static final String __PARAM_COLUMNS = "columns"; 115 /** Name of paramter into columns holding column */ 116 protected static final String __PARAM_COLUMNS_COLUMN = "column"; 117 118 private static final String __INSERTED_GROUPEMENT_SUFFIX = "-LIST-"; 119 120 /** The JSON utils */ 121 protected JSONUtils _jsonUtils; 122 123 /** The SCC mapping helper */ 124 protected SynchronizableContentsCollectionMappingHelper _sccMappingHelper; 125 126 /** The catalogs manager */ 127 protected CatalogsManager _catalogsManager; 128 129 /** The Pégase API manager */ 130 protected PegaseApiManager _pegaseApiManager; 131 132 /** The CDM-fr handler */ 133 protected CDMFRHandler _cdmfrHandler; 134 135 /** The PégaseSCC helper */ 136 protected PegaseSCCMappingHelper _pegaseSccMappingHelper; 137 138 /** The reference table helper */ 139 protected OdfReferenceTableHelper _refTableHelper; 140 141 /** Name of the Pégase column which contains the ID */ 142 protected String _idColumn; 143 144 /** Mapping between ametys attribute and Pégase mapping, by content type*/ 145 protected Map<String, Map<String, MappingEntry>> _mappingByContentType; 146 147 /** Synchronized fields by content type */ 148 protected Map<String, Set<String>> _syncFieldsByContentType; 149 150 /** Synchronized fields */ 151 protected Set<String> _columns; 152 153 /** Context */ 154 protected Context _context; 155 156 /** List of imported contents */ 157 protected Map<String, Integer> _importedContents; 158 159 /** List of synchronized contents having differences */ 160 protected Set<String> _synchronizedContents; 161 162 /** List of updated contents by relation */ 163 protected Set<String> _updatedRelationContents; 164 165 /** Map to link contents to its children at the end of the process */ 166 protected Map<String, Set<String>> _contentsChildren; 167 168 /** Default language configured for ODF */ 169 protected String _odfLang; 170 171 /** The structure code for Pégase */ 172 protected String _structureCode; 173 174 @Override 175 public void service(ServiceManager manager) throws ServiceException 176 { 177 super.service(manager); 178 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 179 _sccMappingHelper = (SynchronizableContentsCollectionMappingHelper) manager.lookup(SynchronizableContentsCollectionMappingHelper.ROLE); 180 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 181 _pegaseApiManager = (PegaseApiManager) manager.lookup(PegaseApiManager.ROLE); 182 _cdmfrHandler = (CDMFRHandler) manager.lookup(CDMFRHandler.ROLE); 183 _pegaseSccMappingHelper = (PegaseSCCMappingHelper) manager.lookup(PegaseSCCMappingHelper.ROLE); 184 _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 185 } 186 187 @Override 188 public void contextualize(Context context) throws ContextException 189 { 190 _context = context; 191 } 192 193 @Override 194 public String getIdField() 195 { 196 return "pegaseSyncCode"; 197 } 198 199 /** 200 * Get the identifier JSON field. 201 * @return the column id 202 */ 203 protected String getIdColumn() 204 { 205 return _idColumn; 206 } 207 208 @SuppressWarnings("unchecked") 209 @Override 210 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 211 { 212 return Optional.ofNullable(additionalParameters) 213 .map(params -> params.get("contentTypes")) 214 .filter(List.class::isInstance) 215 .map(cTypes -> (List<String>) cTypes) 216 .filter(Predicate.not(List::isEmpty)) 217 .map(l -> l.get(0)) 218 .map(_syncFieldsByContentType::get) 219 .orElse(Set.of()); 220 } 221 222 @Override 223 protected Map<String, Object> putIdParameter(String idValue) 224 { 225 Map<String, Object> parameters = new HashMap<>(); 226 parameters.put(getIdColumn(), List.of(idValue)); 227 return parameters; 228 } 229 230 @Override 231 protected void configureDataSource(Configuration configuration) throws ConfigurationException 232 { 233 _odfLang = Config.getInstance().getValue("odf.programs.lang"); 234 235 if (Config.getInstance().getValue("pegase.activate", true, false)) 236 { 237 _structureCode = Config.getInstance().getValue("pegase.structure.code"); 238 239 // Mapping by content type Map<String(Type), Map<String(Ametys field), MappingString(Pégase field)>> 240 _mappingByContentType = new HashMap<>(); 241 _syncFieldsByContentType = new HashMap<>(); 242 _columns = new LinkedHashSet<>(); 243 try 244 { 245 org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 246 File pegaseMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/pegase/pegase-mapping.xml")); 247 248 try (InputStream is = pegaseMapping.isFile() 249 ? new FileInputStream(pegaseMapping) 250 : getClass().getResourceAsStream("/org/ametys/plugins/odfsync/pegase/pegase-mapping.xml")) 251 { 252 boolean programCfgPresent = false; 253 Configuration cfg = new DefaultConfigurationBuilder().build(is); 254 for (Configuration child : cfg.getChildren()) 255 { 256 // Sort the mapping by content type 257 String contentType = child.getAttribute("contentType"); 258 if (contentType != null) 259 { 260 // The program is the starting point of the import, we need to retrieve the global information from it 261 if (ProgramFactory.PROGRAM_CONTENT_TYPE.equals(contentType)) 262 { 263 programCfgPresent = true; 264 _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue(); 265 266 Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN); 267 for (Configuration column : columns) 268 { 269 _columns.add(column.getValue()); 270 } 271 } 272 273 String mappingAsString = child.getChild(__PARAM_MAPPING).getValue(); 274 Map<String, MappingEntry> mapping; 275 if (StringUtils.isNotEmpty(mappingAsString)) 276 { 277 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 278 mapping = _sccMappingHelper.getMappingFromJSON(mappingAsList); 279 280 _syncFieldsByContentType.put(contentType, mapping.keySet().stream() 281 .filter(entry -> mapping.get(entry).synchro()) 282 .collect(Collectors.toSet())); 283 } 284 else 285 { 286 // Empty mapping 287 mapping = Map.of(); 288 } 289 290 _mappingByContentType.put(contentType, mapping); 291 } 292 else 293 { 294 throw new ConfigurationException("One item does not have a 'contentType' tag.", child); 295 } 296 } 297 298 if (!programCfgPresent) 299 { 300 throw new ConfigurationException("No configuration found for program content type in pegase-mapping.xml"); 301 } 302 } 303 } 304 catch (Exception e) 305 { 306 throw new ConfigurationException("Error while parsing pegase-mapping.xml", e); 307 } 308 } 309 } 310 311 @Override 312 protected void configureSearchModel() 313 { 314 List<String> sortableColumns = List.of("code", "libelle"); 315 316 _searchModelConfiguration.addCriterion("libelle", new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_PEGASE_CRITERION_LIBELLE"), "string"); 317 _searchModelConfiguration.addCriterion("validee", new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_PEGASE_CRITERION_VALIDEE"), "boolean", "edition.boolean-combobox"); 318 319 for (String columnName : _columns) 320 { 321 _searchModelConfiguration.addColumn(columnName, new I18nizableText(columnName), sortableColumns.contains(columnName)); 322 } 323 } 324 325 /** 326 * Get the catalog for import. 327 * @return the catalog 328 */ 329 protected String getCatalog() 330 { 331 return Optional.of(getParameterValues()) 332 .map(params -> params.get("catalog")) 333 .map(String.class::cast) 334 .filter(StringUtils::isNotBlank) 335 .orElseGet(() -> _catalogsManager.getDefaultCatalogName()); 336 } 337 338 @Override 339 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 340 { 341 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 342 343 try 344 { 345 ObjetsMaquetteExterneApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteExterneApi(); 346 347 // Get details for all requested programs by objetsMaquetteApi 348 PagedObjetMaquetteSummaries pagedPrograms = _getPagedProgramsSummaries(parameters, offset, limit, sort, objetsMaquetteApi); 349 Long total = pagedPrograms.getTotalElements(); 350 351 if (total != null) 352 { 353 // if total is provided, store it so that getTotalCount won't do a useless full search 354 parameters.put("totalCount", total.longValue()); 355 } 356 357 List<ObjetMaquetteSummary> programs = pagedPrograms.getItems(); 358 if (programs != null) 359 { 360 for (ObjetMaquetteSummary programSummary : programs) 361 { 362 if (programSummary != null) 363 { 364 String pegaseSyncCode = programSummary.getId().toString(); 365 366 // If not an import, fill the results with Pégase synchronization code 367 Map<String, Object> result = results.computeIfAbsent(pegaseSyncCode, __ -> new HashMap<>()); 368 Map<String, String> fields = _getSummaryFields(programSummary); 369 370 for (String keptField : _columns) 371 { 372 String field = fields.get(keptField); 373 result.put(keptField, field); 374 } 375 376 result.put(SCC_UNIQUE_ID, pegaseSyncCode); 377 } 378 } 379 } 380 } 381 catch (ApiException | IOException e) 382 { 383 throw new RuntimeException("Error while getting remote values", e); 384 } 385 386 return results; 387 } 388 389 @Override 390 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 391 { 392 // avoid to relaunch a full search if already done 393 Long totalCount = (Long) searchParameters.get("totalCount"); 394 395 if (totalCount != null) 396 { 397 return totalCount.intValue(); 398 } 399 400 return super.getTotalCount(searchParameters, logger); 401 } 402 403 private PagedObjetMaquetteSummaries _getPagedProgramsSummaries(Map<String, Object> parameters, int offset, int limit, List<Object> sort, ObjetsMaquetteExterneApi objetsMaquetteApi) throws ApiException 404 { 405 Pageable pageable = new Pageable(); 406 int pageNumber = offset / 50; 407 pageable.setPage(pageNumber); 408 pageable.setTaille(limit); 409 410 if (sort == null) 411 { 412 pageable.setTri(List.of()); 413 } 414 else 415 { 416 // Convert the sort parameter from Object to JSON 417 String jsonSortParameters = _jsonUtils.convertObjectToJson(sort.get(0)); 418 // Convert the sort parameter from JSON to Map<String, Object> 419 Map<String, Object> sortParameters = _jsonUtils.convertJsonToMap(jsonSortParameters); 420 // Create the list containing the sort result; it is going to be of the form : ["field,direction"] 421 List<String> sortParametersArray = new ArrayList<>(); 422 // Create the sort parameter that is going to be sent in the list; it is going to be of the form : "field,direction" 423 StringBuilder stringBuilder = new StringBuilder(); 424 // Get the parameter "property" which is the column on which the sorting is meant to be made 425 String property = (String) sortParameters.get("property"); 426 427 if (!"code".equals(property) && !"libelle".equals(property)) 428 { 429 if ("libelle".equals(property)) 430 { 431 stringBuilder.append("libelle,").append((String) sortParameters.get("direction")); 432 sortParametersArray.add(stringBuilder.toString()); 433 } 434 } 435 else 436 { 437 stringBuilder.append(property).append(",").append((String) sortParameters.get("direction")); 438 sortParametersArray.add(stringBuilder.toString()); 439 } 440 441 pageable.setTri(sortParametersArray); 442 } 443 444 String searchLabel = (String) parameters.get("libelle"); 445 List<TypeObjetMaquette> typeObjets = List.of(TypeObjetMaquette.FORMATION); 446 Boolean validated = (Boolean) parameters.get("validee"); 447 return objetsMaquetteApi.rechercherObjetMaquette(_structureCode, pageable, searchLabel, null, typeObjets, null, null, null, null, null, validated, null, null, null, null); 448 } 449 450 private Map<String, String> _getSummaryFields(ObjetMaquetteSummary objectSummary) 451 { 452 return Map.of("id", objectSummary.getId().toString(), 453 "code", StringUtils.defaultString(objectSummary.getCode()), 454 "libelle", StringUtils.defaultString(objectSummary.getLibelle()), 455 "espace", StringUtils.defaultString(objectSummary.getEspaceLibelle())); 456 } 457 458 @SuppressWarnings("unchecked") 459 @Override 460 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 461 { 462 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 463 464 List<String> pegaseSyncCodeValues = (List<String>) parameters.getOrDefault(getIdColumn(), new ArrayList<>()); 465 466 List<String> idValues = new ArrayList<>(); 467 468 try 469 { 470 ObjetsMaquetteExterneApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteExterneApi(); 471 MaquettesExterneApi maquettesApi = _pegaseApiManager.getMaquettesExterneApi(); 472 473 if (!pegaseSyncCodeValues.isEmpty()) 474 { 475 // import or synchronize single items 476 idValues.addAll(pegaseSyncCodeValues); 477 } 478 else 479 { 480 // global synchronization 481 PagedObjetMaquetteSummaries pagedPrograms = _getPagedProgramsSummaries(parameters, 0, Integer.MAX_VALUE, null, objetsMaquetteApi); 482 for (ObjetMaquetteSummary programDetails : pagedPrograms.getItems()) 483 { 484 idValues.add(programDetails.getId().toString()); 485 } 486 } 487 488 results.putAll(_getObjectDetailsForImport(idValues, objetsMaquetteApi, maquettesApi, logger)); 489 } 490 catch (ApiException | IOException e) 491 { 492 throw new RuntimeException("Error while getting remote values", e); 493 } 494 495 return results; 496 } 497 498 Map<String, Map<String, List<Object>>> _getObjectDetailsForImport(List<String> idValues, ObjetsMaquetteExterneApi objetsMaquetteApi, MaquettesExterneApi maquettesApi, Logger logger) throws ApiException 499 { 500 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 501 Set<UUID> alreadyHandledObjects = new HashSet<>(); 502 503 for (String idValue : idValues) 504 { 505 // Get the program tree structure from the idValue 506 MaquetteStructure maquette = maquettesApi.lireStructureMaquette(_structureCode, UUID.fromString(idValue)); 507 ObjetMaquetteStructure racine = maquette.getRacine(); 508 509 results.putAll(_getObjectDetailsForImport(racine, null, alreadyHandledObjects, objetsMaquetteApi, logger)); 510 } 511 512 return results; 513 } 514 515 private Map<String, Map<String, List<Object>>> _getObjectDetailsForImport(ObjetMaquetteStructure item, EnfantsStructure structure, Set<UUID> alreadyHandledObjects, ObjetsMaquetteExterneApi objetsMaquetteApi, Logger logger) throws ApiException 516 { 517 Map<String, Map<String, List<Object>>> results = new LinkedHashMap<>(); 518 519 UUID id = item.getId(); 520 521 if (alreadyHandledObjects.add(id)) 522 { 523 Map<String, Object> detail = objetsMaquetteApi.lireJSONObjetMaquette(_structureCode, id); 524 525 // get data for this particular Pegase object 526 Map<String, List<Object>> objectData = _getObjectFields(detail, structure, item); 527 528 // then compute Ametys children from the Pegase structure 529 ComputedChildren children = _computeChildren(item); 530 531 // link to Ametys children 532 objectData.put("children", children.childrenIds()); 533 534 results.put(id.toString(), objectData); 535 536 // add the newly created intermediary objects, if any 537 results.putAll(children.newObjects()); 538 539 // then finally import recursively next objects 540 for (EnfantsStructure child : children.nextObjects()) 541 { 542 results.putAll(_getObjectDetailsForImport(child.getObjetMaquette(), child, alreadyHandledObjects, objetsMaquetteApi, logger)); 543 } 544 } 545 546 return results; 547 } 548 549 private ComputedChildren _computeChildren(ObjetMaquetteStructure item) 550 { 551 List<EnfantsStructure> enfants = item.getEnfants(); 552 553 List<Object> childrenIds = new ArrayList<>(); 554 List<EnfantsStructure> nextObjects = new ArrayList<>(); 555 Map<String, Map<String, List<Object>>> newObjects = new HashMap<>(); 556 557 if (_isProgramPart(item)) 558 { 559 // item is a programpart, we allow other programparts as direct children 560 List<EnfantsStructure> programPartChildren = enfants.stream().filter(e -> _isProgramPart(e.getObjetMaquette())).toList(); 561 childrenIds.addAll(programPartChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 562 nextObjects.addAll(programPartChildren); 563 564 // if there are direct courses, we should inject an intermediary list 565 _computeIntermediateLists(item, enfants, childrenIds, nextObjects, newObjects); 566 567 // add groups 568 List<EnfantsStructure> listChildren = enfants.stream().filter(e -> _isCourseList(e.getObjetMaquette())).toList(); 569 childrenIds.addAll(listChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 570 nextObjects.addAll(listChildren); 571 } 572 else if (_isCourseList(item)) 573 { 574 List<EnfantsStructure> courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette())).toList(); 575 childrenIds.addAll(courseChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 576 nextObjects.addAll(courseChildren); 577 } 578 else if (_isCourse(item)) 579 { 580 // if there are direct courses, we should inject an intermediary list 581 _computeIntermediateLists(item, enfants, childrenIds, nextObjects, newObjects); 582 583 // add groups 584 List<EnfantsStructure> listChildren = enfants.stream().filter(e -> _isCourseList(e.getObjetMaquette())).toList(); 585 childrenIds.addAll(listChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).toList()); 586 nextObjects.addAll(listChildren); 587 } 588 589 return new ComputedChildren(childrenIds, newObjects, nextObjects); 590 } 591 592 private String _getContentType(ObjetMaquetteStructure item) 593 { 594 String type = item.getType(); 595 if (item.getClasse().equals("F")) 596 { 597 type = "FORMATION"; 598 } 599 else if (item.getClasse().equals("G")) 600 { 601 type = "GROUPEMENT"; 602 } 603 604 return Optional.ofNullable(type) 605 .map(_pegaseSccMappingHelper::getAmetysType) 606 .orElse(CourseFactory.COURSE_CONTENT_TYPE); 607 } 608 609 private boolean _isProgramPart(ObjetMaquetteStructure item) 610 { 611 String contentType = _getContentType(item); 612 613 if (contentType.equals(ProgramFactory.PROGRAM_CONTENT_TYPE) || contentType.equals(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE) || contentType.equals(ContainerFactory.CONTAINER_CONTENT_TYPE)) 614 { 615 return true; 616 } 617 618 return false; 619 } 620 621 private boolean _isCourse(ObjetMaquetteStructure item) 622 { 623 return _getContentType(item).equals(CourseFactory.COURSE_CONTENT_TYPE); 624 } 625 626 private boolean _isCourseList(ObjetMaquetteStructure item) 627 { 628 return _getContentType(item).equals(CourseListFactory.COURSE_LIST_CONTENT_TYPE); 629 } 630 631 private void _computeIntermediateLists(ObjetMaquetteStructure item, List<EnfantsStructure> enfants, List<Object> childrenIds, List<EnfantsStructure> nextObjects, Map<String, Map<String, List<Object>>> newObjects) 632 { 633 List<EnfantsStructure> courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette()) && e.getObligatoire()).toList(); 634 _computeIntermediateList(item, courseChildren, true, childrenIds, nextObjects, newObjects); 635 636 courseChildren = enfants.stream().filter(e -> _isCourse(e.getObjetMaquette()) && !e.getObligatoire()).toList(); 637 _computeIntermediateList(item, courseChildren, false, childrenIds, nextObjects, newObjects); 638 } 639 640 private void _computeIntermediateList(ObjetMaquetteStructure item, List<EnfantsStructure> courseChildren, boolean mandatory, List<Object> childrenIds, List<EnfantsStructure> nextObjects, Map<String, Map<String, List<Object>>> newObjects) 641 { 642 if (!courseChildren.isEmpty()) 643 { 644 String listId = item.getId() + __INSERTED_GROUPEMENT_SUFFIX + (mandatory ? "O" : "F"); 645 childrenIds.add(listId); 646 647 Map<String, List<Object>> listData = new HashMap<>(); 648 listData.put("workflowDescription", List.of(ContentWorkflowDescription.COURSELIST_WF_DESCRIPTION)); 649 listData.put(getIdField(), List.of(listId)); 650 listData.put("title", List.of(item.getLibelle() + " - Liste " + (mandatory ? "O" : "F"))); 651 listData.put("obligatoire", List.of(String.valueOf(mandatory))); 652 listData.put("plageDeChoix", List.of("false")); 653 listData.put("children", courseChildren.stream().map(e -> e.getObjetMaquette().getId().toString()).collect(Collectors.toList())); 654 655 newObjects.put(listId, listData); 656 nextObjects.addAll(courseChildren); 657 } 658 } 659 660 private Map<String, List<Object>> _getObjectFields(Map<String, Object> objectDetails, EnfantsStructure structure, ObjetMaquetteStructure item) 661 { 662 Map<String, List<Object>> result = new HashMap<>(); 663 664 String contentTypeId = _getContentType(item); 665 ContentWorkflowDescription wfDescription = ContentWorkflowDescription.getByContentType(contentTypeId); 666 667 result.put("workflowDescription", List.of(wfDescription)); 668 669 Map<String, MappingEntry> contentTypeMapping = _mappingByContentType.get(contentTypeId); 670 671 for (String itemRef : contentTypeMapping.keySet()) 672 { 673 MappingEntry entry = contentTypeMapping.get(itemRef); 674 Object value = _findDataInFields(entry.remoteRef(), objectDetails); 675 676 result.put(itemRef, value != null ? List.of(value) : null); // Add the retrieved metadata values list to the contentResult 677 } 678 679 // Add enfant structure field 680 Boolean mandatory = structure != null ? structure.getObligatoire() : null; 681 if (mandatory != null) 682 { 683 result.put("mandatory", List.of(mandatory)); 684 } 685 686 // Add the ID field 687 result.put(getIdField(), List.of(objectDetails.get("id").toString())); 688 689 return result; 690 } 691 692 /** 693 * Find nested data in fields 694 * @param dataPath The data path 695 * @param fields The fields 696 * @return The value or null if it was not found. 697 */ 698 @SuppressWarnings("unchecked") 699 protected Object _findDataInFields(String dataPath, Map<String, Object> fields) 700 { 701 String[] pathSegments = StringUtils.split(dataPath, "/"); 702 if (pathSegments != null) 703 { 704 if (pathSegments.length == 1) 705 { 706 return fields.get(dataPath); 707 } 708 else if (pathSegments.length > 1) 709 { 710 Object subFields = fields.get(pathSegments[0]); 711 if (subFields != null && subFields instanceof Map subFieldsMap) 712 { 713 String subDataPath = StringUtils.join(pathSegments, "/", 1, pathSegments.length); 714 return _findDataInFields(subDataPath, subFieldsMap); 715 } 716 } 717 } 718 719 return 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 Strings.CI.contains(title, "Semestre 1") || Strings.CI.contains(title, "S1") -> "s1"; 1089 case String title when Strings.CI.contains(title, "Semestre 2") || Strings.CI.contains(title, "S2") -> "s2"; 1090 case String title when Strings.CI.contains(title, "Semestre 3") || Strings.CI.contains(title, "S3") -> "s3"; 1091 case String title when Strings.CI.contains(title, "Semestre 4") || Strings.CI.contains(title, "S4") -> "s4"; 1092 case String title when Strings.CI.contains(title, "Semestre 5") || Strings.CI.contains(title, "S5") -> "s5"; 1093 case String title when Strings.CI.contains(title, "Semestre 6") || Strings.CI.contains(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}