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