001/*
002 *  Copyright 2018 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 */
016package org.ametys.plugins.odfsync.cdmfr.components.impl;
017
018import java.text.Normalizer;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import javax.jcr.RepositoryException;
028
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.commons.collections4.CollectionUtils;
034import org.apache.commons.collections4.ListUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.slf4j.Logger;
037import org.w3c.dom.Document;
038import org.w3c.dom.Node;
039import org.w3c.dom.NodeList;
040
041import org.ametys.cms.content.external.ExternalizableMetadataHelper;
042import org.ametys.cms.contenttype.ContentType;
043import org.ametys.cms.contenttype.MetadataDefinition;
044import org.ametys.cms.contenttype.MetadataType;
045import org.ametys.cms.contenttype.RepeaterDefinition;
046import org.ametys.cms.repository.ContentQueryHelper;
047import org.ametys.cms.repository.ContentTypeExpression;
048import org.ametys.cms.repository.LanguageExpression;
049import org.ametys.cms.repository.ModifiableDefaultContent;
050import org.ametys.odf.ProgramItem;
051import org.ametys.odf.enumeration.OdfReferenceTableEntry;
052import org.ametys.odf.enumeration.OdfReferenceTableHelper;
053import org.ametys.odf.helper.DeleteODFContentHelper;
054import org.ametys.odf.helper.DeleteODFContentHelper.DeleteMode;
055import org.ametys.odf.program.AbstractProgram;
056import org.ametys.odf.program.Program;
057import org.ametys.odf.program.ProgramFactory;
058import org.ametys.odf.program.ProgramPart;
059import org.ametys.odf.program.SubProgram;
060import org.ametys.odf.program.SubProgramFactory;
061import org.ametys.odf.program.TraversableProgramPart;
062import org.ametys.plugins.odfsync.cdmfr.MergeMetadataForSharedProgramHelper;
063import org.ametys.plugins.odfsync.cdmfr.RemoteCDMFrSynchronizableContentsCollection;
064import org.ametys.plugins.repository.AmetysObjectIterable;
065import org.ametys.plugins.repository.RepositoryConstants;
066import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
067import org.ametys.plugins.repository.query.expression.AndExpression;
068import org.ametys.plugins.repository.query.expression.Expression;
069import org.ametys.plugins.repository.query.expression.Expression.Operator;
070import org.ametys.plugins.repository.query.expression.StringExpression;
071
072
073/**
074 * Component to import a CDM-fr input stream from a remote server with co-accredited mode.
075 */
076public class CoAccreditedRemoteImportCDMFrComponent extends RemoteImportCDMFrComponent
077{
078    /** The name of the JCR node holding the shared metadata  */
079    public static final String SHARED_PROGRAMS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":shared-programs";
080
081    /** The merge metadata helper */
082    protected MergeMetadataForSharedProgramHelper _mergeMetadataHelper;
083    
084    /** The delete ODF content helper */
085    protected DeleteODFContentHelper _deleteODFContent;
086    
087    /** The list of metadata to copy for mention program */
088    protected Set<String> _mentionMetadataPaths;
089    
090    /** The list of metadata to merge */
091    protected Set<String> _metadataPathsToMerge;
092    
093    private ContentType _mentionContentType;
094    private String _mentionId;
095    private String _programToLinkCode;
096    private String _sharedSubProgramType;
097    
098    /**
099     * Enum to define the way to detect shared subProgram
100     */
101    public enum SharedWithType
102    {
103        /**
104         * The main subProgram set the other shared program in the metadata "shared-with"
105         */
106        WITH_SHARED_METADATA,
107       
108        /**
109         * All subProgram with the same title are shared. The first imported subProgram is the main subProgram.
110         */
111        WITH_SAME_TITLE,
112        
113        
114        /**
115         * Shared subProgram are not handled
116         */
117        NONE
118    }
119    
120    @Override
121    public void service(ServiceManager manager) throws ServiceException
122    {
123        super.service(manager);
124        _mergeMetadataHelper = (MergeMetadataForSharedProgramHelper) manager.lookup(MergeMetadataForSharedProgramHelper.ROLE);
125        _deleteODFContent = (DeleteODFContentHelper) manager.lookup(DeleteODFContentHelper.ROLE);
126    }
127    
128    @Override
129    public void initialize() throws Exception
130    {
131        super.initialize();
132        _mentionContentType = _contentTypeEP.getExtension(getProgramWfDescription().getContentType());
133    }
134    
135    @Override
136    public void configure(Configuration configuration) throws ConfigurationException
137    {
138        super.configure(configuration);
139
140        _mentionMetadataPaths = new HashSet<>();
141        _metadataPathsToMerge = new HashSet<>();
142        if ("co-accredited".equals(configuration.getName()))
143        {
144            _configureCoAccreditedParams(configuration);
145        }
146        else
147        {
148            _configureCoAccreditedParams(configuration.getChild("co-accredited"));
149        }
150    }
151    
152    /**
153     * Configure the co-accredited params
154     * @param configuration the configuration
155     * @throws ConfigurationException if an error occurred
156     */
157    protected void _configureCoAccreditedParams(Configuration configuration) throws ConfigurationException
158    {
159        Configuration mentionConf = configuration.getChild("mention");
160        if (mentionConf != null)
161        {
162            Configuration metadatas = mentionConf.getChild("metadata-to-copy");
163            if (metadatas != null)
164            {
165                for (Configuration metadataConf : metadatas.getChildren())
166                {
167                    String metadataPath = metadataConf.getAttribute("path");
168                    _mentionMetadataPaths.add(metadataPath);
169                }
170            }
171        }
172        
173        Configuration sharedWithConf = configuration.getChild("shared-with");
174        if (sharedWithConf != null)
175        {
176            Configuration metadatas = sharedWithConf.getChild("metadata-to-merge");
177            if (metadatas != null)
178            {
179                for (Configuration metadataConf : metadatas.getChildren())
180                {
181                    String metadataPath = metadataConf.getAttribute("path");
182                    _metadataPathsToMerge.add(metadataPath);
183                }
184            }
185        }
186    }
187
188    @Override
189    protected void additionalParameters(Map<String, Object> parameters)
190    {
191        _mentionId = null;
192        _sharedSubProgramType = (String) parameters.getOrDefault(RemoteCDMFrSynchronizableContentsCollection.PARAM_SHARED_WITH_TYPE, SharedWithType.NONE.name());
193        
194        super.additionalParameters(parameters);
195    }
196
197    @Override
198    protected ModifiableDefaultContent _importOrSynchronizeContent(Document doc, Node contentNode, ContentWorkflowDescription wfDescription, String title, String lang, String catalog, String syncCode, Logger logger)
199    {
200        ModifiableDefaultContent content = null;
201        ContentWorkflowDescription contentWfDescription = wfDescription;
202        if (contentNode.getLocalName().equals(_TAG_PROGRAM))
203        {
204            String educationKind = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.EDUCATION_KIND);
205            String mention = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.MENTION);
206
207            // This is a co-accredited program
208            if (StringUtils.isNotBlank(mention) && "parcours".equals(educationKind))
209            {
210                // Change the workflow description of the program to subprogram
211                contentWfDescription = getSubProgramWfDescription();
212
213                // Get or create the mention from the program
214                Program mentionProgram = _getOrCreateMention(doc, contentNode, mention, lang, catalog, logger);
215                _mentionId = mentionProgram.getId();
216                
217                // Store the content to link to the mention
218                _programToLinkCode = syncCode;
219                
220                // Handle shared subPrograms
221                if (_getSharedWithType() != SharedWithType.NONE)
222                {
223                    content = _importOrSynchronizeSharedSubPrograms(doc, mentionProgram, contentNode, contentWfDescription, title, syncCode, lang, catalog, logger);
224                }
225            }
226        }
227
228        if (content == null)
229        {
230            content = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger);
231        }
232        
233        if (_mentionId != null && content instanceof SubProgram && content.getMetadataHolder().getString(getIdField()).equals(_programToLinkCode))
234        {
235            // Mettre les composantes de la mention à jour
236            Program mention = _resolver.resolveById(_mentionId);
237            _updateMentionAttributes(mention, (SubProgram) content);
238        }
239        
240        return content;
241    }
242    
243    /**
244     * Update mention attributes depending on the sub program
245     * @param mention the mention
246     * @param subProgram the subProgram
247     */
248    protected void _updateMentionAttributes(Program mention, SubProgram subProgram)
249    {
250        // Mettre les composantes de la mention à jour
251        List<String> orgUnits = mention.getOrgUnits();
252        List<String> orgUnitsToAdd = subProgram.getOrgUnits();
253        _updateContentsAttribute(mention, AbstractProgram.ORG_UNITS_REFERENCES, orgUnits, orgUnitsToAdd);
254    }
255    
256    /**
257     * For the attribute name of the content, add new contents (contentIdsToAdd) to existed contents (contentIds)
258     * @param content the content to modified
259     * @param attributeName the attribute name
260     * @param contentIds the content ids
261     * @param contentIdsToAdd the content ids to add
262     */
263    protected void _updateContentsAttribute(ModifiableDefaultContent content, String attributeName, List<String> contentIds, List<String> contentIdsToAdd)
264    {
265        if (!CollectionUtils.containsAll(contentIds, contentIdsToAdd))
266        {
267            List<String> newContentIds = ListUtils.union(contentIds, contentIdsToAdd);
268            ExternalizableMetadataHelper.setExternalMetadata(content.getMetadataHolder(), attributeName, newContentIds.toArray(new String[newContentIds.size()]), false);
269        }
270    }
271    
272    /**
273     * Import or synchronized shared subPrograms
274     * @param doc the document
275     * @param mentionProgram the mention program
276     * @param contentNode the content node
277     * @param contentWfDescription the content workflow description
278     * @param title the title of the subprogram
279     * @param syncCode the synchronisation code
280     * @param lang the language 
281     * @param catalog the catalog
282     * @param logger the logger
283     * @return the imported of synchronized content
284     */
285    protected ModifiableDefaultContent _importOrSynchronizeSharedSubPrograms(Document doc, Program mentionProgram, Node contentNode, ContentWorkflowDescription contentWfDescription, String title, String syncCode, String lang, String catalog, Logger logger)
286    {
287        // Is imported program already exist as a subprogram ?
288        ModifiableDefaultContent subProgram = _getContent(lang, catalog, syncCode, contentWfDescription);
289        if (subProgram == null)
290        {
291            // The imported program does not exist as subprogram, create it except if it is a shared subprogram
292            if (_isSecondarySharedSubPrograms(mentionProgram, contentNode, lang, logger))
293            {
294                // Search if there is a main subprogram shared with it.
295                SubProgram mainSubProgram = getMainSharedSubProgram(mentionProgram, contentNode, lang, logger);
296                if (mainSubProgram != null)
297                {
298                    // The imported program is a shared subprogram, it will not be imported nor synchronized, only the main subprogram will be updated
299                    try
300                    {
301                        // Merge shared program metadata from CDMfr
302                        if (_synchronizeSharedMetadata(doc, contentNode, mainSubProgram, null, _metadataPathsToMerge, catalog, lang, logger))
303                        {
304                            if (_mergeMetadataHelper.mergeSharedMetadata(mainSubProgram, contentNode, _metadataPathsToMerge, logger))
305                            {
306                                _saveContentChanges(mainSubProgram, null, true, logger);
307                            }
308                        }
309                    }
310                    catch (RepositoryException e)
311                    {
312                        String warnMsg = "Impossible de synchronizer les métadonnées partagées pour le parcours principal \"" + mainSubProgram.getTitle() + "\"";
313                        logger.warn(warnMsg);
314                    }
315                    
316                    return mainSubProgram;
317                }
318                else
319                {
320                    return super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger);
321                }
322            }
323            else
324            {
325                subProgram = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger);
326                
327                try
328                {
329                    // Synchronize the shared metadata of the main program to be merged later
330                    _synchronizeSharedMetadata(doc, contentNode, (SubProgram) subProgram, null, _metadataPathsToMerge, catalog, lang, logger);
331                }
332                catch (RepositoryException e)
333                {
334                    String warnMsg = "Impossible de synchronizer les métadonnées partagées du parcours principal \"" + subProgram.getTitle() + "\"";
335                    logger.warn(warnMsg);
336                }
337                
338                // Browse existing shared subprograms to first merge them then delete them. 
339                // When we shared subprograms with the title, it can't have other created shared subPrograms because the first one is the main shared subprogram 
340                SharedWithType sharedWithType = _getSharedWithType();
341                if (SharedWithType.WITH_SHARED_METADATA == sharedWithType)
342                {
343                    List<String> sharedWith = _getSharedWithAsString(contentNode, logger);
344                    _synchronizeAndDeleteSharedSubPrograms(mentionProgram, (SubProgram) subProgram, sharedWith, catalog, lang, logger);
345                    
346                    try
347                    {
348                        // Then merge main subprogram
349                        if (_mergeMetadataHelper.mergeSharedMetadata((SubProgram) subProgram, contentNode, _metadataPathsToMerge, logger))
350                        {
351                            _saveContentChanges(subProgram, null, true, logger);
352                        }
353                    }
354                    catch (RepositoryException e)
355                    {
356                        String warnMsg = "Impossible de synchronizer les metadatas du programme principal \"" + subProgram.getTitle() + "\"";
357                        logger.warn(warnMsg);
358                    }
359                }
360                
361                return subProgram;
362            }
363        }
364        else // The imported program exists as subprogram
365        {
366            ModifiableDefaultContent mainSubProgram = super._importOrSynchronizeContent(doc, contentNode, contentWfDescription, title, lang, catalog, syncCode, logger);
367            
368            if (!_isSecondarySharedSubPrograms(mentionProgram, contentNode, lang, logger))
369            {
370                // When we shared subprograms with the title, it can't have other created shared subPrograms because the first one is the main shared subprogram 
371                SharedWithType sharedWithType = _getSharedWithType();
372                if (SharedWithType.WITH_SHARED_METADATA == sharedWithType)
373                {
374                    List<String> sharedWith = _getSharedWithAsString(contentNode, logger);
375
376                    // Browse existing shared subprograms to first merge them then delete them.
377                    _synchronizeAndDeleteSharedSubPrograms(mentionProgram, (SubProgram) subProgram, sharedWith, catalog, lang, logger);
378                }
379                
380                try
381                {
382                    // Synchronize the shared metadata of the main program to be merged later
383                    _synchronizeSharedMetadata(doc, contentNode, (SubProgram) subProgram, null, _metadataPathsToMerge, catalog, lang, logger);
384                    // Then merge main subprogram
385                    if (_mergeMetadataHelper.mergeSharedMetadata((SubProgram) subProgram, contentNode, _metadataPathsToMerge, logger))
386                    {
387                        _saveContentChanges(mainSubProgram, null, true, logger);
388                    }
389                }
390                catch (RepositoryException e)
391                {
392                    String warnMsg = "Impossible de synchronizer les metadatas du programme principal \"" + subProgram.getTitle() + "\"";
393                    logger.warn(warnMsg);
394                }
395            }
396            
397            return mainSubProgram;
398        }
399    }
400    
401    /**
402     * Get of create the mention program
403     * @param doc the document
404     * @param contentNode the content node
405     * @param mentionCode the mention code
406     * @param lang the language
407     * @param catalog the catalog
408     * @param logger the logger
409     * @return the mention program
410     */
411    protected Program _getOrCreateMention(Document doc, Node contentNode, String mentionCode, String lang, String catalog, Logger logger)
412    {
413        String degreeCodeCDM = _xPathProcessor.evaluateAsString(contentNode, AbstractProgram.DEGREE);
414        OdfReferenceTableEntry degreeContent = _odfRefTableHelper.getItemFromCDM(OdfReferenceTableHelper.DEGREE, degreeCodeCDM);
415        String degreeCode = degreeContent != null ? degreeContent.getCode() : degreeCodeCDM;
416        String mentionType = degreeContent != null ? _odfRefTableHelper.getMentionForDegree(degreeContent.getId()) : null;
417        if (mentionType != null)
418        {
419            String mentionId = _getIdFromCDMThenCode(mentionType, mentionCode);
420            if (mentionId != null)
421            {
422                String degreeId = _getIdFromCDMThenCode(OdfReferenceTableHelper.DEGREE, degreeCode);
423                Program mention = _getMention(mentionId, degreeId, lang, catalog);
424                if (mention == null)
425                {
426                    mention = _createMention(doc, contentNode, mentionId, catalog, lang, logger);
427                    _importedContents.put(mention.getId(), getProgramWfDescription().getValidationActionId());
428                }
429                
430                return mention;
431            }
432            else
433            {
434                logger.error("Il n'y a pas de code associé à la mention {}. La formation n'a pas été importée.", mentionCode);
435                _nbError++;
436            }
437        }
438        else
439        {
440            logger.error("Il n'y a pas de type de mention (licence, licence pro, master) associée au diplôme {}. La formation n'a pas été importée.", degreeCode);
441            _nbError++;
442        }
443        
444        return null;
445    }
446    
447    /**
448     * Create the mention
449     * @param doc the document
450     * @param contentNode the content node
451     * @param mentionId the mention id
452     * @param catalog the catalog
453     * @param lang the language
454     * @param logger the logger
455     * @return the created mention
456     */
457    protected Program _createMention(Document doc, Node contentNode, String mentionId, String catalog, String lang, Logger logger)
458    {
459        String contentTitle = _odfRefTableHelper.getItemLabel(mentionId, lang);
460        ContentWorkflowDescription wfDescription = getProgramWfDescription();
461        Map<String, Object> resultMap = _synchroComponent.createContentAction(wfDescription.getContentType(), wfDescription.getWorkflowName(), wfDescription.getInitialActionId(), lang, contentTitle, _contentPrefix, logger);
462        if ((boolean) resultMap.getOrDefault("error", false))
463        {
464            _nbError++;
465        }
466        
467        Program mention = (Program) resultMap.get("content");
468
469        if (mention != null)
470        {
471            boolean hasChanges = false;
472            if (catalog != null)
473            {
474                hasChanges = ExternalizableMetadataHelper.setMetadata(mention.getMetadataHolder(), ProgramItem.CATALOG, catalog);
475            }
476
477            hasChanges = ExternalizableMetadataHelper.setExternalMetadata(mention.getMetadataHolder(), AbstractProgram.MENTION, mentionId, true) || hasChanges;
478            hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, AbstractProgram.DEGREE, lang, catalog, logger) || hasChanges;
479            hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, AbstractProgram.DOMAIN, lang, catalog, logger) || hasChanges;
480            
481            for (String metadataPath : _mentionMetadataPaths)
482            {
483                hasChanges = _synchronizeMentionMetadata(doc, contentNode, mention, metadataPath, lang, catalog, logger) || hasChanges;
484            }
485            
486            _saveContentChanges(mention, wfDescription.getContentType(), hasChanges, logger);
487        }
488        
489        return mention;
490    }
491    
492    /**
493     * Get the mention program
494     * @param mentionId the mention content id
495     * @param degreeId the degree content id
496     * @param lang the language
497     * @param catalog the catalog
498     * @return the mention program or <code>null</code> if it doesn't exist
499     */
500    protected Program _getMention(String mentionId, String degreeId, String lang, String catalog)
501    {
502        List<Expression> expList = new ArrayList<>();
503        expList.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
504        expList.add(new LanguageExpression(Operator.EQ, lang));
505        expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
506        expList.add(new StringExpression(AbstractProgram.DEGREE, Operator.EQ, degreeId));
507        expList.add(new StringExpression(AbstractProgram.MENTION, Operator.EQ, mentionId));
508
509        AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()]));
510        String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp);
511
512        AmetysObjectIterable<Program> contents = _resolver.query(xPathQuery);
513        
514        if (contents.getSize() > 0)
515        {
516            return contents.iterator().next();
517        }
518        
519        return null;
520    }
521    
522    /**
523     * Synchronize metadata in the mention program
524     * @param doc the document
525     * @param contentNode the content node
526     * @param mention the mention program
527     * @param metadataPath the metadata path to synchronize
528     * @param lang the language
529     * @param catalog the catalog
530     * @param logger the logger
531     * @return true if some changes were made
532     */
533    protected boolean _synchronizeMentionMetadata(Document doc, Node contentNode, ModifiableDefaultContent mention, String metadataPath, String lang, String catalog, Logger logger)
534    {
535        Node metadataNode = _xPathProcessor.selectSingleNode(contentNode, metadataPath);
536        if (metadataNode != null)
537        {
538            return _synchronizeMetadata(doc, metadataNode, mention, metadataPath, metadataPath, _mentionContentType, lang, catalog, logger);
539        }
540        return false;
541    }
542
543    @Override
544    protected void additionalOperationsBeforeSave(ModifiableDefaultContent content, Logger logger) throws RepositoryException
545    {
546        if (_mentionId != null && content instanceof SubProgram && content.getMetadataHolder().getString(getIdField()).equals(_programToLinkCode))
547        {
548            boolean hasChanges = false;
549            
550            ModifiableDefaultContent mentionContent = _resolver.resolveById(_mentionId);
551            
552            // Relier le programme à la mention
553            hasChanges = _synchroComponent.updateRelation(mentionContent.getMetadataHolder(), TraversableProgramPart.CHILD_PROGRAM_PARTS, content, false) || hasChanges;
554            if (_synchroComponent.updateRelation(content.getMetadataHolder(), ProgramPart.PARENT_PROGRAM_PARTS, _mentionId, false))
555            {
556                _saveContentChanges(content, getSubProgramWfDescription().getContentType(), true, logger);
557            }
558
559            if (hasChanges)
560            {
561                _saveContentChanges(mentionContent, _mentionContentType.getId(), hasChanges, logger);
562            }
563        }
564    }
565    
566    /**
567     * Get the defined way to detect shared program
568     * @return shared with type
569     */
570    protected SharedWithType _getSharedWithType()
571    {
572        return SharedWithType.valueOf(_sharedSubProgramType);
573    }
574    
575    /**
576     * Get the main subprogram shared with the program representing by the content node
577     * @param mentionProgram the root mention program
578     * @param contentNode the content node
579     * @param lang the content lang
580     * @param logger the logger
581     * @return the main subprogram if the program is a shared subprogram or <code>null</code> otherwise
582     */
583    protected SubProgram getMainSharedSubProgram(Program mentionProgram, Node contentNode, String lang, Logger logger)
584    {
585        SharedWithType sharedWithType = _getSharedWithType();
586        switch (sharedWithType)
587        {
588            case WITH_SHARED_METADATA:
589                return _getMainSharedWithSubProgram(mentionProgram, contentNode, logger);
590            case WITH_SAME_TITLE:
591                return _getSubProgramWithSameTitle(mentionProgram, contentNode, lang);
592            default:
593                return null;
594        }
595    }
596    
597    /**
598     * True if the subProgram node is shared with a main subProgram.
599     * False if the subProgram node is not shared or if the main subProgram is not already imported
600     * @param mentionProgram the mention program
601     * @param contentNode the content node
602     * @param lang the lang
603     * @param logger the logger
604     * @return true if it's a secondary subProgram
605     */
606    protected boolean _isSecondarySharedSubPrograms(Program mentionProgram, Node contentNode, String lang, Logger logger)
607    {
608        SharedWithType sharedWithType = _getSharedWithType();
609        switch (sharedWithType)
610        {
611            case WITH_SHARED_METADATA:
612                List<String> sharedWith = _getSharedWithAsString(contentNode, logger);
613                return sharedWith.isEmpty();
614            case WITH_SAME_TITLE:
615                return _hasSubProgramWithSameTitle(mentionProgram, contentNode, lang, logger);
616            default:
617                return false;
618        }
619    }
620    
621    /**
622     * True if there is a subProgram with the same title of the content node
623     * @param mentionProgram the mention program
624     * @param contentNode the content node
625     * @param lang the lang
626     * @param logger the logger
627     * @return true if there is a subProgram with the same title of the content node
628     */
629    protected boolean _hasSubProgramWithSameTitle(Program mentionProgram, Node contentNode, String lang, Logger logger)
630    {
631        SubProgram subProgram = _getSubProgramWithSameTitle(mentionProgram, contentNode, lang);
632        return subProgram != null;
633    }
634    
635    
636    /**
637     * Get the subProgram with the same title
638     * @param mentionProgram the mention program
639     * @param contentNode the content node
640     * @param lang the lang
641     * @return the subProgram with the same title. null if there are no subProgram with same title.
642     */
643    protected SubProgram _getSubProgramWithSameTitle(Program mentionProgram, Node contentNode, String lang)
644    {
645        String contentTitle = _xPathProcessor.evaluateAsString(contentNode, "title");
646        
647        for (ProgramPart child : mentionProgram.getProgramPartChildren())
648        {
649            if (child instanceof SubProgram)
650            {
651                SubProgram subProgram  = (SubProgram) child;
652                if (_getNormalizeTitle(subProgram.getTitle()).equals(_getNormalizeTitle(contentTitle)))
653                {
654                    return subProgram;
655                }
656            }
657        }
658        return null;
659    }
660    
661    private String _getNormalizeTitle(String title)
662    {
663        // To lower case
664        // then remove accents
665        return Normalizer.normalize(title.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
666    }
667    
668    /**
669     * Get the list of "shared with" program from the remote document
670     * @param contentNode The content node
671     * @param logger the logger
672     * @return The CDM code of shared program
673     */
674    protected List<String> _getSharedWithAsString (Node contentNode, Logger logger)
675    {
676        List<String> codes = new ArrayList<>();
677        
678        Node sharedWithNode = _xPathProcessor.selectSingleNode(contentNode, AbstractProgram.SHARED_WITH);
679        
680        if (sharedWithNode != null)
681        {
682            NodeList itemNodes = sharedWithNode.getChildNodes();
683            for (int j = 0; j < itemNodes.getLength(); j++)
684            {
685                String code = itemNodes.item(j).getTextContent().trim();
686                if (StringUtils.isNotBlank(code))
687                {
688                    codes.add(code);
689                }
690            }
691        }
692        
693        return codes;
694    }
695    
696    /**
697     * Get the main subprogram shared with the program representing by the content node
698     * @param mentionProgram the root mention program
699     * @param contentNode the content node
700     * @param logger the logger
701     * @return the main subprogram if the program is a shared subprogram or <code>null</code> otherwise
702     */
703    protected SubProgram _getMainSharedWithSubProgram(Program mentionProgram, Node contentNode, Logger logger)
704    {
705        String programCode = _xPathProcessor.evaluateAsString(contentNode, ProgramItem.CODE);
706        
707        for (ProgramPart child : mentionProgram.getProgramPartChildren())
708        {
709            if (child instanceof SubProgram)
710            {
711                SubProgram subProgram  = (SubProgram) child;
712                List<String> sharedProgram = Arrays.asList(subProgram.getSharedWith());
713                if (sharedProgram.contains(programCode))
714                {
715                    return subProgram;
716                }
717            }
718        }
719        return null;
720    }
721    
722    /**
723     * Get the shared subprogram with the given code
724     * @param mentionProgram The root mention program
725     * @param code The CDMfr code
726     * @param logger the logger
727     * @return The shared subprogram if exist or <code>null</code> if not found
728     */
729    protected SubProgram _getSharedSubProgram (Program mentionProgram, String code, Logger logger)
730    {
731        for (ProgramPart child : mentionProgram.getProgramPartChildren())
732        {
733            if (child instanceof SubProgram)
734            {
735                SubProgram subProgram  = (SubProgram) child;
736                if (subProgram.getCode().equals(code))
737                {
738                    return subProgram;
739                }
740            }
741        }
742        return null;
743    }
744    
745    /**
746     * Delete a shared secondary subprogram.
747     * This subprogram was created by a precede import before the main subprogram was imported
748     * @param sharedProgram the shared program
749     * @param logger the logger
750     */
751    protected void deleteSharedSubProgram(ModifiableDefaultContent sharedProgram, Logger logger)
752    {
753        String title = sharedProgram.getTitle();
754        String infoMsg = "Suppression du parcours partagé secondaire \"" + title + "\"";
755        logger.info(infoMsg);
756        
757        String contentId = sharedProgram.getId();
758        List<String> contentIds = Collections.singletonList(contentId);
759        
760        // Delete content bypassing the rights check
761        Map<String, Object> results = _deleteODFContent.deleteContentsWithLog(contentIds, DeleteMode.FULL.name(), true);
762        
763        @SuppressWarnings("unchecked")
764        Map<String, Object> result = (Map<String, Object>) results.get(contentId);
765        if (result.containsKey("check-before-deletion-failed") && (boolean) result.get("check-before-deletion-failed"))
766        {
767            logger.warn("Can't deleted shared content " + title + " (" + contentId + "). See previous logs for more information.");
768        }
769    }
770
771    /**
772     * Synchronize the shared metadata of a shared subprogram before deleting the subprogram
773     * @param rootProgram The root mention
774     * @param mainSubProgram The main subprogram
775     * @param sharedWith The CDMfr of shared subprogram
776     * @param catalog the catalog
777     * @param lang the language
778     * @param logger The logger
779     * @return <code>true</code> if changes were made
780     */
781    protected boolean _synchronizeAndDeleteSharedSubPrograms (Program rootProgram, SubProgram mainSubProgram, List<String> sharedWith, String catalog, String lang, Logger logger)
782    {
783        boolean hasChanged = false;
784        
785        // Browse existing shared subprograms to first merge them then delete them.
786        for (String code : sharedWith)
787        {
788            SubProgram sharedSubProgram = _getSharedSubProgram(rootProgram, code, logger);
789            if (sharedSubProgram != null)
790            {
791                String infoMsg = "Fusion avec le parcours partagé \"" + sharedSubProgram.getTitle() + "\"";
792                logger.info(infoMsg);
793                
794                try
795                {
796                    _synchronizeSharedMetadata(null, null, mainSubProgram, sharedSubProgram, _metadataPathsToMerge, catalog, lang, logger);
797                }
798                catch (RepositoryException e)
799                {
800                    String warnMsg = "Impossible de synchronizer les metadatas du programme partagé \"" + sharedSubProgram.getTitle() + "\" avec le programme principal \"" + mainSubProgram.getTitle() + "\"";
801                    logger.warn(warnMsg);
802                }
803                
804                // Then delete the secondary shared subprogram
805                deleteSharedSubProgram(sharedSubProgram, logger);
806                hasChanged = true;
807            }
808        }
809        
810        return hasChanged;
811    }
812    
813    /**
814     * Synchronize the shared program node for a main subprogram, from a secondary subprogram
815     * @param doc the document. Can be null if the sharedProgram is not
816     * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not
817     * @param mainProgram the main program
818     * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not
819     * @param metadataToMerge the set of metadata to merge
820     * @param catalog the catalog
821     * @param lang the language
822     * @param logger the logger
823     * @return <code>true</code> if changes were made
824     * @throws RepositoryException if an error occurred
825     */
826    public boolean _synchronizeSharedMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, Set<String> metadataToMerge, String catalog, String lang, Logger logger) throws RepositoryException
827    {
828        boolean hasChanged = false;
829        
830        ModifiableCompositeMetadata sharedMetadataHolder = _getSharedMetadataHolder(doc, sharedProgramNode, mainProgram, sharedProgram, logger);
831        for (String metadataPath : metadataToMerge)
832        {
833            ContentType cType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
834            
835            if (_mergeMetadataHelper.isValidMetadataPath(cType, metadataPath, logger))
836            {
837                String[] pathSegments = StringUtils.split(metadataPath, '/');
838                MetadataDefinition metadataDefinition = cType.getMetadataDefinition(pathSegments[0]);
839                
840                if (metadataDefinition.isMultiple())
841                {
842                    hasChanged = _synchronizeSharedMultipleMetadata(doc, sharedProgramNode, mainProgram, sharedProgram, metadataDefinition, sharedMetadataHolder, metadataPath, catalog, lang, logger) || hasChanged;
843                }
844                else
845                {
846                    hasChanged = _synchronizeSharedSingleMetadata(doc, sharedProgramNode, mainProgram, sharedProgram, metadataDefinition, sharedMetadataHolder, metadataPath, catalog, lang, logger) || hasChanged;
847                }
848            }
849        }
850        
851        return hasChanged;
852    }
853
854    /**
855     * Get the metadata composite holding shared metadata of subprograms
856     * @param doc the document. Can be null if the sharedProgram is not
857     * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not
858     * @param mainProgram the main program
859     * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not
860     * @param logger the logger
861     * @return the shared metadata holder
862     * @throws RepositoryException if an error occurred
863     */
864    protected ModifiableCompositeMetadata _getSharedMetadataHolder(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, Logger logger) throws RepositoryException
865    {
866        // The data comes from a content
867        if (doc == null)
868        {
869            return _mergeMetadataHelper.getSharedMetadataHolder(mainProgram, sharedProgram.getCode(), logger);
870        }
871        else // The data comes from the DOM
872        {
873            String sharedProgramCode = _xPathProcessor.evaluateAsString(sharedProgramNode, ProgramItem.CODE);
874            return _mergeMetadataHelper.getSharedMetadataHolder(mainProgram, sharedProgramCode, logger);
875        }
876    }
877    
878    /**
879     * Synchronize a shared multiple metadata for shared program
880     * @param doc the document. Can be null if the sharedProgram is not
881     * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not
882     * @param mainProgram the main program
883     * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not
884     * @param metadataDefinition the metadata definition
885     * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms
886     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
887     * @param catalog the catalog
888     * @param lang the language
889     * @param logger the logger
890     * @return <code>true</code> if changes were made
891     */
892    protected boolean _synchronizeSharedMultipleMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger)
893    {
894        boolean hasChanged = false;
895        ContentType contentType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
896        Map<String, Object> params = Map.of("contentTypes", List.of(contentType.getId()));
897        boolean synchronize = getLocalAndExternalFields(params).contains(logicalMetadataPath);
898        
899        MetadataType type = metadataDefinition.getType();
900        
901        // Data comes from a content
902        List<Object> metadataValues = new ArrayList<>();
903        if (doc == null)
904        {
905            metadataValues = _getMultipleValuesFromContent(sharedProgram, logicalMetadataPath, type, logger);
906        }
907        else
908        {
909            metadataValues = _getMultipleValuesFromDOM(doc, sharedProgramNode, mainProgram, metadataDefinition, logicalMetadataPath, type, catalog, lang, logger);
910        }
911        
912        
913        Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(mainProgram, contentType, logicalMetadataPath, sharedMetadataHolder, logicalMetadataPath, metadataValues, synchronize, false, logger);
914        if (resultMap.getOrDefault("error", Boolean.FALSE))
915        {
916            _nbError++;
917        }
918        hasChanged = resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue();
919        
920        return hasChanged;
921    }
922
923    /**
924     * Get multiple values from content 
925     * @param sharedProgram the shared program
926     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
927     * @param type the type of the data to retrieve
928     * @param logger the logger
929     * @return the list of values
930     */
931    protected List<Object> _getMultipleValuesFromContent(SubProgram sharedProgram, String logicalMetadataPath, MetadataType type, Logger logger)
932    {
933        List<Object> metadataValues = new ArrayList<>();
934        ModifiableCompositeMetadata metadataHolder = sharedProgram.getMetadataHolder();
935        switch (type)
936        {
937            case STRING:
938            case REFERENCE:
939            case CONTENT:
940                for (String value : metadataHolder.getStringArray(logicalMetadataPath, new String[0]))
941                {
942                    metadataValues.add(value);
943                }
944                break;
945            case DOUBLE:
946                for (double value : metadataHolder.getDoubleArray(logicalMetadataPath, new double[0]))
947                {
948                    metadataValues.add(value);
949                }
950                break;
951            case LONG:
952                for (long value : metadataHolder.getLongArray(logicalMetadataPath, new long[0]))
953                {
954                    metadataValues.add(value);
955                }
956                break;
957            default:
958                String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée.";
959                logger.warn(warn);
960                break;    
961        }
962        
963        return metadataValues;
964    }
965    
966    /**
967     * Get multiple values from DOM 
968     * @param doc the document
969     * @param sharedProgramDOM the shared program node
970     * @param mainProgram the main program
971     * @param metadataDefinition the metadata definition
972     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
973     * @param type the type of the data to retrieve
974     * @param catalog the catalog
975     * @param lang the lang
976     * @param logger the logger
977     * @return the list of values
978     */
979    protected List<Object> _getMultipleValuesFromDOM (Document doc, Node sharedProgramDOM, SubProgram mainProgram, MetadataDefinition metadataDefinition, String logicalMetadataPath, MetadataType type, String catalog, String lang, Logger logger)
980    {
981        List<String> metadataValues = new ArrayList<>();
982        
983        Node metadataNode = _xPathProcessor.selectSingleNode(sharedProgramDOM, logicalMetadataPath);
984        if (metadataNode != null)
985        {
986            switch (type)
987            {
988                case STRING:
989                case REFERENCE:
990                case CONTENT:
991                    NodeList itemNodes = metadataNode.getChildNodes();
992                    for (int j = 0; j < itemNodes.getLength(); j++)
993                    {
994                        String metadataValue = itemNodes.item(j).getTextContent().trim();
995                        if (StringUtils.isNotEmpty(metadataValue))
996                        {
997                            metadataValues.add(metadataValue);
998                        }
999                    }
1000                    break;
1001                default:
1002                    String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée.";
1003                    logger.warn(warn);
1004            }
1005        }
1006        
1007        return _handleMetadataValues(doc, mainProgram, metadataDefinition, metadataValues, lang, catalog, logger);
1008    }
1009    
1010    /**
1011     * Synchronize single metadata for shared program
1012     * @param doc the document. Can be null if the sharedProgram is not
1013     * @param sharedProgramNode the shared program node. Can be null if the sharedProgram is not
1014     * @param mainProgram the main program
1015     * @param sharedProgram the shared program. Can be null if the doc and the sharedProgramNode is not
1016     * @param metadataDefinition the metadata definition
1017     * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms
1018     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
1019     * @param catalog the catalog
1020     * @param lang the language
1021     * @param logger the logger
1022     * @return <code>true</code> if changes were made
1023     */
1024    protected boolean _synchronizeSharedSingleMetadata(Document doc, Node sharedProgramNode, SubProgram mainProgram, SubProgram sharedProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger)
1025    {
1026        MetadataType type = metadataDefinition.getType();
1027        switch (type)
1028        {
1029            case COMPOSITE:
1030                if (metadataDefinition instanceof RepeaterDefinition)
1031                {
1032                    if (doc == null)
1033                    {
1034                        return synchronizeRepeaterMetadataFromContent(sharedProgram, sharedMetadataHolder, logicalMetadataPath, logger);
1035                    }
1036                    else
1037                    {
1038                        return synchronizeRepeaterMetadataFromDOM(doc, sharedProgramNode, mainProgram, metadataDefinition, sharedMetadataHolder, logicalMetadataPath, catalog, lang, logger);
1039                    }
1040                }
1041                else
1042                {
1043                    String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée.";
1044                    logger.warn(warn);
1045                    break;    
1046                }
1047            default:
1048                String warn = "Le type de métadonnée " + type.toString() + " n'est pas supporté pour la fusion des parcours partagées. La métadonnée '" + logicalMetadataPath + "' est ignorée.";
1049                logger.warn(warn);
1050                break;
1051        }
1052        return false;
1053    }
1054  
1055    /**
1056     * Synchronize repeater from content
1057     * @param sharedProgram the shared program
1058     * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms
1059     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
1060     * @param logger the logger
1061     * @return <code>true</code> if changes were made
1062     */
1063    protected boolean synchronizeRepeaterMetadataFromContent(SubProgram sharedProgram, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, Logger logger)
1064    {
1065        if (sharedMetadataHolder.hasMetadata(logicalMetadataPath))
1066        {
1067            // Remove old values
1068            sharedMetadataHolder.removeMetadata(logicalMetadataPath);
1069        }
1070        
1071        if (sharedProgram.getMetadataHolder().hasMetadata(logicalMetadataPath))
1072        {
1073            ModifiableCompositeMetadata sharedRepeater = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true);
1074            ModifiableCompositeMetadata repeaterToCopy = sharedProgram.getMetadataHolder().getCompositeMetadata(logicalMetadataPath);
1075            repeaterToCopy.copyTo(sharedRepeater);
1076        }
1077        
1078        return true;
1079    }
1080
1081    /**
1082     * Synchronize repeater from DOM
1083     * @param doc the document
1084     * @param sharedProgramNode the shared program node
1085     * @param mainProgram the main program
1086     * @param metadataDefinition the metadata definition
1087     * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms
1088     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
1089     * @param catalog the catalog
1090     * @param lang the language
1091     * @param logger the logger
1092     * @return <code>true</code> if changes were made
1093     */
1094    protected boolean synchronizeRepeaterMetadataFromDOM(Document doc, Node sharedProgramNode, SubProgram mainProgram, MetadataDefinition metadataDefinition, ModifiableCompositeMetadata sharedMetadataHolder, String logicalMetadataPath, String catalog, String lang, Logger logger)
1095    {
1096        boolean hasChanges = false;
1097        Node metadataNode = _xPathProcessor.selectSingleNode(sharedProgramNode, logicalMetadataPath);
1098        if (metadataNode != null)
1099        {
1100            ModifiableCompositeMetadata repeater = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true);
1101            
1102            // Remove locale entries (unable to compare locales and remotes ...)
1103            String[] metadataNames = repeater.getMetadataNames();
1104            for (String entryName : metadataNames)
1105            {
1106                repeater.removeMetadata(entryName);
1107                hasChanges = true;
1108            }
1109            
1110            ModifiableCompositeMetadata repeaterMetadataHolder = sharedMetadataHolder.getCompositeMetadata(logicalMetadataPath, true);
1111
1112            // Create new entries from remote data
1113            NodeList entryNodes = metadataNode.getChildNodes();
1114            for (int i = 0; i < entryNodes.getLength(); i++)
1115            {
1116                Node entryNode = entryNodes.item(i);
1117                String entryName = entryNode.getAttributes().getNamedItem("name").getTextContent().trim();
1118                
1119                NodeList childNodes = entryNode.getChildNodes();
1120                for (int j = 0; j < childNodes.getLength(); j++)
1121                {
1122                    Node childNode = childNodes.item(j);
1123                    String subMetadataName = childNode.getLocalName();
1124                    
1125                    ModifiableCompositeMetadata entryMetadataHolder = repeaterMetadataHolder.getCompositeMetadata(entryName, true);
1126                    hasChanges = synchronizeSimpleMetadataFromDOM(doc, mainProgram, childNode, metadataDefinition.getMetadataDefinition(subMetadataName), logicalMetadataPath + "/" + subMetadataName, entryMetadataHolder, subMetadataName, catalog, lang, logger)  || hasChanges;
1127                }
1128            }
1129        }
1130        
1131        return hasChanges;
1132    }
1133    
1134    /**
1135     * Synchronize a simple metadata from DOM
1136     * @param doc the document
1137     * @param subProgram the subProgram content
1138     * @param metadataNode The metadata DOM node
1139     * @param metadataDefinition the metadata definition
1140     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
1141     * @param sharedMetadataHolder the metadata composite holding shared metadata of subprograms
1142     * @param metadataName the metadata name
1143     * @param catalog the catalog
1144     * @param lang the language
1145     * @param logger The logger
1146     * @return <code>true</code> if changes were made
1147     */
1148    public boolean synchronizeSimpleMetadataFromDOM (Document doc, SubProgram subProgram, Node metadataNode, MetadataDefinition metadataDefinition, String logicalMetadataPath, ModifiableCompositeMetadata sharedMetadataHolder, String metadataName, String catalog, String lang, Logger logger)
1149    {
1150        List<String> metadataValues = null;
1151        ContentType contentType = _contentTypeEP.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
1152        
1153        MetadataType type = metadataDefinition.getType();
1154        switch (type)
1155        {
1156            case BINARY:
1157                return _handleBinaryMetadata(metadataNode, subProgram, sharedMetadataHolder, metadataName, false, logger);
1158            case FILE:
1159                return _handleFileMetadata(metadataNode, subProgram, logicalMetadataPath, sharedMetadataHolder, metadataName, false, contentType, logger);
1160            case GEOCODE:
1161                return _handleGeocodeMetadata(metadataNode, subProgram, sharedMetadataHolder, metadataName, false);
1162            case CONTENT:
1163            case STRING:
1164            case LONG:
1165            case DOUBLE:
1166            case REFERENCE:
1167            case BOOLEAN:
1168                if (metadataDefinition.isMultiple())
1169                {
1170                    NodeList itemNodes = metadataNode.getChildNodes();
1171                    metadataValues = new ArrayList<>();
1172                    for (int j = 0; j < itemNodes.getLength(); j++)
1173                    {
1174                        String metadataValue = itemNodes.item(j).getTextContent().trim();
1175                        if (StringUtils.isNotEmpty(metadataValue))
1176                        {
1177                            metadataValues.add(metadataValue);
1178                        }
1179                    }
1180                }
1181                else
1182                {
1183                    String metadataValue = metadataNode.getTextContent().trim();
1184                    if (StringUtils.isNotEmpty(metadataValue))
1185                    {
1186                        metadataValues = List.of(metadataValue);
1187                    }
1188                }
1189                break;
1190            default:
1191                String warn = "Le type de métadonnée '" + type.toString() + "' n'est pas supporté pour la fusion des entrées de repeater de parcours partagés. La métadonnée est ignorée"; 
1192                logger.warn(warn);
1193                return false;
1194        }
1195        
1196        List<Object> handleMetadataValues = _handleMetadataValues(doc, subProgram, metadataDefinition, metadataValues, lang, catalog, logger);
1197        Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(subProgram, contentType, logicalMetadataPath, sharedMetadataHolder, metadataName, handleMetadataValues, false, false, logger);
1198        if (resultMap.getOrDefault("error", Boolean.FALSE))
1199        {
1200            _nbError++;
1201        }
1202        return resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue();
1203    }
1204}