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