001/*
002 *  Copyright 2022 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.ws.structure;
018
019import java.io.IOException;
020import java.math.BigDecimal;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.UUID;
027import java.util.regex.Pattern;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.data.ContentValue;
036import org.ametys.cms.data.RichTextHelper;
037import org.ametys.cms.repository.Content;
038import org.ametys.odf.ODFHelper;
039import org.ametys.odf.ProgramItem;
040import org.ametys.odf.course.Course;
041import org.ametys.odf.course.CourseFactory;
042import org.ametys.odf.courselist.CourseList;
043import org.ametys.odf.courselist.CourseList.ChoiceType;
044import org.ametys.odf.enumeration.OdfReferenceTableEntry;
045import org.ametys.odf.program.AbstractProgram;
046import org.ametys.odf.program.Container;
047import org.ametys.odf.program.Program;
048import org.ametys.odf.program.ProgramFactory;
049import org.ametys.odf.program.ProgramPart;
050import org.ametys.odf.program.SubProgram;
051import org.ametys.plugins.odfsync.export.AbstractExportStructure;
052import org.ametys.plugins.odfsync.export.ExportReport;
053import org.ametys.plugins.odfsync.export.ExportReport.ExportStatus;
054import org.ametys.plugins.odfsync.export.ExportReport.ProblemTypes;
055import org.ametys.plugins.odfsync.pegase.ws.PegaseApiManager;
056import org.ametys.plugins.odfsync.pegase.ws.PegaseExportException;
057import org.ametys.plugins.odfsync.pegase.ws.PegaseHelper;
058import org.ametys.plugins.repository.AmetysRepositoryException;
059import org.ametys.plugins.repository.data.UnknownDataException;
060import org.ametys.runtime.config.Config;
061import org.ametys.runtime.i18n.I18nizableText;
062
063import fr.pcscol.pegase.odf.ApiException;
064import fr.pcscol.pegase.odf.api.ObjetsMaquetteApi;
065import fr.pcscol.pegase.odf.model.Contexte;
066import fr.pcscol.pegase.odf.model.CreerFormationRequest;
067import fr.pcscol.pegase.odf.model.CreerGroupementRequest;
068import fr.pcscol.pegase.odf.model.CreerObjetFormationRequest;
069import fr.pcscol.pegase.odf.model.CreerObjetMaquetteRequest;
070import fr.pcscol.pegase.odf.model.DescripteursAglae;
071import fr.pcscol.pegase.odf.model.DescripteursEnqueteRequest;
072import fr.pcscol.pegase.odf.model.DescripteursFormationRequest;
073import fr.pcscol.pegase.odf.model.DescripteursFormationSyllabus;
074import fr.pcscol.pegase.odf.model.DescripteursGroupementRequest;
075import fr.pcscol.pegase.odf.model.DescripteursObjetFormationRequest;
076import fr.pcscol.pegase.odf.model.DescripteursObjetFormationSyllabus;
077import fr.pcscol.pegase.odf.model.DescripteursObjetMaquetteRequest;
078import fr.pcscol.pegase.odf.model.DescripteursSiseRequest;
079import fr.pcscol.pegase.odf.model.DescripteursSyllabus;
080import fr.pcscol.pegase.odf.model.Enfant;
081import fr.pcscol.pegase.odf.model.EnfantsStructure;
082import fr.pcscol.pegase.odf.model.MaquetteStructure;
083import fr.pcscol.pegase.odf.model.ObjetMaquetteDetail;
084import fr.pcscol.pegase.odf.model.ObjetMaquetteStructure;
085import fr.pcscol.pegase.odf.model.ObjetMaquetteSummary;
086import fr.pcscol.pegase.odf.model.Pageable;
087import fr.pcscol.pegase.odf.model.PagedObjetMaquetteSummaries;
088import fr.pcscol.pegase.odf.model.PlageDeChoix;
089import fr.pcscol.pegase.odf.model.TypeObjetMaquette;
090
091/**
092 * The structure to export the program in Pegase
093 */
094public class PegaseProgramStructure extends AbstractExportStructure implements Component, Initializable
095{
096    /** Role */
097    public static final String ROLE = PegaseProgramStructure.class.getName();
098    
099    /* Constants */
100    /** The attribute name for the Pégase code */
101    private static final String __CODE_PEGASE_ATTRIBUTE_NAME = "pegaseCode";
102    /** The pattern of a Pégase code */
103    private static final Pattern __CODE_PATTERN = Pattern.compile("^[A-Z0-9\\-]{3,30}$");
104    /** The attribute name for the Pégase Sync code (UUID) */
105    private static final String __PEGASE_SYNC_CODE = "pegaseSyncCode";
106    
107    /** The ects attribute path */
108    private static final String __ECTS_DATA_PATH = "ects";
109    
110    /** The type ID for a subProgram in pegase */
111    private static final String __PARCOURS_TYPE_ID = "PARCOURS-TYPE";
112    /** The type ID for a year in pegase */
113    private static final String __ANNEE_TYPE_ID = "ANNEE";
114    /** The type ID for a semester in pegase */
115    private static final String __SEMESTRE_TYPE_ID = "SEMESTRE";
116    
117    /** The mandatory attributes by content type */
118    private static final Map<String, Set<String>> __MANDATORY_ATTRIBUTES_BY_CONTENT_TYPE = new HashMap<>();
119    static
120    {
121        __MANDATORY_ATTRIBUTES_BY_CONTENT_TYPE.put(
122                ProgramFactory.PROGRAM_CONTENT_TYPE,
123                Set.of(
124                        AbstractProgram.EDUCATION_KIND
125                )
126        );
127        __MANDATORY_ATTRIBUTES_BY_CONTENT_TYPE.put(
128                CourseFactory.COURSE_CONTENT_TYPE,
129                Set.of(Course.COURSE_TYPE)
130        );
131    }
132
133    /* Components */
134    private PegaseApiManager _pegaseApiManager;
135    private ODFHelper _odfHelper;
136    private RichTextHelper _richTextHelper;
137    private PegaseHelper _pegaseHelper;
138
139    /* Pégase configuration */
140    private boolean _isActive;
141    private String _structureCode;
142    private boolean _trustAmetys;
143
144    
145    @Override
146    public void service(ServiceManager manager) throws ServiceException
147    {
148        super.service(manager);
149        
150        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
151        _pegaseApiManager = (PegaseApiManager) manager.lookup(PegaseApiManager.ROLE);
152        _richTextHelper = (RichTextHelper) manager.lookup(RichTextHelper.ROLE);
153        _pegaseHelper = (PegaseHelper) manager.lookup(PegaseHelper.ROLE);
154    }
155    
156    public void initialize() throws Exception
157    {
158        _isActive = Config.getInstance().getValue("pegase.activate", true, false);
159        if (_isActive)
160        {
161            _structureCode = Config.getInstance().getValue("pegase.structure.code");
162            _trustAmetys = Config.getInstance().getValue("pegase.trust", true, false);
163        }
164    }
165    
166    private String _getEtag(ObjetsMaquetteApi objetsMaquetteApi)
167    {
168        List<String> etags = objetsMaquetteApi.getApiClient().getResponseHeaders().get("ETag");
169        
170        // Defaults to version 1
171        String etag = "1";
172        if (etags != null && !etags.isEmpty())
173        {
174            etag = etags.get(0);
175        }
176        
177        return etag;
178    }
179    
180    private Pageable _getPageable(int page, int taille, List<String> tri)
181    {
182        Pageable pageable = new Pageable();
183        pageable.setPage(page);
184        pageable.setTaille(taille);
185        pageable.setTri(tri);
186        
187        return pageable;
188    }
189    
190    /**
191     * Checks if the program has all the required fields
192     * @param program the program to check
193     * @param report the Pegase export report
194     */
195    @Override
196    public void checkProgram(Program program, ExportReport report)
197    {
198        if (!_isActive)
199        {
200            throw new UnsupportedOperationException("Pégase is not active in the configuration, you cannot check the program for the export.");
201        }
202
203        try
204        {
205            _checkProgramItem(program, report);
206        }
207        catch (PegaseExportException e)
208        {
209            report.setStatus(ExportStatus.ERROR);
210            getLogger().error("An error occured while checking the program", e);
211        }
212    }
213
214    /**
215     * Checks if the program item has all the required fields
216     * @param programItem the program item to check
217     * @param report the Pegase export report
218     * @throws PegaseExportException If an error occurs while retrieving the Pégase espace
219     */
220    private void _checkProgramItem(ProgramItem programItem, ExportReport report) throws PegaseExportException
221    {
222        // Check program item attributes
223        _checkAttributes((Content) programItem, report);
224        
225        // Check children
226        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
227        {
228            _checkProgramItem(child, report);
229        }
230    }
231    
232    private void _checkAttributes(Content content, ExportReport report) throws PegaseExportException
233    {
234        // If it is a Program, check the codes and the coherence with Pégase
235        if (content instanceof Program)
236        {
237            _checkPegaseCodesForProgram(content, report);
238        }
239        // If it is a Program, only check the codes
240        else if (content instanceof ProgramItem)
241        {
242            _checkPegaseCodes(content, report);
243        }
244
245        // Check the other mandatory attributes by type
246        String contentType = content.getTypes()[0];
247        Set<String> mandatoryAttributes = __MANDATORY_ATTRIBUTES_BY_CONTENT_TYPE.getOrDefault(contentType, Set.of());
248        for (String attribute : mandatoryAttributes)
249        {
250            if (!content.hasValue(attribute))
251            {
252                _addMandatoryDataPathAndReport(content, attribute, report);
253            }
254        }
255    }
256    
257    private void _checkPegaseCodesForProgram(Content content, ExportReport report) throws PegaseExportException
258    {
259        UUID syncCode = null;
260        
261        // Check if the Pegase sync code is present and valid
262        boolean hasValidSyncCode = false;
263        if (content.hasValue(__PEGASE_SYNC_CODE))
264        {
265            syncCode = _getUuidIfValid(content.getValue(__PEGASE_SYNC_CODE));
266            
267            hasValidSyncCode = syncCode != null;
268            if (!hasValidSyncCode)
269            {
270                report.addInvalidDataPath(content, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_EXPORT_PEGASE_INVALID_SYNC_CODE"));
271            }
272        }
273        
274        // Check if the Pegase code is present
275        boolean hasValidPegaseCode = false;
276        if (content.hasValue(__CODE_PEGASE_ATTRIBUTE_NAME))
277        {
278            hasValidPegaseCode = _isValidPegaseCode(content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME));
279            if (!hasValidPegaseCode)
280            {
281                report.addInvalidDataPath(content, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_EXPORT_PEGASE_INVALID_CODE"));
282            }
283        }
284        
285        // If both codes are present and valid, check their coherence with each other and with Pégase
286        if (hasValidSyncCode && hasValidPegaseCode)
287        {
288            _checkProgramCoherenceWithTwoCodes(content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME), syncCode, report);
289        }
290        // If only the sync codes is present and valid, check its coherence with Pégase
291        else if (hasValidSyncCode && !hasValidPegaseCode)
292        {
293            _getProgramAndcheckCoherenceForUUID(syncCode, report);
294        }
295        // If only the Pégase codes is present and valid, check its coherence with Pégase
296        else if (!hasValidSyncCode && hasValidPegaseCode)
297        {
298            _checkProgramCoherenceByCode(content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME), report);
299        }
300        // If neither of the codes is present, report the problem
301        else
302        {
303            _addMandatoryPegaseCodeAndReport(content, report);
304        }
305    }
306    
307    /**
308     * Check if an object or a non editable program for the UUID and if the pegaseCode requested is coherent
309     * @param report The report
310     * @param pegaseCode The code wanted
311     * @param pegaseSyncCode The UUID of the Pégase object requested
312     * @throws PegaseExportException If an error occurs while retriving the espaceId
313     */
314    private void _checkProgramCoherenceWithTwoCodes(String pegaseCode, UUID pegaseSyncCode, ExportReport report) throws PegaseExportException
315    {
316        ObjetMaquetteSummary programFound = _getProgramAndcheckCoherenceForUUID(pegaseSyncCode, report);
317        
318        // If a program was found and was coherent with the UUID, check that the pegaseCode requested is coherent with the PegaseCode of the program found
319        if (programFound != null
320            && !ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS.equals(report.getStatus())
321            && !pegaseCode.equals(programFound.getCode()))
322        {
323            report.setStatus(ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS);
324        }
325    }
326    
327    /**
328     * Check if an object or a non editable program already exists for the UUID and retrives it
329     * @param report The report
330     * @param syncCode The UUID of the Pégase object requested
331     * @throws PegaseExportException If an error occurs while retriving the espaceId
332     */
333    private ObjetMaquetteSummary _getProgramAndcheckCoherenceForUUID(UUID syncCode, ExportReport report) throws PegaseExportException
334    {
335        try
336        {
337            ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
338            
339            // Get the objects with the same ID (syncCode) as the one requested
340            PagedObjetMaquetteSummaries pagedObjetMaquetteSummaries = objetsMaquetteApi.rechercherObjetMaquette(_structureCode, _getPageable(0, 10, List.of()), null, _pegaseHelper.getEspaceId(_structureCode).toString(), List.of(), null, null, List.of(syncCode), null, null, null, null);
341            
342            // Check that it can be edited
343            if (pagedObjetMaquetteSummaries.getTotalElements() == 1)
344            {
345                ObjetMaquetteSummary objetMaquetteSummary = pagedObjetMaquetteSummaries.getItems().get(0);
346
347                // If the object is not a Program or it is already validated, then it is not editable
348                if (!TypeObjetMaquette.FORMATION.equals(objetMaquetteSummary.getTypeObjetMaquette())
349                    || objetMaquetteSummary.getValideInAnyContexte())
350                {
351                    report.setStatus(ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS);
352                }
353                
354                return objetMaquetteSummary;
355            }
356            // Should not be possible, there can only be one object with the ID
357            else
358            {
359                report.setStatus(ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS);
360                return null;
361            }
362        }
363        catch (IOException | ApiException e)
364        {
365            report.setStatus(ExportStatus.ERROR);
366            getLogger().warn("Une erreur est survenue lors la recherche de l'élément de code de synchronization Pégase '{}' pour vérifier son existance préalable dans Pégase", syncCode, e);
367        }
368        
369        return null;
370    }
371    
372    /**
373     * Check if an object or a non editable program already exists for the PegaseCode wanted
374     * @param report The report
375     * @param pegaseCode The code wanted
376     * @throws PegaseExportException If an error occurred while retrieving the Pégase espace
377     */
378    private void _checkProgramCoherenceByCode(String pegaseCode, ExportReport report) throws PegaseExportException
379    {
380        try
381        {
382            ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
383            
384            // Get the objects with the same code as the one requested
385            PagedObjetMaquetteSummaries pagedObjetMaquetteSummaries = objetsMaquetteApi.rechercherObjetMaquette(_structureCode, _getPageable(0, 10, List.of()), pegaseCode, _pegaseHelper.getEspaceId(_structureCode).toString(), List.of(), null, null, List.of(), null, null, null, null);
386            
387            Long numberOfResults = pagedObjetMaquetteSummaries.getTotalElements();
388            // If there is one result, check if it is coherent
389            if (numberOfResults == 1)
390            {
391                ObjetMaquetteSummary objetMaquetteSummary = pagedObjetMaquetteSummaries.getItems().get(0);
392                
393                // If the object found is not a program or if it is validated, then it is not editable
394                if (!TypeObjetMaquette.FORMATION.equals(objetMaquetteSummary.getTypeObjetMaquette())
395                    || objetMaquetteSummary.getValideInAnyContexte())
396                {
397                    report.setStatus(ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS);
398                }
399            }
400            // If there are more than one result found, it is not editable
401            else if (numberOfResults > 1)
402            {
403                report.setStatus(ExportStatus.NON_EDITABLE_PROGRAM_ALREADY_EXISTS);
404            }
405            // If there are no results, the program is exportable to Pégase (since no sync code was requested)
406        }
407        catch (IOException | ApiException e)
408        {
409            report.setStatus(ExportStatus.ERROR);
410            getLogger().warn("Une erreur est survenue lors la recherche de l'élément de code de synchronization Pégase '{}' pour vérifier son existance préalable dans Pégase", pegaseCode, e);
411        }
412    }
413    
414    /**
415     * Check if there is at least one of the two Pegase codes
416     * @param report The report
417     * @param content The content
418     */
419    private void _checkPegaseCodes(Content content, ExportReport report)
420    {
421        // If there is no pegase sync code or if it is invalid, check the pegaseCode
422        if (!content.hasValue(__PEGASE_SYNC_CODE) || !(_getUuidIfValid(content.getValue(__PEGASE_SYNC_CODE)) != null))
423        {
424            // If there is no pegase code or it is invalid, report the missing fields
425            if (!content.hasValue(__CODE_PEGASE_ATTRIBUTE_NAME) || !_isValidPegaseCode(content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME)))
426            {
427                _addMandatoryPegaseCodeAndReport(content, report);
428            }
429        }
430    }
431
432    private UUID _getUuidIfValid(String uuidToCheck)
433    {
434        try
435        {
436            return UUID.fromString(uuidToCheck);
437        }
438        catch (Exception e)
439        {
440            return null;
441        }
442    }
443    
444    private boolean _isValidPegaseCode(String attribute)
445    {
446        return __CODE_PATTERN.matcher(attribute).matches();
447    }
448    
449    private void _addMandatoryDataPathAndReport(Content content, String dataPath, ExportReport report)
450    {
451        I18nizableText invalidMessage = new I18nizableText(
452            "plugin.odf-sync",
453            "PLUGINS_ODF_SYNC_EXPORT_PEGASE_MANDATORY_FIELD",
454            Map.of("fieldName", content.getDefinition(dataPath).getLabel())
455        );
456        report.addInvalidDataPath(content, invalidMessage);
457    }
458    
459    private void _addMandatoryPegaseCodeAndReport(Content content, ExportReport report)
460    {
461        I18nizableText invalidMessage = new I18nizableText(
462            "plugin.odf-sync",
463            "PLUGINS_ODF_SYNC_EXPORT_PEGASE_MANDATORY_FIELDS",
464            Map.of("fieldName", content.getDefinition(__PEGASE_SYNC_CODE).getLabel(),
465                   "secondFieldName", content.getDefinition(__CODE_PEGASE_ATTRIBUTE_NAME).getLabel())
466        );
467        report.addInvalidDataPath(content, invalidMessage);
468    }
469    
470    private Set<ProgramItem> _getAllChildrenProgramItems(ProgramItem programItem)
471    {
472        return _getAllChildrenProgramItems(programItem, new HashSet<>());
473    }
474
475    
476    private Set<ProgramItem> _getAllChildrenProgramItems(ProgramItem programItem, Set<ProgramItem> allChildrenProgramItems)
477    {
478        allChildrenProgramItems.add(programItem);
479        
480        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
481        {
482            if (!allChildrenProgramItems.contains(child))
483            {
484                allChildrenProgramItems.addAll(_getAllChildrenProgramItems(child, allChildrenProgramItems));
485            }
486        }
487        
488        return allChildrenProgramItems;
489    }
490    
491    private ObjetMaquetteAndEtagIfExists _getPegaseProgramIfAlreadyExists(Program program, ExportReport report) throws PegaseExportException
492    {
493        return _getPegaseObjetMaquetteDetailIfAlreadyExists(program, TypeObjetMaquette.FORMATION, report);
494    }
495    
496    private ObjetMaquetteAndEtagIfExists _getPegaseObjetFormationIfAlreadyExists(Content content, ExportReport report) throws PegaseExportException
497    {
498        return _getPegaseObjetMaquetteDetailIfAlreadyExists(content, TypeObjetMaquette.OBJET_FORMATION, report);
499    }
500    
501    private ObjetMaquetteAndEtagIfExists _getPegaseGroupIfAlreadyExists(Content content, ExportReport report) throws PegaseExportException
502    {
503        return _getPegaseObjetMaquetteDetailIfAlreadyExists(content, TypeObjetMaquette.GROUPEMENT, report);
504    }
505    
506    private ObjetMaquetteAndEtagIfExists _getPegaseObjetMaquetteDetailIfAlreadyExists(Content content, TypeObjetMaquette type, ExportReport report) throws PegaseExportException
507    {
508        try
509        {
510            // If there is a sync code, it has been verified to be valid, try to get the Pégase object
511            if (content.hasValue(__PEGASE_SYNC_CODE))
512            {
513                String id = content.getValue(__PEGASE_SYNC_CODE);
514                try
515                {
516                    ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
517                    ObjetMaquetteDetail objetMaquetteFound = objetsMaquetteApi.lireObjetMaquette(_structureCode, UUID.fromString(id));
518                    
519                    // If the object found is not in the right type, throw an exception
520                    if (!type.equals(objetMaquetteFound.getTypeObjetMaquette()))
521                    {
522                        throw new PegaseExportException("Erreur lors de l'export de " + content.getTitle() + ". Un élément avec l'identifiant Pégase existe déjà et n'est pas mutualisable ou modifiable");
523                    }
524                    
525                    return new ObjetMaquetteAndEtagIfExists(objetMaquetteFound, _getEtag(objetsMaquetteApi));
526                }
527                catch (ApiException e)
528                {
529                    throw new PegaseExportException("Un code de synchronisation ('" + id + "') Pégase a été entré pour l'élément " + content.getTitle() + " mais aucun élément ne correspond à ce code dans Pégase.", e);
530                }
531            }
532            else if (content.hasValue(__CODE_PEGASE_ATTRIBUTE_NAME))
533            {
534                String pegaseCode = content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME);
535                
536                return _getObjetMaquetteDetailFromPaged(content, pegaseCode, type, report);
537            }
538        }
539        catch (ApiException | IOException e)
540        {
541            throw new PegaseExportException("Une erreur est survenue en essayant de récupérer un élément déjà existant pour l'élément '" + content.getTitle() + "'.", e);
542        }
543        
544        return new ObjetMaquetteAndEtagIfExists(null, null);
545    }
546    
547    private ObjetMaquetteAndEtagIfExists _getObjetMaquetteDetailFromPaged(Content content, String pegaseCode, TypeObjetMaquette type, ExportReport report) throws ApiException, IOException, PegaseExportException
548    {
549        PagedObjetMaquetteSummaries pagedObjetsMaquetteFound = _pegaseApiManager.getObjetsMaquetteApi().rechercherObjetMaquette(_structureCode, _getPageable(0, Integer.MAX_VALUE, List.of()), pegaseCode, _pegaseHelper.getEspaceId(_structureCode).toString(), List.of(), null, null, List.of(), null, false, false, false);
550        
551        ObjetMaquetteAndEtagIfExists objetMaquetteFound = _lookForObjetMaquetteInPage(content, pagedObjetsMaquetteFound, pegaseCode, type, report);
552        
553        if (objetMaquetteFound != null)
554        {
555            return objetMaquetteFound;
556        }
557        
558        for (int page = 0; page < pagedObjetsMaquetteFound.getTotalPages(); page++)
559        {
560            objetMaquetteFound = _lookForObjetMaquetteInPage(content, page, Integer.MAX_VALUE, pegaseCode, type, report);
561
562            if (objetMaquetteFound != null)
563            {
564                return objetMaquetteFound;
565            }
566        }
567        
568        return new ObjetMaquetteAndEtagIfExists(null, null);
569    }
570    
571    private ObjetMaquetteAndEtagIfExists _lookForObjetMaquetteInPage(Content content, int page, int taille, String pegaseCode, TypeObjetMaquette type, ExportReport report) throws ApiException, IOException, PegaseExportException
572    {
573        PagedObjetMaquetteSummaries pagedObjetsMaquetteFound = _pegaseApiManager.getObjetsMaquetteApi().rechercherObjetMaquette(_structureCode, _getPageable(page, taille, List.of()), pegaseCode, _pegaseHelper.getEspaceId(_structureCode).toString(), List.of(), null, null, List.of(), null, false, false, false);
574        
575        return _lookForObjetMaquetteInPage(content, pagedObjetsMaquetteFound, pegaseCode, type, report);
576    }
577    
578    private ObjetMaquetteAndEtagIfExists _lookForObjetMaquetteInPage(Content content, PagedObjetMaquetteSummaries pagedObjetsMaquetteFound, String pegaseCode, TypeObjetMaquette type, ExportReport report) throws ApiException, IOException, UnknownDataException, AmetysRepositoryException, PegaseExportException
579    {
580        List<ObjetMaquetteSummary> items = pagedObjetsMaquetteFound.getItems();
581        if (items != null)
582        {
583            for (ObjetMaquetteSummary objetMaquette : items)
584            {
585                if (pegaseCode.equals(objetMaquette.getCode()))
586                {
587                    if (objetMaquette.getTypeObjetMaquette().equals(type) && !objetMaquette.getValideInAnyContexte())
588                    {
589                        ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
590                        return new ObjetMaquetteAndEtagIfExists(objetsMaquetteApi.lireObjetMaquette(_structureCode, objetMaquette.getId()), _getEtag(objetsMaquetteApi));
591                    }
592                    else
593                    {
594                        report.updateExportReport(ExportStatus.ERROR, ProblemTypes.ELEMENT_ALREADY_EXIST);
595                        throw new PegaseExportException("Erreur lors de l'export de " + content.getTitle() + ". Un élément avec l'identifiant ou le code Pégase existe déjà et n'est pas mutualisable ou modifiable");
596                    }
597                }
598            }
599        }
600        
601        return new ObjetMaquetteAndEtagIfExists(null, null);
602    }
603    
604    private void _initCreerObjetMaquetteRequest(CreerObjetMaquetteRequest creerObjetMaquetteRequest, TypeObjetMaquette typeObjetMaquette, Boolean mutualise, Content content) throws PegaseExportException
605    {
606        creerObjetMaquetteRequest.setCode(content.getValue(__CODE_PEGASE_ATTRIBUTE_NAME));
607        creerObjetMaquetteRequest.setTypeObjetMaquette(typeObjetMaquette);
608        creerObjetMaquetteRequest.setEspaceId(_pegaseHelper.getEspaceId(_structureCode));
609        creerObjetMaquetteRequest.setMutualise(mutualise);
610    }
611    
612    private Enfant _createPegaseChild(UUID pegaseId, boolean mandatory)
613    {
614        Enfant pegaseChild = new Enfant();
615        
616        pegaseChild.setId(pegaseId.toString());
617        pegaseChild.setObligatoire(mandatory);
618        
619        return pegaseChild;
620    }
621    
622    /**
623     * Create a program in Pegase
624     * @param program the program to export
625     * @param report the Pegase export report
626     */
627    @Override
628    public void createProgram(Program program, ExportReport report)
629    {
630        if (!_isActive)
631        {
632            throw new UnsupportedOperationException("Pégase is not active in the configuration, you cannot import or synchronize a program in Pégase.");
633        }
634
635        int nbTotal = _getAllChildrenProgramItems(program).size();
636
637        report.setNbTotal(nbTotal);
638
639        try
640        {
641            // Create the Program in Pegase
642            ObjetMaquetteDetail pegaseProgram = _createOrUpdateProgram(program, report);
643            report.addElementExported(program);
644
645            // Get the children of the Ametys program
646            List<ProgramPart> children = program.getProgramPartChildren();
647            
648            // The map of the children to link to the program and their own children
649            Map<String, PegaseChildWithChildren> childrenToLink = new HashMap<>();
650
651            for (ProgramPart child : children)
652            {
653                // Create the pegase instance of the child
654                PegaseChildWithChildren enfant = _createChild((Content) child, false, report);
655
656                // Add the Enfant (created from the Pegase Id of the child) to the list of "Enfant" to link, if it is not already attached
657                if (enfant != null)
658                {
659                    childrenToLink.put(enfant.pegaseChild().getId(), enfant);
660                }
661            }
662
663            _detachAndAttachAllChildren(program, pegaseProgram.getId(), childrenToLink, report);
664        }
665        catch (Exception e)
666        {
667            report.updateStatus(ExportStatus.ERROR);
668            getLogger().error("Une erreur est survenue lors de l'export de la formation '{}' ({}) dans Pégase", program.getTitle(), program.getId(), e);
669        }
670    }
671    
672    private ObjetMaquetteDetail _createOrUpdateProgram(Program program, ExportReport report) throws PegaseExportException
673    {
674        try
675        {
676            ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists = _getPegaseProgramIfAlreadyExists(program, report);
677            
678            ObjetMaquetteDetail objetMaquetteDetailFound = objetMaquetteAndEtagIfExists.objetMaquetteDetail();
679            
680            // Program does not already exists, need to create it
681            if (objetMaquetteDetailFound == null)
682            {
683                return _createProgram(program);
684            }
685            // Program already exists, need to update it
686            else
687            {
688                // Get the previous etag (number of version) required to update the program
689                String etag = objetMaquetteAndEtagIfExists.etag();
690                return _updateProgram(program, objetMaquetteDetailFound, etag);
691            }
692        }
693        catch (Exception e)
694        {
695            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.API_ERROR);
696
697            throw new PegaseExportException("Une erreur est survenue lors de la création ou modification de la formation dans Pégase", e);
698        }
699    }
700    
701    private ObjetMaquetteDetail _createProgram(Program program) throws ApiException, IOException, PegaseExportException
702    {
703        ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
704        
705        CreerFormationRequest creerFormationRequest = new CreerFormationRequest();
706        _initCreerObjetMaquetteRequest(creerFormationRequest, TypeObjetMaquette.FORMATION, false, program);
707        
708        // Update descripteurs formation
709        creerFormationRequest.setDescripteursObjetMaquette(_getDescripteursFormationRequest(program));
710
711        ObjetMaquetteDetail programCreated = objetsMaquetteApi.creerObjetMaquette(_structureCode, creerFormationRequest);
712        
713        String etag = _getEtag(objetsMaquetteApi);
714        
715        return _updateDescripteurProgram(program, programCreated.getId(), etag, objetsMaquetteApi);
716    }
717    
718    private ObjetMaquetteDetail _updateProgram(Program program, ObjetMaquetteDetail objetMaquetteDetailFound, String etag) throws IOException, ApiException, PegaseExportException
719    {
720        ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
721
722        // Update descripteurs formation
723        DescripteursFormationRequest descripteursFormationRequest = _getDescripteursFormationRequest(program);
724        
725        // If no updates were necessary, return the former objetMaquetteDetail
726        UUID pegaseProgramId = objetMaquetteDetailFound.getId();
727        objetsMaquetteApi.modifierDescripteursObjetMaquette(_structureCode, pegaseProgramId, etag, descripteursFormationRequest);
728        
729        return _updateDescripteurProgram(program, pegaseProgramId, _getEtag(objetsMaquetteApi), objetsMaquetteApi);
730    }
731    
732    private ObjetMaquetteDetail _updateDescripteurProgram(AbstractProgram program, UUID pegaseId, String etag, ObjetsMaquetteApi objetsMaquetteApi) throws ApiException, IOException, PegaseExportException
733    {
734        _updateDescripteursEnqueteFormation(program, pegaseId, etag, objetsMaquetteApi);
735        
736        return _updateDescripteursSyllabus(program, pegaseId, TypeObjetMaquette.FORMATION, new DescripteursFormationSyllabus());
737    }
738    
739    private DescripteursFormationRequest _getDescripteursFormationRequest(Program program)
740    {
741        DescripteursFormationRequest descripteursFormationRequest = new DescripteursFormationRequest();
742        
743        String programTitle = program.getTitle();
744        
745        descripteursFormationRequest.setLibelle(StringUtils.truncate(programTitle, 50));
746        descripteursFormationRequest.setLibelleLong(StringUtils.truncate(programTitle, 150));
747        descripteursFormationRequest.setEcts(_getEctsFromContent(program));
748        
749        // Education king -> Type
750        descripteursFormationRequest.setType(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.EDUCATION_KIND));
751        
752        return descripteursFormationRequest;
753    }
754    
755    private ObjetMaquetteDetail _updateDescripteursSyllabus(Content content, UUID pegaseProgramId, TypeObjetMaquette type, DescripteursSyllabus descripteursSyllabus) throws ApiException, IOException, PegaseExportException
756    {
757        ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
758        
759        String description = null;
760        if (content.hasValue("description"))
761        {
762            description = _richTextHelper.richTextToString(content.getValue("description"));
763        }
764        else if (content.hasValue("presentation"))
765        {
766            description = _richTextHelper.richTextToString(content.getValue("presentation"));
767        }
768        
769        if (description != null)
770        {
771            descripteursSyllabus.setDescription(StringUtils.truncate(description, 2000)); // la description doit faire moins de 2000 caractères
772        }
773        
774        if (content.hasValue("objectives"))
775        {
776            descripteursSyllabus.setObjectif(StringUtils.truncate(_richTextHelper.richTextToString(content.getValue("objectives")), 2000)); // l'objectif doit faire moins de 2000 caractères
777        }
778        
779        if (content.hasValue("neededPrerequisite"))
780        {
781            descripteursSyllabus.setPrerequisPedagogique(StringUtils.truncate(_richTextHelper.richTextToString(content.getValue("neededPrerequisite")), 2000)); // les prérequis doit faire moins de 2000 caractères
782        }
783        
784        // Retrieve the created object to get its current Etag
785        ObjetMaquetteAndEtagIfExists objetMaquetteExistingAndEtag = _getPegaseObjetMaquetteDetailIfAlreadyExists(content, type, new ExportReport(content));
786        
787        return objetsMaquetteApi.modifierDescripteursSyllabusObjetMaquette(_structureCode, pegaseProgramId, objetMaquetteExistingAndEtag.etag(), descripteursSyllabus);
788    }
789    
790    private ObjetMaquetteDetail _updateDescripteursEnqueteFormation(AbstractProgram program, UUID pegaseProgramId, String etag, ObjetsMaquetteApi objetsMaquetteApi) throws ApiException
791    {
792        DescripteursSiseRequest descripteursSiseRequest = new DescripteursSiseRequest();
793        
794        // Degree -> typeDiplome
795        descripteursSiseRequest.setTypeDiplome(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.DEGREE));
796        // EducationLevel -> niveauFormation
797        descripteursSiseRequest.setNiveauDiplomeSise(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.LEVEL));
798        // RncpLevel -> niveauDiplome
799        descripteursSiseRequest.setNiveauDiplome(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.RNCP_LEVEL));
800        // Domain
801        descripteursSiseRequest.setDomaineFormation(_pegaseHelper.getPegaseCodeForFirstValue(program, AbstractProgram.DOMAIN));
802        // Mention
803        descripteursSiseRequest.setMention(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.MENTION));
804        // ProgramFields -> champFormation
805        descripteursSiseRequest.setChampFormation(_pegaseHelper.getPegaseCodeForField(program, AbstractProgram.PROGRAM_FIELD));
806        
807        // Update descripteurs enquete
808        DescripteursEnqueteRequest descripteursEnqueteRequest = new DescripteursEnqueteRequest();
809        descripteursEnqueteRequest.setDescripteursSise(descripteursSiseRequest);
810        
811        DescripteursAglae descripteursAglae = new DescripteursAglae();
812        descripteursAglae.setHabilitePourBoursesAglae(false);
813        descripteursEnqueteRequest.setDescripteursAglae(descripteursAglae);
814        
815        return objetsMaquetteApi.modifierDescripteursEnqueteObjetMaquette(_structureCode, pegaseProgramId, etag, descripteursEnqueteRequest);
816    }
817    
818    private BigDecimal _getEctsFromContent(Content content)
819    {
820        if (content.hasValue(__ECTS_DATA_PATH))
821        {
822            Object value = content.getValue(__ECTS_DATA_PATH);
823            if (value instanceof Double ects)
824            {
825                return BigDecimal.valueOf(ects);
826            }
827            else if (value instanceof ContentValue contentValue)
828            {
829                try
830                {
831                    BigDecimal ects = contentValue.getContentIfExists()
832                            .map(c -> c.<String>getValue(OdfReferenceTableEntry.CODE))
833                            .map(Double::parseDouble)
834                            .map(BigDecimal::valueOf)
835                            .orElse(null);
836                    return ects;
837                }
838                catch (NumberFormatException e)
839                {
840                    getLogger().debug("The ects value '{}' is not a double, therefore, it is not compatible with Pégase ects and will not be exported", contentValue);
841                }
842                
843            }
844        }
845        
846        return null;
847    }
848    
849    private  PegaseChildWithChildren _createChild(Content content, boolean parentMutualise, ExportReport report)
850    {
851        boolean success = true;
852        try
853        {
854            // Conteneur : semestre, année -> OF
855            if (content instanceof Container container)
856            {
857                return _createOFFromContainer(container, parentMutualise, report);
858            }
859            // Parcours -> OF
860            if (content instanceof SubProgram subProgram)
861            {
862                return _createObjetFormationAndChildren(subProgram, __PARCOURS_TYPE_ID, parentMutualise, report);
863            }
864            // ELP -> OF
865            if (content instanceof Course course)
866            {
867                return _createObjetFormationAndChildren(course, _pegaseHelper.getPegaseCodeForFirstValue(course, Course.COURSE_TYPE), parentMutualise, report);
868            }
869            // liste d'ELP -> Groupement
870            if (content instanceof CourseList)
871            {
872                return _createGroupFromCourseList((CourseList) content, parentMutualise, report);
873            }
874    
875            return null;
876        }
877        catch (Exception ex)
878        {
879            success = false;
880            getLogger().error("Erreur lors de l'export de l'élément '{}'", content.getTitle(), ex);
881            return null;
882        }
883        finally
884        {
885            if (success)
886            {
887                report.addElementExported(content);
888            }
889        }
890    }
891    
892    /**
893     * Creates a Pégase objet formation from a container
894     * @param container The container
895     * @param parentMutualise If the element's parent is mutualised
896     * @param report The report
897     * @return An {@link Enfant} object, Pégase child
898     * @throws PegaseExportException If an error occurs
899     * @throws IOException If an API error occurs
900     * @throws ApiException If an error occurs
901     */
902    private PegaseChildWithChildren _createOFFromContainer(Container container, boolean parentMutualise, ExportReport report) throws PegaseExportException, IOException, ApiException
903    {
904        String type = null;
905
906        String natureCode = getContainerNatureCode(container);
907        boolean isYear = "annee".equals(natureCode);
908        boolean isSemester = "semestre".equals(natureCode);
909
910        // If it is a container of nature : semester of year, export it as an OF of type semester or year
911        if (isYear || isSemester)
912        {
913            type = isYear ? __ANNEE_TYPE_ID : __SEMESTRE_TYPE_ID;
914
915            return _createObjetFormationAndChildren(container, type, parentMutualise, report);
916        }
917        
918        // Else, ignore it
919        return null;
920    }
921    
922    /**
923     * Creates a Pégase objet formation from a content
924     * @param content The content
925     * @param parentMutualise If the element's parent is mutualised
926     * @param report The report
927     * @return An {@link Enfant} object, Pégase child
928     * @throws PegaseExportException If an error occurs
929     * @throws IOException If an API error occurs
930     * @throws ApiException If an error occurs
931     */
932    private PegaseChildWithChildren _createObjetFormationAndChildren(Content content, String type, boolean parentMutualise, ExportReport report) throws IOException, PegaseExportException, ApiException
933    {
934        // Create or update the object
935        ObjetMaquetteDetail objetMaquetteDetailCreated = _createOrUpdateObjetFormation(content, type, parentMutualise, report);
936        
937        _updateDescripteursSyllabus(content, objetMaquetteDetailCreated.getId(), TypeObjetMaquette.OBJET_FORMATION, new DescripteursObjetFormationSyllabus());
938        
939        // Create the children if there are any
940        ChildrenToAttachForObjectRequest childrenToAttach = _createChildrenIfAny(content, objetMaquetteDetailCreated, report);
941
942        UUID objetMaquetteDetailCreatedId = objetMaquetteDetailCreated.getId();
943        
944        // Create and return an Enfant object from the child created
945        return new PegaseChildWithChildren(_createPegaseChild(objetMaquetteDetailCreatedId, true), childrenToAttach);
946
947    }
948    
949    private ObjetMaquetteDetail _createOrUpdateObjetFormation(Content content, String type, boolean parentMutualise, ExportReport report) throws PegaseExportException
950    {
951        try
952        {
953            // Check if the object already exists
954            ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists = _getPegaseObjetFormationIfAlreadyExists(content, report);
955            
956            ObjetMaquetteDetail objetMaquetteDetail = objetMaquetteAndEtagIfExists.objetMaquetteDetail();
957            // If it already exists, update the object
958            if (objetMaquetteDetail != null)
959            {
960                return _updateObjetFormation(objetMaquetteAndEtagIfExists, content, type, parentMutualise, report);
961            }
962            // If it did not already exist, create the object
963            else
964            {
965                return _createObjetFormation(content, type, parentMutualise, report);
966            }
967        }
968        catch (Exception e)
969        {
970            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.API_ERROR);
971
972            throw new PegaseExportException("Une erreur est survenue lors de la récupération de l'objet Pégase déjà existant", e);
973        }
974    }
975    
976    private PegaseChildWithChildren _createGroupFromCourseList(CourseList courseList, boolean parentMutualise, ExportReport report) throws PegaseExportException
977    {
978        ChoiceType type = courseList.getType();
979        boolean mandatory = !ChoiceType.OPTIONAL.equals(type);
980        PlageDeChoix plageDeChoix = ChoiceType.CHOICE.equals(type) ? _buildPlageDeChoix(courseList) : null;
981        
982        ObjetMaquetteDetail objetMaquetteDetailCreated = _createOrUpdateGroup(courseList, parentMutualise, plageDeChoix, report);
983
984        ChildrenToAttachForObjectRequest childrenToAttach = _createChildrenIfAny(courseList, objetMaquetteDetailCreated, report);
985
986        UUID objetMaquetteDetailCreatedId = objetMaquetteDetailCreated.getId();
987        
988        return new PegaseChildWithChildren(_createPegaseChild(objetMaquetteDetailCreatedId, mandatory), childrenToAttach);
989    }
990    
991    private ObjetMaquetteDetail _createOrUpdateGroup(CourseList courseList, boolean parentMutualise, PlageDeChoix plageDeChoix, ExportReport report) throws PegaseExportException
992    {
993        try
994        {
995            // Check if the object already exists
996            ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists = _getPegaseGroupIfAlreadyExists(courseList, report);
997            
998            ObjetMaquetteDetail objetMaquetteDetail = objetMaquetteAndEtagIfExists.objetMaquetteDetail();
999            // If it already exists, update the object
1000            if (objetMaquetteDetail != null)
1001            {
1002                return _updateGroup(objetMaquetteAndEtagIfExists, courseList, parentMutualise, plageDeChoix, report);
1003            }
1004            // If it did not already exist, create the object
1005            else
1006            {
1007                return _createGroup(courseList, parentMutualise, plageDeChoix, report);
1008            }
1009        }
1010        catch (Exception e)
1011        {
1012            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.API_ERROR);
1013
1014            throw new PegaseExportException("Une erreur est survenue lors de la récupération de l'objet Pégase déjà existant", e);
1015        }
1016    }
1017    
1018    private ObjetMaquetteDetail _updateGroup(ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists, Content content, boolean parentMutualise, PlageDeChoix plageDeChoix, ExportReport report) throws PegaseExportException, IOException
1019    {
1020        DescripteursGroupementRequest descripteursGroupementRequest = new DescripteursGroupementRequest();
1021        
1022        String label = content.getTitle();
1023        descripteursGroupementRequest.setLibelle(StringUtils.truncate(label, 50));
1024        descripteursGroupementRequest.setLibelleLong(StringUtils.truncate(label, 150));
1025        descripteursGroupementRequest.setPlageDeChoix(plageDeChoix);
1026        
1027        return _updateObjetMaquette(objetMaquetteAndEtagIfExists, content, parentMutualise, descripteursGroupementRequest, report);
1028    }
1029    
1030    private ObjetMaquetteDetail _createGroup(CourseList courseList, boolean parentMutualise, PlageDeChoix plageDeChoix, ExportReport report) throws PegaseExportException
1031    {
1032        DescripteursGroupementRequest descripteursGroupementRequest = new DescripteursGroupementRequest();
1033        if (plageDeChoix != null)
1034        {
1035            descripteursGroupementRequest.setPlageDeChoix(plageDeChoix);
1036        }
1037        
1038        CreerGroupementRequest creerGroupementRequest = new CreerGroupementRequest();
1039
1040        return _createObjetMaquette(courseList, TypeObjetMaquette.GROUPEMENT, parentMutualise, descripteursGroupementRequest, creerGroupementRequest, report);
1041    }
1042    
1043    private ChildrenToAttachForObjectRequest _createChildrenIfAny(Content content, ObjetMaquetteDetail objetMaquetteDetailCreated, ExportReport report) throws UnknownDataException, AmetysRepositoryException
1044    {
1045        Map<String, PegaseChildWithChildren> children = new HashMap<>();
1046        if (content instanceof ProgramItem programItem)
1047        {
1048            // Create the children
1049            children = _createAllChildren(_odfHelper.getChildProgramItems(programItem), objetMaquetteDetailCreated.getMutualise(), report);
1050        }
1051        
1052        return new ChildrenToAttachForObjectRequest(content, objetMaquetteDetailCreated.getId(), children);
1053    }
1054    
1055    private Map<String, PegaseChildWithChildren> _createAllChildren(List<ProgramItem> programItem, boolean parentMutualise, ExportReport report)
1056    {
1057        Map<String, PegaseChildWithChildren> children = new HashMap<>();
1058
1059        for (ProgramItem childContent : programItem)
1060        {
1061            PegaseChildWithChildren child = _createChild((Content) childContent, parentMutualise, report);
1062
1063            // If the child was created, add it to the list of children to attach
1064            if (child != null)
1065            {
1066                children.put(child.pegaseChild().getId(), child);
1067            }
1068        }
1069
1070        return children;
1071    }
1072    
1073    private void _detachAndAttachAllChildren(Content content, UUID pegaseParentID, Map<String, PegaseChildWithChildren> children, ExportReport report) throws PegaseExportException
1074    {
1075        try
1076        {
1077            // The children that are not going to be related to the program anymore
1078            Map<String, PegaseChildWithChildren> childrenToCheck = Map.copyOf(children);
1079            
1080            _detachChildren(content, pegaseParentID, children, report);
1081            
1082            // For each child that is not directly related to the program anymore, attach its own children recursively
1083            for (PegaseChildWithChildren childToAttach : childrenToCheck.values())
1084            {
1085                if (!children.containsValue(childToAttach))
1086                {
1087                    ChildrenToAttachForObjectRequest childrenToAttach = childToAttach.childrenToAttachRequest();
1088                    _attachAllChildren(childrenToAttach.parentContent(), childrenToAttach.parentId(), childrenToAttach.children(), report);
1089                }
1090            }
1091            
1092            _attachAllChildren(content, pegaseParentID, children, report);
1093        }
1094        catch (Exception e)
1095        {
1096            throw new PegaseExportException("Une erreur est survenue lors de l'attachement des enfants de la formation '" + content.getTitle() + "'");
1097        }
1098    }
1099    
1100    private void _detachChildren(Content content, UUID pegaseParentObjectId, Map<String, PegaseChildWithChildren> children, ExportReport report) throws UnknownDataException, AmetysRepositoryException, PegaseExportException
1101    {
1102        try
1103        {
1104            // Detach children recursively
1105            for (PegaseChildWithChildren childToAttach : children.values())
1106            {
1107                ChildrenToAttachForObjectRequest childrenToAttach = childToAttach.childrenToAttachRequest();
1108                _detachChildren(childrenToAttach.parentContent(), childrenToAttach.parentId(), childrenToAttach.children(), report);
1109            }
1110            
1111            MaquetteStructure structureMaquetteParent = _pegaseApiManager.getMaquettesApi().lireStructureMaquette(_structureCode, pegaseParentObjectId);
1112            
1113            ObjetMaquetteStructure parentRacine = structureMaquetteParent.getRacine();
1114            List<EnfantsStructure> childrenAlreadyAttached = parentRacine.getEnfants();
1115            
1116            // If there are children already attached and the option trustAmetys is checked, remove every child already attached from the list of children to attach
1117            if (childrenAlreadyAttached != null)
1118            {
1119                for (EnfantsStructure child : childrenAlreadyAttached)
1120                {
1121                    UUID childId = child.getObjetMaquette().getId();
1122                    String stringChildId = childId.toString();
1123                    
1124                    // Detach children in Pégase in order to reattach them correctly
1125                    if (children.keySet().contains(stringChildId))
1126                    {
1127                        _pegaseApiManager.getObjetsMaquetteApi().retirerEnfant(_structureCode, pegaseParentObjectId, childId);
1128                    }
1129                    // If trust ametys is selected, detach children attached in Pégase which are not attached in Ametys
1130                    else if (_trustAmetys)
1131                    {
1132                        try
1133                        {
1134                            _pegaseApiManager.getObjetsMaquetteApi().retirerEnfant(_structureCode, pegaseParentObjectId, childId);
1135                        }
1136                        catch (ApiException e)
1137                        {
1138                            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.API_ERROR, content);
1139                            getLogger().warn("L'enfant de code Pégase '{}' de l'élément '{}' n'a pas pu être détaché dans Pégase", child.getId(), content.getTitle(), e);
1140                        }
1141                    }
1142                }
1143            }
1144        }
1145        catch (ApiException e)
1146        {
1147            report.updateExportReport(ExportStatus.WARN, ProblemTypes.API_ERROR, content);
1148 
1149            getLogger().warn("Les liens avec les enfants de l'élément '{}' n'ont pas pu être traités car les enfants depuis Pégase n'ont pas pu être récupérés.", content.getTitle(), e);
1150        }
1151        catch (Exception e)
1152        {
1153            throw new PegaseExportException("Erreur lors de l'attachement des enfants de l'élément " + content.getTitle() + ".", e);
1154        }
1155    }
1156    
1157    private void _attachAllChildren(Content content, UUID pegaseParentId, Map<String, PegaseChildWithChildren> children, ExportReport report) throws UnknownDataException, AmetysRepositoryException, PegaseExportException, IOException
1158    {
1159        // If there are children that need to be attached
1160        if (!children.isEmpty())
1161        {
1162            // For each child, attach its own children recursively
1163            for (PegaseChildWithChildren childToAttach : children.values())
1164            {
1165                ChildrenToAttachForObjectRequest childrenToAttach = childToAttach.childrenToAttachRequest();
1166                _attachAllChildren(childrenToAttach.parentContent(), childrenToAttach.parentId(), childrenToAttach.children(), report);
1167            }
1168            
1169            for (PegaseChildWithChildren child : children.values())
1170            {
1171                Enfant pegaseChildToAttach = child.pegaseChild();
1172                try
1173                {
1174                    _changeMutualiseIfNecessary(UUID.fromString(pegaseChildToAttach.getId()), pegaseParentId);
1175                
1176                    // Try to attach them
1177                    _pegaseApiManager.getObjetsMaquetteApi().ajouterEnfant(_structureCode, pegaseParentId, pegaseChildToAttach);
1178                }
1179                catch (ApiException ex)
1180                {
1181                    report.updateExportReport(ExportStatus.WARN, ProblemTypes.LINKS_MISSING, content);
1182                    
1183                    getLogger().warn("Une erreur est survenue lors de l'attachement de l'enfant ({}) à l'élément ({}) dans Pégase", pegaseChildToAttach.getId(), content.getTitle(), ex);
1184                }
1185            }
1186        }
1187    }
1188    
1189    private ObjetMaquetteAndEtagIfExists _changeMutualiseIfNecessary(UUID childId, UUID parentID) throws IOException, ApiException
1190    {
1191        ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
1192        ObjetMaquetteDetail childObjetMaquetteDetail = objetsMaquetteApi.lireObjetMaquette(_structureCode, childId);
1193        String etag = _getEtag(objetsMaquetteApi);
1194        ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists = new ObjetMaquetteAndEtagIfExists(childObjetMaquetteDetail, etag);
1195        
1196        List<Contexte> contextes = childObjetMaquetteDetail.getContextes();
1197        
1198        // If the object has no parents (orphan), it does not need to become mutualised, and if it has multiple parents, it is already mutualised
1199        // If the object has exactly one parent
1200        if (!childObjetMaquetteDetail.getMutualise() && contextes.size() == 1)
1201        {
1202            List<UUID> chemins = contextes.get(0).getChemin();
1203            int numberOfChemins = chemins.size();
1204            
1205            // If the direct parent is not the program we are attaching it to, set mutualise to true
1206            // If there is only one element in chemins, then it is the child itself
1207            if (numberOfChemins > 1 && !parentID.equals(chemins.get(numberOfChemins - 1)))
1208            {
1209                return _makeHierarchyMutualised(objetMaquetteAndEtagIfExists);
1210            }
1211        }
1212        
1213        return objetMaquetteAndEtagIfExists;
1214    }
1215    
1216    private ObjetMaquetteAndEtagIfExists _makeHierarchyMutualised(ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists) throws ApiException, IOException
1217    {
1218        ObjetMaquetteDetail objetMaquetteDetail = objetMaquetteAndEtagIfExists.objetMaquetteDetail();
1219        
1220        ObjetsMaquetteApi objetsMaquetteApi = null;
1221
1222        // If there are any, make the children mutualise
1223        if (objetMaquetteDetail.getEnfants().size() != 0)
1224        {
1225            MaquetteStructure structureMaquetteParent = _pegaseApiManager.getMaquettesApi().lireStructureMaquette(_structureCode, objetMaquetteDetail.getId());
1226            
1227            objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
1228            ObjetMaquetteStructure racine = structureMaquetteParent.getRacine();
1229            
1230            List<EnfantsStructure> children = racine.getEnfants();
1231            
1232            for (EnfantsStructure child : children)
1233            {
1234                // Need to retrieve the objet maquette again to get the Etag
1235                ObjetMaquetteDetail childObjetMaquette = objetsMaquetteApi.lireObjetMaquette(_structureCode, child.getObjetMaquette().getId());
1236                // Make the child hierarchy mutualise recusively
1237                _makeHierarchyMutualised(new ObjetMaquetteAndEtagIfExists(childObjetMaquette, _getEtag(objetsMaquetteApi)));
1238            }
1239        }
1240        else
1241        {
1242            // Retrieve the API again to reset the "external" attribute to false
1243            objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
1244        }
1245        
1246        objetMaquetteDetail = objetsMaquetteApi.rendreMutualise(_structureCode, objetMaquetteDetail.getId(), objetMaquetteAndEtagIfExists.etag());
1247        
1248        return new ObjetMaquetteAndEtagIfExists(objetMaquetteDetail, _getEtag(objetsMaquetteApi));
1249    }
1250    
1251    private ObjetMaquetteDetail _updateObjetFormation(ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists, Content content, String type, boolean parentMutualise, ExportReport report) throws PegaseExportException, IOException
1252    {
1253        DescripteursObjetFormationRequest descripteursObjetFormationRequest = new DescripteursObjetFormationRequest();
1254        
1255        String label = content.getTitle();
1256        descripteursObjetFormationRequest.setLibelle(StringUtils.truncate(label, 50));
1257        descripteursObjetFormationRequest.setLibelleLong(StringUtils.truncate(label, 150));
1258        
1259        // Set the formation type (ANNEE, SEMESTRE, UE...)
1260        descripteursObjetFormationRequest.setType(type);
1261        
1262        descripteursObjetFormationRequest.setEcts(_getEctsFromContent(content));
1263        
1264        return _updateObjetMaquette(objetMaquetteAndEtagIfExists, content, parentMutualise, descripteursObjetFormationRequest, report);
1265    }
1266    
1267    private ObjetMaquetteDetail _updateObjetMaquette(ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExists, Content content, boolean parentMutualise, DescripteursObjetMaquetteRequest descObjetMaquette, ExportReport report) throws PegaseExportException, IOException
1268    {
1269        try
1270        {
1271            ObjetMaquetteAndEtagIfExists objetMaquetteAndEtagIfExistsMutualised = objetMaquetteAndEtagIfExists;
1272            // If the parent is mutualized, make the object mutualized too, and retrieve the new Etag and object from the update
1273            if (parentMutualise)
1274            {
1275                objetMaquetteAndEtagIfExistsMutualised = _makeHierarchyMutualised(objetMaquetteAndEtagIfExists);
1276            }
1277            
1278            ObjetMaquetteDetail objetMaquetteDetailFound = objetMaquetteAndEtagIfExistsMutualised.objetMaquetteDetail();
1279            ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
1280            
1281            ObjetMaquetteDetail objetMaquetteDetail = objetsMaquetteApi.modifierDescripteursObjetMaquette(_structureCode, objetMaquetteDetailFound.getId(), objetMaquetteAndEtagIfExistsMutualised.etag(), descObjetMaquette);
1282            
1283            return objetMaquetteDetail;
1284        }
1285        catch (ApiException e)
1286        {
1287            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.ELEMENT_NOT_EXPORTED);
1288
1289            throw new PegaseExportException("L'élément " + content.getTitle() + " n'a pas pu être exportée car sa modification dans Pégase a posé un problème", e);
1290        }
1291    }
1292    
1293    private ObjetMaquetteDetail _createObjetFormation(Content content, String type, boolean parentMutualise, ExportReport report) throws PegaseExportException
1294    {
1295        DescripteursObjetFormationRequest descripteursObjetFormationRequest = new DescripteursObjetFormationRequest();
1296        descripteursObjetFormationRequest.setType(type);
1297        
1298        descripteursObjetFormationRequest.setEcts(_getEctsFromContent(content));
1299        
1300        CreerObjetFormationRequest creerObjetFormationRequest = new CreerObjetFormationRequest();
1301        
1302        return _createObjetMaquette(content, TypeObjetMaquette.OBJET_FORMATION, parentMutualise, descripteursObjetFormationRequest, creerObjetFormationRequest, report);
1303    }
1304    
1305    private ObjetMaquetteDetail _createObjetMaquette(Content content, TypeObjetMaquette typeObjetMaquette, boolean parentMutualise, DescripteursObjetMaquetteRequest descripteursObjetMaquetteRequest, CreerObjetMaquetteRequest creerObjetMaquetteRequest, ExportReport report) throws PegaseExportException
1306    {
1307        try
1308        {
1309            ObjetsMaquetteApi objetsMaquetteApi = _pegaseApiManager.getObjetsMaquetteApi();
1310            
1311            _initCreerObjetMaquetteRequest(creerObjetMaquetteRequest, typeObjetMaquette, parentMutualise, content);
1312
1313            String label = content.getTitle();
1314            descripteursObjetMaquetteRequest.setLibelle(StringUtils.truncate(label, 50));
1315            descripteursObjetMaquetteRequest.setLibelleLong(StringUtils.truncate(label, 150));
1316            
1317            creerObjetMaquetteRequest.setDescripteursObjetMaquette(descripteursObjetMaquetteRequest);
1318
1319            ObjetMaquetteDetail objetMaquetteDetail = objetsMaquetteApi.creerObjetMaquette(_structureCode, creerObjetMaquetteRequest);
1320            
1321            return objetMaquetteDetail;
1322        }
1323        catch (IOException e)
1324        {
1325            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.API_ERROR);
1326
1327            throw new PegaseExportException("Une erreur est survenue lors de la création de l'objet Pégase", e);
1328        }
1329        catch (ApiException e)
1330        {
1331            report.updateExportReport(ExportStatus.ERROR, ProblemTypes.ELEMENT_NOT_EXPORTED);
1332
1333            throw new PegaseExportException("L'élément " + content.getTitle() + " n'a pas pu être exporté dû à un problème rencontré avec Pégase", e);
1334        }
1335    }
1336    
1337    private PlageDeChoix _buildPlageDeChoix(CourseList courseList)
1338    {
1339        PlageDeChoix plageDeChoix = new PlageDeChoix();
1340        
1341        Long min = courseList.getValue(CourseList.MIN_COURSES, false, 0L);
1342        plageDeChoix.setMin(min.intValue());
1343        
1344        Long max = courseList.getValue(CourseList.MAX_COURSES, false, min);
1345        plageDeChoix.setMax(max.intValue());
1346        
1347        return plageDeChoix;
1348    }
1349    
1350    /**
1351     * If the objet exists, store the objetMaquette detail and its Etag
1352     * @param objetMaquetteDetail The objet maquette detail
1353     * @param etag The etag of the object (its version)
1354     */
1355    protected record ObjetMaquetteAndEtagIfExists (ObjetMaquetteDetail objetMaquetteDetail, String etag) { /* empty */ }
1356    
1357    /**
1358     * Store the parameters necessary to attach all children of an object
1359     * @param parentContent The content of the object
1360     * @param parentId The pegase ID of the object
1361     * @param children The children of the object
1362     */
1363    protected record ChildrenToAttachForObjectRequest (Content parentContent, UUID parentId, Map<String, PegaseChildWithChildren> children) { /* empty */ }
1364    
1365    /**
1366     * Store the Pegase child and its children to attach request
1367     * @param pegaseChild The pegase child
1368     * @param childrenToAttachRequest The children to attach request parameters as a {@link ChildrenToAttachForObjectRequest} object
1369     */
1370    protected record PegaseChildWithChildren (Enfant pegaseChild, ChildrenToAttachForObjectRequest childrenToAttachRequest) { /* empty */ }
1371}