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