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}