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;
017
018import java.io.ByteArrayInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.URL;
024import java.net.URLDecoder;
025import java.text.ParseException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.Set;
036
037import javax.jcr.RepositoryException;
038import javax.xml.transform.TransformerException;
039
040import org.apache.avalon.framework.activity.Initializable;
041import org.apache.avalon.framework.component.Component;
042import org.apache.avalon.framework.configuration.Configurable;
043import org.apache.avalon.framework.configuration.Configuration;
044import org.apache.avalon.framework.configuration.ConfigurationException;
045import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
046import org.apache.avalon.framework.context.ContextException;
047import org.apache.avalon.framework.context.Contextualizable;
048import org.apache.avalon.framework.service.ServiceException;
049import org.apache.avalon.framework.service.ServiceManager;
050import org.apache.avalon.framework.service.Serviceable;
051import org.apache.cocoon.Constants;
052import org.apache.cocoon.ProcessingException;
053import org.apache.cocoon.environment.Context;
054import org.apache.commons.io.IOUtils;
055import org.apache.commons.lang3.StringUtils;
056import org.apache.excalibur.xml.dom.DOMParser;
057import org.apache.excalibur.xml.xpath.PrefixResolver;
058import org.apache.excalibur.xml.xpath.XPathProcessor;
059import org.slf4j.Logger;
060import org.w3c.dom.Document;
061import org.w3c.dom.Element;
062import org.w3c.dom.NamedNodeMap;
063import org.w3c.dom.Node;
064import org.w3c.dom.NodeList;
065import org.xml.sax.InputSource;
066import org.xml.sax.SAXException;
067
068import org.ametys.cms.ObservationConstants;
069import org.ametys.cms.content.external.ExternalizableMetadataHelper;
070import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus;
071import org.ametys.cms.contenttype.ContentType;
072import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
073import org.ametys.cms.contenttype.MetadataDefinition;
074import org.ametys.cms.contenttype.MetadataType;
075import org.ametys.cms.contenttype.RepeaterDefinition;
076import org.ametys.cms.repository.ContentQueryHelper;
077import org.ametys.cms.repository.ContentTypeExpression;
078import org.ametys.cms.repository.LanguageExpression;
079import org.ametys.cms.repository.ModifiableContent;
080import org.ametys.cms.repository.ModifiableDefaultContent;
081import org.ametys.cms.repository.comment.DefaultCommentManagerExtensionPoint;
082import org.ametys.core.observation.Event;
083import org.ametys.core.observation.ObservationManager;
084import org.ametys.core.user.CurrentUserProvider;
085import org.ametys.odf.ProgramItem;
086import org.ametys.odf.catalog.CatalogsManager;
087import org.ametys.odf.cdmfr.CDMHelper;
088import org.ametys.odf.course.Course;
089import org.ametys.odf.course.CourseFactory;
090import org.ametys.odf.courselist.CourseList;
091import org.ametys.odf.courselist.CourseListFactory;
092import org.ametys.odf.enumeration.OdfReferenceTableEntry;
093import org.ametys.odf.enumeration.OdfReferenceTableHelper;
094import org.ametys.odf.observation.OdfObservationConstants;
095import org.ametys.odf.orgunit.OrgUnit;
096import org.ametys.odf.orgunit.OrgUnitFactory;
097import org.ametys.odf.orgunit.RootOrgUnitProvider;
098import org.ametys.odf.person.PersonFactory;
099import org.ametys.odf.program.ContainerFactory;
100import org.ametys.odf.program.ProgramFactory;
101import org.ametys.odf.program.ProgramPart;
102import org.ametys.odf.program.SubProgramFactory;
103import org.ametys.odf.program.TraversableProgramPart;
104import org.ametys.odf.translation.TranslationHelper;
105import org.ametys.plugins.contentio.ContentImporterHelper;
106import org.ametys.plugins.contentio.synchronize.BaseSynchroComponent;
107import org.ametys.plugins.odfsync.cdmfr.CDMFrSyncExtensionPoint;
108import org.ametys.plugins.odfsync.cdmfr.transformers.CDMFrSyncTransformer;
109import org.ametys.plugins.repository.AmetysObjectIterable;
110import org.ametys.plugins.repository.AmetysObjectResolver;
111import org.ametys.plugins.repository.AmetysRepositoryException;
112import org.ametys.plugins.repository.metadata.CompositeMetadata;
113import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata;
114import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
115import org.ametys.plugins.repository.metadata.ModifiableFile;
116import org.ametys.plugins.repository.metadata.ModifiableResource;
117import org.ametys.plugins.repository.metadata.ModifiableRichText;
118import org.ametys.plugins.repository.query.expression.AndExpression;
119import org.ametys.plugins.repository.query.expression.Expression;
120import org.ametys.plugins.repository.query.expression.Expression.Operator;
121import org.ametys.plugins.repository.query.expression.StringExpression;
122import org.ametys.runtime.config.Config;
123
124import com.google.common.collect.ImmutableList;
125import com.google.common.collect.ImmutableMap;
126
127/**
128 * Abstract class of a component to import a CDM-fr input stream.
129 */
130public abstract class AbstractImportCDMFrComponent implements ImportCDMFrComponent, Serviceable, Initializable, Contextualizable, Configurable, Component
131{
132    /** Tag to identify a program */
133    protected static final String _TAG_PROGRAM = "program";
134
135    /** Tag to identify a subprogram */
136    protected static final String _TAG_SUBPROGRAM = "subProgram";
137    
138    /** Tag to identify a container */
139    protected static final String _TAG_CONTAINER = "container";
140    
141    /** Tag to identify a courseList */
142    protected static final String _TAG_COURSELIST = "coursesReferences";
143    
144    private static final ContentWorkflowDescription _PROGRAM_WF_DESCRIPTION = new ContentWorkflowDescription(ProgramFactory.PROGRAM_CONTENT_TYPE, "program", 1, 4);
145    private static final ContentWorkflowDescription _SUBPROGRAM_WF_DESCRIPTION = new ContentWorkflowDescription(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, "subprogram", 1, 4);
146    private static final ContentWorkflowDescription _CONTAINER_WF_DESCRIPTION = new ContentWorkflowDescription(ContainerFactory.CONTAINER_CONTENT_TYPE, "container", 1, 4);
147    private static final ContentWorkflowDescription _COURSELIST_WF_DESCRIPTION = new ContentWorkflowDescription(CourseListFactory.COURSE_LIST_CONTENT_TYPE, "courselist", 1, 4);
148    private static final ContentWorkflowDescription _COURSE_WF_DESCRIPTION = new ContentWorkflowDescription(CourseFactory.COURSE_CONTENT_TYPE, "course", 1, 4);
149    private static final ContentWorkflowDescription _ORGUNIT_WF_DESCRIPTION = new ContentWorkflowDescription(OrgUnitFactory.ORGUNIT_CONTENT_TYPE, "orgunit", 1, 4);
150    private static final ContentWorkflowDescription _PERSON_WF_DESCRIPTION = new ContentWorkflowDescription(PersonFactory.PERSON_CONTENT_TYPE, "person", 1, 4);
151
152    private static final PrefixResolver _PREFIX_RESOLVER = new DocbookPrefixResolver();
153    
154    /** The Cocoon context */
155    protected Context _cocoonContext;
156    
157    /** The DOM parser */
158    protected DOMParser _domParser;
159
160    /** The XPath processor */
161    protected XPathProcessor _xPathProcessor;
162
163    /** Extension point to transform CDM-fr */
164    protected CDMFrSyncExtensionPoint _cdmFrSyncExtensionPoint;
165
166    /** Default language configured for ODF */
167    protected String _odfLang;
168    
169    /** The catalog manager */
170    protected CatalogsManager _catalogsManager;
171
172    /** The ametys object resolver */
173    protected AmetysObjectResolver _resolver;
174
175    /** The ODF TableRef Helper */
176    protected OdfReferenceTableHelper _odfRefTableHelper;
177
178    /** The content type extension point */
179    protected ContentTypeExtensionPoint _contentTypeEP;
180
181    /** The current user provider */
182    protected CurrentUserProvider _currentUserProvider;
183
184    /** The observation manager */
185    protected ObservationManager _observationManager;
186    
187    /** The root orgunit provider */
188    protected RootOrgUnitProvider _rootOUProvider;
189
190    /** The base SCC component */
191    protected BaseSynchroComponent _synchroComponent;
192    
193    /** List of synchronized contents */
194    protected Map<String, Integer> _importedContents;
195    
196    /** List of synchronized contents having differences */
197    protected Set<String> _synchronizedContents;
198    
199    /** List of synchronized contents (to avoid a treatment twice or more) */
200    protected Set<String> _updatedContents;
201
202    /** Number of errors encountered */
203    protected int _nbError;
204    /** Number of created contents */
205    protected int _nbCreatedContents;
206    /** Number of synchronized contents */
207    protected int _nbSynchronizedContents;
208    /** Number of unchanged contents */
209    protected int _nbNotChangedContents;
210    /** The prefix of the contents */
211    protected String _contentPrefix;
212    /** Synchronized fields by content type */
213    protected Map<String, Set<String>> _syncFieldsByContentType;
214
215    public void initialize() throws Exception
216    {
217        _odfLang = Config.getInstance().getValueAsString("odf.programs.lang");
218    }
219
220    @Override
221    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
222    {
223        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
224    }
225
226    public void configure(Configuration configuration) throws ConfigurationException
227    {
228        _parseSynchronizedFields();
229    }
230    
231    @Override
232    public void service(ServiceManager manager) throws ServiceException
233    {
234        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
235        _domParser = (DOMParser) manager.lookup(DOMParser.ROLE);
236        _xPathProcessor = (XPathProcessor) manager.lookup(XPathProcessor.ROLE);
237        _cdmFrSyncExtensionPoint = (CDMFrSyncExtensionPoint) manager.lookup(CDMFrSyncExtensionPoint.ROLE);
238        _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE);
239        _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
240        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
241        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
242        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
243        _rootOUProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
244        _synchroComponent = (BaseSynchroComponent) manager.lookup(BaseSynchroComponent.ROLE);
245    }
246
247    @Override
248    public String getIdField()
249    {
250        return "cdmfrSyncCode";
251    }
252
253    /**
254     * Get the synchronized metadata from the configuration file
255     * @throws ConfigurationException if the configuration is not valid.
256     */
257    private void _parseSynchronizedFields() throws ConfigurationException
258    {
259        _syncFieldsByContentType = new HashMap<>();
260        
261        @SuppressWarnings("resource")
262        InputStream is = null;
263        try
264        {
265            File apogeeMapping = new File(_cocoonContext.getRealPath("/WEB-INF/param/odf-synchro.xml"));
266            if (!apogeeMapping.isFile())
267            {
268                is = getClass().getResourceAsStream("/org/ametys/plugins/odfsync/cdmfr/odf-synchro.xml");
269            }
270            else
271            {
272                is = new FileInputStream(apogeeMapping);
273            }
274            Configuration cfg = new DefaultConfigurationBuilder().build(is);
275
276            Configuration[] cTypesConf = cfg.getChildren("content-type");
277            for (Configuration cTypeConf : cTypesConf)
278            {
279                String contentType = cTypeConf.getAttribute("id");
280                Set<String> syncMetadata = _configureSynchronizedFields(cTypeConf, "");
281                _syncFieldsByContentType.put(contentType, syncMetadata);
282            }
283        }
284        catch (Exception e)
285        {
286            throw new ConfigurationException("Error while parsing odf-synchro.xml", e);
287        }
288        finally
289        {
290            IOUtils.closeQuietly(is);
291        }
292    }
293
294    private Set<String> _configureSynchronizedFields(Configuration configuration, String prefix) throws ConfigurationException
295    {
296        Set<String> syncMetadata = new HashSet<>();
297        Configuration[] metaConf = configuration.getChildren("metadata");
298        
299        if (metaConf.length > 0)
300        {
301            for (Configuration metadata : metaConf)
302            {
303                if (metadata.getChildren("metadata").length > 0)
304                {
305                    // composite
306                    syncMetadata.addAll(_configureSynchronizedFields(metadata, prefix + metadata.getAttribute("name") + "/"));
307                }
308                else
309                {
310                    syncMetadata.add(prefix + metadata.getAttribute("name"));
311                }
312            }
313        }
314        else if (configuration.getAttribute("name", null) != null)
315        {
316            syncMetadata.add(prefix + configuration.getAttribute("name"));
317        }
318        
319        return syncMetadata;
320    }
321    
322    @Override
323    @SuppressWarnings("unchecked")
324    public synchronized Map<String, Object> handleInputStream(InputStream input, Map<String, Object> parameters, Logger logger) throws ProcessingException
325    {
326        List<ModifiableDefaultContent> importedPrograms = new ArrayList<>();
327        
328        _importedContents = (Map<String, Integer>) parameters.getOrDefault("importedContents", new HashMap<>());
329        _synchronizedContents = (Set<String>) parameters.getOrDefault("synchronizedContents", new HashSet<>());
330        _updatedContents = (Set<String>) parameters.getOrDefault("updatedContents", new HashSet<>());
331        _nbCreatedContents = (int) parameters.getOrDefault("nbCreatedContents", 0);
332        _nbSynchronizedContents = (int) parameters.getOrDefault("nbSynchronizedContents", 0);
333        _nbNotChangedContents = (int) parameters.getOrDefault("nbNotChangedContents", 0);
334        _nbError = (int) parameters.getOrDefault("nbError", 0);
335        _contentPrefix = (String) parameters.getOrDefault("contentPrefix", "cdmfr-");
336        additionalParameters(parameters);
337        
338        Map<String, Object> resultMap = new HashMap<>();
339        
340        try
341        {
342            Document doc = _domParser.parseDocument(new InputSource(input));
343            doc = transformDocument(doc, new HashMap<String, Object>(), logger);
344
345            if (doc != null)
346            {
347                String defaultLang = _getXPathString(doc, "CDM/@language", _odfLang);
348                
349                NodeList nodes = doc.getElementsByTagName(_TAG_PROGRAM);
350                
351                for (int i = 0; i < nodes.getLength(); i++)
352                {
353                    Node contentNode = nodes.item(i);
354                    String syncCode = _xPathProcessor.evaluateAsString(contentNode, "@CDMid");
355                    String contentLang = _getXPathString(contentNode, "@language", defaultLang);
356                    contentLang = StringUtils.substring(contentLang, 0, 2).toLowerCase(); // on keep the language from the locale
357                    
358                    String catalog = getCatalogName(contentNode);
359
360                    importedPrograms.add(_importOrSynchronizeContent(doc, contentNode, getProgramWfDescription(), syncCode, contentLang, catalog, syncCode, logger));
361                }
362                
363                // Apply changes (synchronize action)
364                for (String contentId : _synchronizedContents)
365                {
366                    ModifiableDefaultContent content = _resolver.resolveById(contentId);
367                    _synchroComponent.applyChanges(content, BaseSynchroComponent.SYNCHRONIZE_WORKFLOW_ACTION_ID, org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, logger);
368                }
369                
370                // Validate contents
371                if (validateAfterImport())
372                {
373                    for (String contentId : _importedContents.keySet())
374                    {
375                        ModifiableDefaultContent content = _resolver.resolveById(contentId);
376                        _synchroComponent.validateContent(content, _importedContents.get(contentId), logger);
377                    }
378                }
379            }
380        }
381        catch (IOException | ProcessingException e)
382        {
383            throw new ProcessingException("An error occured while transforming the stream.", e);
384        }
385        catch (SAXException e)
386        {
387            throw new ProcessingException("An error occured while parsing the stream.", e);
388        }
389        catch (RepositoryException e)
390        {
391            throw new ProcessingException("An error occured while applying changes on contents.", e);
392        }
393
394        resultMap.put("importedContents", _importedContents);
395        resultMap.put("synchronizedContents", _synchronizedContents);
396        resultMap.put("updatedContents", _updatedContents);
397        resultMap.put("nbCreatedContents", _nbCreatedContents);
398        resultMap.put("nbSynchronizedContents", _nbSynchronizedContents);
399        resultMap.put("nbNotChangedContents", _nbNotChangedContents);
400        resultMap.put("nbError", _nbError);
401        resultMap.put("importedPrograms", importedPrograms);
402
403        return resultMap;
404    }
405
406    /**
407     * True to validate the contents after import
408     * @return True to validate the contents after import
409     */
410    protected abstract boolean validateAfterImport();
411
412    /**
413     * When returns true, a content created by a previous synchro will be removed if it does not exist anymore during the current synchro.
414     * @return true if a content created by a previous synchro has to be removed if it does not exist anymore during the current synchro.
415     */
416    protected abstract boolean removalSync();
417    
418    /**
419     * Transform the document depending of it structure.
420     * @param document Document to transform.
421     * @param parameters Optional parameters for transformation
422     * @param logger The logger
423     * @return The transformed document.
424     * @throws IOException if an error occurs.
425     * @throws SAXException if an error occurs.
426     * @throws ProcessingException if an error occurs.
427     */
428    protected Document transformDocument(Document document, Map<String, Object> parameters, Logger logger) throws IOException, SAXException, ProcessingException
429    {
430        CDMFrSyncTransformer transformer = _cdmFrSyncExtensionPoint.getTransformer(document);
431        if (transformer == null)
432        {
433            logger.error("Cannot match a CDM-fr transformer to this file structure.");
434            return null;
435        }
436        
437        return transformer.transform(document, parameters);
438    }
439
440    /**
441     * Get the name of catalog to use for import
442     * @param contentNode The node of program
443     * @return The catalog to used
444     */
445    protected String getCatalogName(Node contentNode)
446    {
447        String defaultCatalog = _catalogsManager.getDefaultCatalogName();
448        
449        String contentCatalog = _getXPathString(contentNode, "catalog", defaultCatalog);
450        if (_catalogsManager.getCatalog(contentCatalog) == null)
451        {
452            // Catalog is empty or do not exist, use the default catalog
453            return defaultCatalog;
454        }
455        
456        return contentCatalog;
457    }
458    
459    /**
460     * Get or create the content from the synchronization code, the lang, the catalog and the content type.
461     * @param title The title
462     * @param lang The lang
463     * @param catalog The catalog
464     * @param syncCode The synchronization code
465     * @param wfDescription The workflow description
466     * @param logger The logger
467     * @return the retrieved or created content
468     * @throws RepositoryException if an error occurs
469     */
470    protected ModifiableDefaultContent _getOrCreateContent(String title, String lang, String catalog, String syncCode, ContentWorkflowDescription wfDescription, Logger logger) throws RepositoryException
471    {
472        List<Expression> expList = getExpressionsList(lang, syncCode, wfDescription.getContentType(), catalog);
473        AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()]));
474        String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp);
475
476        AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(xPathQuery);
477
478        if (contents.getSize() > 0)
479        {
480            return contents.iterator().next();
481        }
482        
483        Map<String, Object> resultMap = _synchroComponent.createContentAction(wfDescription.getContentType(), wfDescription.getWorkflowName(), wfDescription.getInitialActionId(), lang, title, _contentPrefix, logger);
484        if ((boolean) resultMap.getOrDefault("error", false))
485        {
486            _nbError++;
487        }
488        
489        ModifiableDefaultContent content = (ModifiableDefaultContent) resultMap.get("content");
490        
491        if (content != null)
492        {
493            ExternalizableMetadataHelper.setMetadata(content.getMetadataHolder(), getIdField(), syncCode);
494            if (catalog != null && content instanceof ProgramItem)
495            {
496                ExternalizableMetadataHelper.setMetadata(content.getMetadataHolder(), ProgramItem.METADATA_CATALOG, catalog);
497            }
498            additionalOperationsBeforeSave(content, logger);
499            content.saveChanges();
500            _importedContents.put(content.getId(), wfDescription.getValidationActionId());
501            _nbCreatedContents++;
502        }
503        return content;
504    }
505    
506    /**
507     * Additional parameters for specific treatments.
508     * @param parameters The parameters map to get
509     */
510    protected abstract void additionalParameters(Map<String, Object> parameters);
511    
512    /**
513     * Additional operation to do on the content before saving it.
514     * @param content The content
515     * @param logger The logger
516     * @throws RepositoryException if an error occurs
517     */
518    protected abstract void additionalOperationsBeforeSave(ModifiableDefaultContent content, Logger logger) throws RepositoryException;
519    
520    /**
521     * Import or synchronize the content.
522     * @param doc XML document
523     * @param contentNode Node of the content
524     * @param wfDescription The workflow description
525     * @param title The title
526     * @param lang The lang
527     * @param catalog The catalog
528     * @param syncCode The synchronization code
529     * @param logger The logger
530     * @return The imported or synchronized content
531     */
532    protected ModifiableDefaultContent _importOrSynchronizeContent(Document doc, Node contentNode, ContentWorkflowDescription wfDescription, String title, String lang, String catalog, String syncCode, Logger logger)
533    {
534        ModifiableDefaultContent content = null;
535        try
536        {
537            content = _getOrCreateContent(title, lang, catalog, syncCode, wfDescription, logger);
538            if (content != null)
539            {
540                _synchronizeContent(doc, contentNode, content, wfDescription.getContentType(), lang, catalog, syncCode, logger);
541            }
542        }
543        catch (RepositoryException e)
544        {
545            _nbError++;
546            logger.error("An error occurred while importing or synchronizing content", e);
547        }
548        return content;
549    }
550    
551    /**
552     * Synchronize content 
553     * @param doc The root document
554     * @param contentNode the DOM content node
555     * @param content The content to synchronize
556     * @param contentTypeId The content type ID
557     * @param lang Parent program language (to select the good courses in the CDM-FR file)
558     * @param catalog The catalog of parent program
559     * @param syncCode The synchronization code
560     * @param logger The logger
561     */
562    protected void _synchronizeContent(Document doc, Node contentNode, ModifiableDefaultContent content, String contentTypeId, String lang, String catalog, String syncCode, Logger logger)
563    {
564        if (_updatedContents.add(content.getId()))
565        {
566            logger.info("Synchronization of the content '{}' with the content type '{}'", content.getTitle(), contentTypeId);
567            
568            List<ModifiableDefaultContent> children = new LinkedList<>();
569            
570            if (content.isLocked())
571            {
572                logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), content.getLockOwner());
573            }
574            else
575            {
576                boolean hasChanges = false;
577                ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
578                int courseListPosition = 0;
579                
580                NodeList metadataNodes = contentNode.getChildNodes();
581                for (int i = 0; i < metadataNodes.getLength(); i++)
582                {
583                    Node metadataNode = metadataNodes.item(i);
584                    String metadataName = metadataNode.getLocalName();
585    
586                    if (metadataName.equals(_TAG_SUBPROGRAM))
587                    {
588                        String subContentSyncCode = _xPathProcessor.evaluateAsString(metadataNode, "@CDMid");
589                        String title = _xPathProcessor.evaluateAsString(metadataNode, "title");
590                        ModifiableDefaultContent subProgram = _importOrSynchronizeContent(doc, metadataNode, getSubProgramWfDescription(), title, lang, catalog, subContentSyncCode, logger);
591                        if (subProgram != null)
592                        {
593                            children.add(subProgram);
594                        }
595                    }
596                    else if (metadataName.equals(_TAG_CONTAINER))
597                    {
598                        String subContentSyncCode = _xPathProcessor.evaluateAsString(metadataNode, "@CDMid");
599                        String title = _xPathProcessor.evaluateAsString(metadataNode, "title");
600                        ModifiableDefaultContent container = _importOrSynchronizeContent(doc, metadataNode, getContainerWfDescription(), title, lang, catalog, subContentSyncCode, logger);
601                        if (container != null)
602                        {
603                            children.add(container);
604                        }
605                    }
606                    else if (metadataName.equals(_TAG_COURSELIST))
607                    {
608                        courseListPosition++;
609                        // For courseList from another source than Ametys, there is no unique code, then a code is generated with the parent ID and the position in the parent : [parentId]-[position]
610                        String subContentSyncCode = _getXPathString(metadataNode, "@code", syncCode + "-" + courseListPosition);
611                        String title = _getXPathString(metadataNode, "@name", "Liste d'éléments pédagogiques");
612                        ModifiableDefaultContent courseList = _importOrSynchronizeContent(doc, metadataNode, getCourseListWfDescription(), title, lang, catalog, subContentSyncCode, logger);
613                        if (courseList != null)
614                        {
615                            _synchronizeCourseList(doc, metadataNode, courseList, lang, catalog, logger);
616                            children.add(courseList);
617                        }
618                    }
619                    // Explicitely ignore catalog metadata which is forced at content creation
620                    else if (!metadataName.equals(ProgramItem.METADATA_CATALOG))
621                    {
622                        hasChanges = _synchronizeMetadata(doc, metadataNode, content, metadataName, metadataName, contentType, lang, logger) || hasChanges;
623                    }
624                }
625
626                hasChanges = _setRelations(content, children, logger) || hasChanges;
627                
628                // Create translation links
629                _linkTranslationsIfExist(content, contentTypeId);
630                
631                _saveContentChanges(content, contentTypeId, hasChanges, logger);
632            }
633        }
634    }
635
636    /**
637     * Synchronize a course list, it has attributes to synchronize.
638     * @param doc The XML document
639     * @param courseListNode The XML node of the course list
640     * @param courseList The course list content
641     * @param lang The lang
642     * @param catalog The catalog
643     * @param logger The logger
644     */
645    protected void _synchronizeCourseList(Document doc, Node courseListNode, ModifiableDefaultContent courseList, String lang, String catalog, Logger logger)
646    {
647        boolean hasChanges = false;
648        
649        ContentType contentType = _contentTypeEP.getExtension(getCourseListWfDescription().getContentType());
650        NamedNodeMap attributes = courseListNode.getAttributes();
651        for (int i = 0; i < attributes.getLength(); i++)
652        {
653            Node attributeNode = attributes.item(i);
654            String attributeName = attributeNode.getLocalName();
655            // Explicitely ignore catalog metadata which is forced at content creation
656            if (!attributeName.equals(ProgramItem.METADATA_CATALOG))
657            {
658                hasChanges = _synchronizeMetadata(doc, attributeNode, courseList, attributeName, attributeName, contentType, lang, logger) || hasChanges;
659            }
660        }
661
662        List<ModifiableDefaultContent> courses = new LinkedList<>();
663        NodeList itemNodes = courseListNode.getChildNodes();
664        for (int i = 0; i < itemNodes.getLength(); i++)
665        {
666            String syncCode = itemNodes.item(i).getTextContent().trim();
667            
668            Node courseNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "course[@CDMid = '" + syncCode + "' and @language = '" + lang + "']");
669            if (courseNode == null)
670            {
671                courseNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "course[@CDMid = '" + syncCode + "']");
672            }
673            
674            if (courseNode != null)
675            {
676                String elpLang = _getXPathString(courseNode, "@language", lang);
677
678                // Check the catalog is the same as the parent program
679                String courseCatalog = getCatalogName(courseNode);
680                if (courseCatalog != null && !courseCatalog.equals(catalog))
681                {
682                    String elpCode = _xPathProcessor.evaluateAsString(courseNode, "elpCode");
683                    logger.error("The course '{}' belongs to a different catalog than the one from the imported/synchronized program : '{}' vs '{}'. No synchronization will be done on this course.", elpCode, courseCatalog, catalog);
684                }
685                else
686                {
687                    String title = _xPathProcessor.evaluateAsString(courseNode, "title");
688                    ModifiableDefaultContent course = _importOrSynchronizeContent(doc, courseNode, getCourseWfDescription(), title, elpLang, catalog, syncCode, logger);
689                    if (course != null)
690                    {
691                        courses.add(course);
692                    }
693                }
694            }
695        }
696        
697        if (!courses.isEmpty())
698        {
699            hasChanges = ExternalizableMetadataHelper.setMetadata(courseList.getMetadataHolder(), CourseList.METADATA_CHILD_COURSES, courses.toArray(new ModifiableDefaultContent[courses.size()])) || hasChanges;
700            for (ModifiableDefaultContent course : courses)
701            {
702                hasChanges = _synchroComponent.updateRelation(course.getMetadataHolder(), Course.METADATA_PARENT_COURSE_LISTS, courseList, false) || hasChanges;
703            }
704        }
705        
706        _saveContentChanges(courseList, getCourseListWfDescription().getContentType(), hasChanges, logger);
707    }
708
709    private void _fetchImages(Node docbookNode, ModifiableDefaultContent content, ModifiableRichText richText, String metadataPath, Logger logger)
710    {
711        NodeList unfetchUrls = _xPathProcessor.selectNodeList(docbookNode, ".//docbook:mediaobject/docbook:imageobject/docbook:imagedata", _PREFIX_RESOLVER);
712        
713        for (int i = 0; i < unfetchUrls.getLength(); i++)
714        {
715            Element href = (Element) unfetchUrls.item(i);
716            String imageUrl = href.getAttribute("fileref");
717            
718            if (DefaultCommentManagerExtensionPoint.URL_VALIDATOR.matcher(imageUrl).matches())
719            {
720                @SuppressWarnings("resource")
721                InputStream is = null;
722                try
723                {
724                    URL url = new URL(imageUrl);
725                    is = url.openStream();
726                    
727                    String path = url.getPath();
728                    String fileName = path.substring(path.lastIndexOf("/") + 1);
729                    
730                    ModifiableFile file = null;
731                    if (richText.getAdditionalDataFolder().hasFile(fileName))
732                    {
733                        file = richText.getAdditionalDataFolder().getFile(fileName);
734                    }
735                    else
736                    {
737                        file = richText.getAdditionalDataFolder().addFile(fileName); 
738                    }
739                    
740                    ModifiableResource resource = file.getResource();
741                    resource.setInputStream(is);
742                    resource.setLastModified(new Date());
743                    
744                    String mimeType = _cocoonContext.getMimeType(fileName);
745                    if (mimeType == null)
746                    {
747                        mimeType = "application/unknown";
748                    }
749                    resource.setMimeType(mimeType);
750                    
751                    String fetchUrl = content.getId() + "@" + metadataPath + ";" + fileName;
752                    href.setAttribute("fileref", fetchUrl);
753                }
754                catch (Exception e)
755                {
756                    logger.warn("Unable to retrieve remote image '{}'.", imageUrl, e);
757                }
758                finally
759                {
760                    IOUtils.closeQuietly(is);
761                }
762            }
763        }
764    }
765    
766    /**
767     * Synchronize a metadata (can be a composite or a repeater).
768     * @param doc The XML document
769     * @param metadataNode The metadata node
770     * @param content The content
771     * @param logicalMetadataPath The logical metadata path (to retrieve the definition)
772     * @param completeMetadataPath The complete metadata path (to retrieve the metadata holder)
773     * @param contentType The content type
774     * @param lang The lang
775     * @param logger The logger
776     * @return <code>true</code> if changes occurs
777     */
778    protected boolean _synchronizeMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger)
779    {
780        List<String> metadataValues = null;
781        MetadataDefinition metadataDef = contentType.getMetadataDefinitionByPath(logicalMetadataPath);
782        Map<String, Object> params = ImmutableMap.of("contentType", contentType.getId());
783        boolean synchronize = getLocalAndExternalFields(params).contains(logicalMetadataPath);
784        if (metadataDef != null)
785        {
786            if (metadataDef instanceof RepeaterDefinition)
787            {
788                return _handleRepeaterMetadata(doc, metadataNode, content, logicalMetadataPath, completeMetadataPath, contentType, lang, logger);
789            }
790            else if (metadataDef.getType() == MetadataType.COMPOSITE)
791            {
792                return _handleCompositeMetadata(doc, metadataNode, content, logicalMetadataPath, completeMetadataPath, contentType, lang, logger);
793            }
794            else if (metadataDef.getType() == MetadataType.RICH_TEXT)
795            {
796                return _handleRichTextMetadata(content, logicalMetadataPath, completeMetadataPath, metadataNode, synchronize, logger);
797            }
798            else if (metadataDef.getType() == MetadataType.BINARY)
799            {
800                return _handleBinaryMetadata(metadataNode, content, completeMetadataPath, synchronize, logger);
801            }
802            else if (metadataDef.getType() == MetadataType.FILE)
803            {
804                return _handleFileMetadata(metadataNode, content, logicalMetadataPath, completeMetadataPath, synchronize, contentType, logger);
805            }
806            else if (metadataDef.getType() == MetadataType.GEOCODE)
807            {
808                return _handleGeocodeMetadata(metadataNode, content, completeMetadataPath, synchronize);
809            }
810            else if (metadataDef.getType() == MetadataType.BOOLEAN)
811            {
812                metadataValues = ImmutableList.of("true");
813            }
814            else if (metadataDef.isMultiple())
815            {
816                NodeList itemNodes = metadataNode.getChildNodes();
817                metadataValues = new ArrayList<>();
818                for (int j = 0; j < itemNodes.getLength(); j++)
819                {
820                    String metadataValue = itemNodes.item(j).getTextContent().trim();
821                    if (StringUtils.isNotEmpty(metadataValue))
822                    {
823                        metadataValues.add(metadataValue);
824                    }
825                }
826            }
827            else
828            {
829                String metadataValue = metadataNode.getTextContent().trim();
830                if (StringUtils.isNotEmpty(metadataValue))
831                {
832                    metadataValues = ImmutableList.of(metadataValue);
833                }
834            }
835        }
836
837        Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(content, contentType, logicalMetadataPath, completeMetadataPath, _handleMetadataValues(doc, content, metadataDef, metadataValues, lang, logger), synchronize, _importedContents.containsKey(content.getId()), logger);
838        if (resultMap.getOrDefault("error", Boolean.FALSE))
839        {
840            _nbError++;
841        }
842        return resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue();
843    }
844    
845    private boolean _handleGeocodeMetadata(Node metadataNode, ModifiableDefaultContent content, String completeMetadataPath, boolean synchronize)
846    {
847        ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath);
848        String[] arrayPath = completeMetadataPath.split("/");
849        String metadataName = arrayPath[arrayPath.length - 1];
850        
851        if (!metadataNode.hasChildNodes())
852        {
853            return _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize);
854        }
855
856        boolean hasChanges = false;
857        ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL;
858        ModifiableCompositeMetadata geoCode = ExternalizableMetadataHelper.getCompositeMetadata(metadataHolder, metadataName, status, true);
859        if (_importedContents.containsKey(content.getId()))
860        {
861            hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges;
862        }
863        
864        NodeList coordinates = metadataNode.getChildNodes();
865        for (int j = 0; j < coordinates.getLength(); j++)
866        {
867            Node coordinate = coordinates.item(j);
868            hasChanges = ExternalizableMetadataHelper.setMetadata(geoCode, coordinate.getLocalName(), coordinate.getTextContent().trim()) || hasChanges;
869        }
870        return hasChanges;
871    }
872
873    private boolean _handleFileMetadata(Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, boolean synchronize, ContentType contentType, Logger logger)
874    {
875        ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath);
876        String[] arrayPath = completeMetadataPath.split("/");
877        String metadataName = arrayPath[arrayPath.length - 1];
878        
879        boolean hasChanges = false;
880        String value = metadataNode.getTextContent().trim();
881        boolean metadataExists = metadataHolder.hasMetadata(metadataName);
882        CompositeMetadata.MetadataType metadataType = metadataExists ? metadataHolder.getType(metadataName) : null;
883        if (DefaultCommentManagerExtensionPoint.URL_VALIDATOR.matcher(value).matches())
884        {
885            if (metadataExists && metadataType != CompositeMetadata.MetadataType.BINARY)
886            {
887                hasChanges = _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize) || hasChanges;
888            }
889            hasChanges = _handleBinaryMetadata(metadataNode, content, completeMetadataPath, synchronize, logger) || hasChanges;
890        }
891        else
892        {
893            if (metadataExists && metadataType != CompositeMetadata.MetadataType.STRING)
894            {
895                hasChanges = _synchroComponent.removeMetadataIfExists(metadataHolder, metadataName, synchronize) || hasChanges;
896            }
897            
898            Map<String, Boolean> resultMap = _synchroComponent.synchronizeMetadata(content, contentType, logicalMetadataPath, completeMetadataPath, ImmutableList.of(value), synchronize, _importedContents.containsKey(content.getId()), logger);
899            if (resultMap.getOrDefault("error", Boolean.FALSE))
900            {
901                _nbError++;
902            }
903            hasChanges = resultMap.getOrDefault("hasChanges", Boolean.FALSE).booleanValue() || hasChanges;
904        }
905        return hasChanges;
906    }
907    
908    private boolean _handleBinaryMetadata(Node metadataNode, ModifiableDefaultContent content, String completeMetadataPath, boolean synchronize, Logger logger)
909    {
910        ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath);
911        String[] arrayPath = completeMetadataPath.split("/");
912        String metadataName = arrayPath[arrayPath.length - 1];
913        
914        boolean hasChanges = false;
915        
916        String value = metadataNode.getTextContent().trim();
917        
918        if (StringUtils.isEmpty(value))
919        {
920            return _synchroComponent.removeMetadataIfExists(metadataHolder, completeMetadataPath, synchronize);
921        }
922        
923        @SuppressWarnings("resource")
924        InputStream is = null;
925        try
926        {
927            URL url = new URL(value);
928            is = url.openStream();
929
930            ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL;
931            ModifiableBinaryMetadata binaryMetadata = ExternalizableMetadataHelper.getBinaryMetadata(metadataHolder, metadataName, status, true);
932
933            byte[] bytes = IOUtils.toByteArray(is);
934            byte[] oldValue = new byte[0];
935            try
936            {
937                oldValue = IOUtils.toByteArray(binaryMetadata.getInputStream());
938            }
939            catch (AmetysRepositoryException e)
940            {
941                logger.debug("The old value of '{}' should not be initialized on the content '{}'.", completeMetadataPath, content.getId(), e);
942            }
943
944            if (!Objects.deepEquals(bytes, oldValue))
945            {
946                String path = url.getPath();
947                String filename = path.substring(path.lastIndexOf("/") + 1);
948                String mimeType = _cocoonContext.getMimeType(filename);
949                if (mimeType == null)
950                {
951                    mimeType = "application/unknown";
952                }
953                binaryMetadata.setFilename(URLDecoder.decode(filename, "UTF-8"));
954                binaryMetadata.setMimeType(mimeType);
955                binaryMetadata.setLastModified(new Date());
956                binaryMetadata.setInputStream(is);
957    
958                hasChanges = true;
959            }
960
961            if (_importedContents.containsKey(content.getId()))
962            {
963                hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges;
964            }
965        }
966        catch (IOException e)
967        {
968            logger.error("Unable to retrieve remote file input stream", e);
969        }
970        finally
971        {
972            IOUtils.closeQuietly(is);
973        }
974
975        return hasChanges;
976    }
977    
978    private boolean _handleRepeaterMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger)
979    {
980        boolean hasChanges = false;
981        
982        ModifiableCompositeMetadata repeater = content.getMetadataHolder().getCompositeMetadata(completeMetadataPath, true);
983        
984        // Remove locale entries (unable to compare locales and remotes ...)
985        String[] metadataNames = repeater.getMetadataNames();
986        for (String entryName : metadataNames)
987        {
988            repeater.removeMetadata(entryName);
989            hasChanges = true;
990        }
991        
992        // Create new entries from remote data
993        NodeList entryNodes = metadataNode.getChildNodes();
994        for (int i = 0; i < entryNodes.getLength(); i++)
995        {
996            Node entryNode = entryNodes.item(i);
997            String entryName = entryNode.getAttributes().getNamedItem("name").getTextContent().trim();
998            
999            NodeList childNodes = entryNode.getChildNodes();
1000            for (int j = 0; j < childNodes.getLength(); j++)
1001            {
1002                Node childNode = childNodes.item(j);
1003                String subMetadataName = childNode.getLocalName();
1004                
1005                hasChanges = _synchronizeMetadata(doc, childNode, content, logicalMetadataPath + "/" + subMetadataName, completeMetadataPath + "/" + entryName + "/" + subMetadataName, contentType, lang, logger) || hasChanges;
1006            }
1007        }
1008        
1009        return hasChanges;
1010    }
1011    
1012    private boolean _handleCompositeMetadata(Document doc, Node metadataNode, ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, ContentType contentType, String lang, Logger logger)
1013    {
1014        boolean hasChanges = false;
1015        NodeList subMetadataNodes = metadataNode.getChildNodes();
1016        for (int i = 0; i < subMetadataNodes.getLength(); i++)
1017        {
1018            Node subMetadataNode = subMetadataNodes.item(i);
1019            hasChanges = _synchronizeMetadata(doc, subMetadataNode, content, logicalMetadataPath + "/" + subMetadataNode.getLocalName(), completeMetadataPath + "/" + subMetadataNode.getLocalName(), contentType, lang, logger) || hasChanges;
1020        }
1021        return hasChanges;
1022    }
1023    
1024    private boolean _handleRichTextMetadata(ModifiableDefaultContent content, String logicalMetadataPath, String completeMetadataPath, Node metadataNode, boolean synchronize, Logger logger)
1025    {
1026        ModifiableCompositeMetadata metadataHolder = _synchroComponent.getMetadataHolder(content.getMetadataHolder(), completeMetadataPath);
1027        String[] arrayPath = completeMetadataPath.split("/");
1028        String metadataName = arrayPath[arrayPath.length - 1];
1029
1030        if (metadataNode.hasChildNodes())
1031        {
1032            boolean hasChanges = false;
1033            try
1034            {
1035                String docbook = ContentImporterHelper.serializeNode(metadataNode.getFirstChild());
1036                try (ByteArrayInputStream is =  new ByteArrayInputStream(docbook.getBytes("UTF-8")))
1037                {
1038                    ExternalizableMetadataStatus status = synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL;
1039                    ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(metadataHolder, metadataName, status, true);
1040                    
1041                    _fetchImages(metadataNode.getFirstChild(), content, richText, completeMetadataPath, logger);
1042                    richText.setInputStream(is);
1043                    richText.setMimeType("text/xml");
1044                    richText.setLastModified(new Date()); 
1045
1046                    if (_importedContents.containsKey(content.getId()))
1047                    {
1048                        hasChanges = ExternalizableMetadataHelper.updateStatus(metadataHolder, metadataName, status) || hasChanges;
1049                    }
1050                    
1051                    hasChanges = true;
1052                }
1053                catch (IOException e)
1054                {
1055                    logger.error("An error occured while parsing the rich text '{}' of the content '{}'", logicalMetadataPath, content.getTitle(), e);
1056                }
1057            }
1058            catch (TransformerException e)
1059            {
1060                logger.error("Error serializing a docbook node.", e);
1061            }
1062            return hasChanges;
1063        }
1064        
1065        return ExternalizableMetadataHelper.removeMetadataIfExists(content.getMetadataHolder(), completeMetadataPath);
1066    }
1067    
1068    private List<Object> _handleMetadataValues(Document doc, ModifiableDefaultContent content, MetadataDefinition metadataDef, List<String> metadataValues, String lang, Logger logger)
1069    {
1070        if (metadataValues == null)
1071        {
1072            return null;
1073        }
1074        
1075        List<Object> finalMetadataValues = new ArrayList<>();
1076        for (String metadataValue : metadataValues)
1077        {
1078            switch (metadataDef.getType())
1079            {
1080                case DATE:
1081                    try
1082                    {
1083                        finalMetadataValues.add(CDMHelper.CDM_DATE_FORMAT.parse(metadataValue));
1084                    }
1085                    catch (ParseException e)
1086                    {
1087                        logger.warn("Unable to parse the metadata '{}' of the content '{}' with the value '{}', it should respect the following format '{}'.", metadataDef.getName(), content.getId(), metadataValue, CDMHelper.CDM_DATE_FORMAT);
1088                    }
1089                    break;
1090                case BOOLEAN:
1091                    finalMetadataValues.add(Boolean.TRUE);
1092                    break;
1093                case CONTENT:
1094                    String refContentTypeId = metadataDef.getContentType();
1095                    if (refContentTypeId != null)
1096                    {
1097                        ContentType refContentType = _contentTypeEP.getExtension(refContentTypeId);
1098                        if (refContentTypeId.equals(PersonFactory.PERSON_CONTENT_TYPE))
1099                        {
1100                            Node personNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "person[@CDMid ='" + metadataValue + "']");
1101                            ModifiableDefaultContent person = _importOrSynchronizeContent(doc, personNode, getPersonWfDescription(), metadataValue, lang, null, metadataValue, logger);
1102                            if (person != null)
1103                            {
1104                                finalMetadataValues.add(person);
1105                            }
1106                        }
1107                        else if (refContentTypeId.equals(OrgUnitFactory.ORGUNIT_CONTENT_TYPE))
1108                        {
1109                            Node ouNode = _xPathProcessor.selectSingleNode(doc.getFirstChild(), "orgunit[@CDMid ='" + metadataValue + "']");
1110                            ModifiableDefaultContent orgUnit = _importOrSynchronizeContent(doc, ouNode, getOrgUnitWfDescription(), metadataValue, lang, null, metadataValue, logger);
1111                            if (orgUnit != null)
1112                            {
1113                                finalMetadataValues.add(orgUnit);
1114                            }
1115                        }
1116                        else if (refContentType.isReferenceTable() && _odfRefTableHelper.isTableReference(refContentTypeId))
1117                        {
1118                            String entryId = _getIdFromCDMThenCode(refContentTypeId, metadataValue);
1119                            if (StringUtils.isNotEmpty(entryId))
1120                            {
1121                                finalMetadataValues.add(_resolver.resolveById(entryId));
1122                            }
1123                            else
1124                            {
1125                                logger.warn("There is not entry corresponding to the CDM-fr or Ametys code '{}' in the reference table '{}'.", metadataValue, refContentTypeId);
1126                            }
1127                        }
1128                    }
1129                    else
1130                    {
1131                        logger.warn("Cannot match data '{}' of content type '{}' because it is not typed.", metadataDef.getName(), metadataDef.getReferenceContentType());
1132                    }
1133                    break;
1134                default:
1135                    if (content instanceof CourseList && metadataDef.getName().equals(CourseList.METADATA_CHOICE_TYPE))
1136                    {
1137                        finalMetadataValues.add(metadataValue.toUpperCase());
1138                    }
1139                    else
1140                    {
1141                        finalMetadataValues.add(metadataValue);
1142                    }
1143                    break;
1144            }
1145        }
1146        
1147        return finalMetadataValues;
1148    }
1149
1150    /**
1151     * Search for translated contents
1152     * @param importedContent The imported content
1153     * @param contentType The content type
1154     */
1155    protected void _linkTranslationsIfExist(ModifiableContent importedContent, String contentType)
1156    {
1157        if (importedContent instanceof ProgramItem)
1158        {
1159            Map<String, String> translations = new HashMap<>();
1160    
1161            List<Expression> expList = getExpressionsList(importedContent.getLanguage(), importedContent.getMetadataHolder().getString(getIdField()), contentType, importedContent.getMetadataHolder().getString(ProgramItem.METADATA_CATALOG, null));
1162            AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()]));
1163            String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp);
1164    
1165            AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(xPathQuery);
1166            
1167            for (ModifiableDefaultContent content : contents)
1168            {
1169                translations.put(content.getLanguage(), content.getId());
1170                
1171                Map<String, String> translations2 = TranslationHelper.getTranslations(content);
1172                translations2.put(importedContent.getLanguage(), importedContent.getId());
1173                TranslationHelper.setTranslations(content, translations2);
1174    
1175                Map<String, Object> eventParams = new HashMap<>();
1176                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
1177                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
1178                _observationManager.notify(new Event(OdfObservationConstants.ODF_CONTENT_TRANSLATED, _currentUserProvider.getUser(), eventParams));
1179            }
1180            
1181            TranslationHelper.setTranslations(importedContent, translations);
1182        }
1183    }
1184
1185    /**
1186     * Save the changes of the content.
1187     * @param content Content to save
1188     * @param contentTypeId The content type (for logs)
1189     * @param hasChanges If there are changes to save
1190     * @param logger The logger
1191     */
1192    protected void _saveContentChanges(ModifiableDefaultContent content, String contentTypeId, boolean hasChanges, Logger logger)
1193    {
1194        if (hasChanges)
1195        {
1196            content.saveChanges();
1197            _synchronizedContents.add(content.getId());
1198            if (!_importedContents.containsKey(content.getId()))
1199            {
1200                _nbSynchronizedContents++;
1201            }
1202        }
1203        else
1204        {
1205            logger.info("No changes detected for content '{}' with the content type '{}'", content.getTitle(), contentTypeId);
1206            if (!_importedContents.containsKey(content.getId()))
1207            {
1208                _nbNotChangedContents++;
1209            }
1210        }
1211    }
1212    
1213    /**
1214     * Set relations for the content.
1215     * @param content The content to update
1216     * @param children Children to set
1217     * @param logger The logger
1218     * @return <code>true</code> if changes occurs
1219     */
1220    protected boolean _setRelations(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, Logger logger)
1221    {
1222        boolean hasChanges = false;
1223        
1224        if (!children.isEmpty())
1225        {
1226            hasChanges = _setChildren(content, children, logger) || hasChanges;
1227        }
1228    
1229        // Set the orgUnit parent (if no parent is set)
1230        if (content instanceof OrgUnit)
1231        {
1232            hasChanges = _setOrgUnitParent(content, logger);
1233        }
1234        
1235        return hasChanges;
1236    }
1237    
1238    /**
1239     * Set children for the given content.
1240     * @param content Content to add the children
1241     * @param children Children to add
1242     * @param logger The logger
1243     * @return <code>true</code> if changes occurs
1244     */
1245    protected boolean _setChildren(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, Logger logger)
1246    {
1247        boolean hasChanges = false;
1248        
1249        String metadataName = null;
1250        String invertMetadataName = null;
1251        if (content instanceof CourseList)
1252        {
1253            metadataName = CourseList.METADATA_CHILD_COURSES;
1254            invertMetadataName = Course.METADATA_PARENT_COURSE_LISTS;
1255        }
1256        else if (content instanceof TraversableProgramPart)
1257        {
1258            metadataName = TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS;
1259            invertMetadataName = ProgramPart.METADATA_PARENT_PROGRAM_PARTS;
1260        }
1261        else if (content instanceof Course)
1262        {
1263            metadataName = Course.METADATA_CHILD_COURSE_LISTS;
1264            invertMetadataName = CourseList.METADATA_PARENT_COURSES;
1265        }
1266        
1267        if (metadataName != null)
1268        {
1269            hasChanges = _updateDoubleRelation(content, children, metadataName, invertMetadataName, logger);
1270        }
1271        
1272        return hasChanges;
1273    }
1274    
1275    /**
1276     * Set the orgUnit parent to rootOrgUnit.
1277     * @param orgUnit The orgunit to link
1278     * @param logger The logger
1279     * @return <code>true</code> if changes occurs
1280     */
1281    protected boolean _setOrgUnitParent(ModifiableDefaultContent orgUnit, Logger logger)
1282    {
1283        boolean hasChanges = false;
1284        
1285        // Set the orgUnit parent (if no parent is set)
1286        ModifiableCompositeMetadata holder = orgUnit.getMetadataHolder();
1287        if (!holder.hasMetadata(OrgUnit.METADATA_PARENT_ORGUNIT))
1288        {
1289            OrgUnit rootOrgUnit = _rootOUProvider.getRoot();
1290            
1291            hasChanges = ExternalizableMetadataHelper.setMetadata(holder, OrgUnit.METADATA_PARENT_ORGUNIT, rootOrgUnit) || hasChanges;
1292            hasChanges = _synchroComponent.updateRelation(rootOrgUnit.getMetadataHolder(), OrgUnit.METADATA_CHILD_ORGUNITS, orgUnit, false) || hasChanges;
1293
1294            try
1295            {
1296                _synchroComponent.applyChanges(rootOrgUnit, 22, ObservationConstants.EVENT_CONTENT_MODIFIED, logger);
1297            }
1298            catch (RepositoryException e)
1299            {
1300                logger.error("An error occured during updating root org unit after synchronizing the content '{}'.", orgUnit.getId(), logger);
1301            }
1302        }
1303        
1304        return hasChanges;
1305    }
1306
1307    /**
1308     * Get the content ID from the CDM code, if there is no match with the CDM code, then we search with the code.
1309     * If nothing is found we return null. 
1310     * @param tableRefId The reference table ID
1311     * @param cdmCode The CDM code
1312     * @return A content ID or <code>null</code>
1313     */
1314    protected String _getIdFromCDMThenCode(String tableRefId, String cdmCode)
1315    {
1316        OdfReferenceTableEntry entry = _odfRefTableHelper.getItemFromCDM(tableRefId, cdmCode);
1317        if (entry == null)
1318        {
1319            entry = _odfRefTableHelper.getItemFromCode(tableRefId, cdmCode);
1320        }
1321        return entry != null ? entry.getId() : null;
1322    }
1323    
1324    private boolean _updateDoubleRelation(ModifiableDefaultContent content, List<ModifiableDefaultContent> children, String metadataName, String invertMetadataName, Logger logger)
1325    {
1326        boolean hasChanges = false;
1327        ModifiableCompositeMetadata holder = content.getMetadataHolder();
1328        
1329        // "Normal" relation
1330        if (removalSync() || !holder.hasMetadata(metadataName))
1331        {
1332            hasChanges = ExternalizableMetadataHelper.setMetadata(holder, metadataName, children.toArray(new ModifiableDefaultContent[children.size()]));
1333        }
1334        else
1335        {
1336            List<String> oldValues = new ArrayList<>(Arrays.asList(holder.getStringArray(metadataName)));
1337            
1338            for (ModifiableDefaultContent child : children)
1339            {
1340                hasChanges = _synchroComponent.updateRelation(holder, metadataName, child, false) || hasChanges;
1341                oldValues.remove(child.getId());
1342            }
1343            
1344            if (logger.isWarnEnabled())
1345            {
1346                for (String oldValue : oldValues)
1347                {
1348                    // Warn the old contents
1349                    ModifiableDefaultContent child = _resolver.resolveById(oldValue);
1350                    logger.warn("The ODF content '{}' ({}) is not linked anymore to the content '{}', it should be manually removed.", child.getTitle(), oldValue, content.getTitle());
1351                }
1352            }
1353        }
1354        
1355        // Invert relation
1356        for (ModifiableDefaultContent child : children)
1357        {
1358            hasChanges = _synchroComponent.updateRelation(child.getMetadataHolder(), invertMetadataName, content, false) || hasChanges;
1359        }
1360        
1361        return hasChanges;
1362    }
1363    
1364    private String _getXPathString(Node metadataNode, String xPath, String defaultValue)
1365    {
1366        String value = _xPathProcessor.evaluateAsString(metadataNode, xPath);
1367        if (StringUtils.isEmpty(value))
1368        {
1369            value = defaultValue;
1370        }
1371        return value;
1372    }
1373
1374    /**
1375     * Get the program workflow description.
1376     * @return A {@link ContentWorkflowDescription} containing informations about the program workflow
1377     */
1378    protected ContentWorkflowDescription getProgramWfDescription()
1379    {
1380        return _PROGRAM_WF_DESCRIPTION;
1381    }
1382
1383    /**
1384     * Get the subprogram workflow description.
1385     * @return A {@link ContentWorkflowDescription} containing informations about the subprogram workflow
1386     */
1387    protected ContentWorkflowDescription getSubProgramWfDescription()
1388    {
1389        return _SUBPROGRAM_WF_DESCRIPTION;
1390    }
1391
1392    /**
1393     * Get the container workflow description.
1394     * @return A {@link ContentWorkflowDescription} containing informations about the container workflow
1395     */
1396    protected ContentWorkflowDescription getContainerWfDescription()
1397    {
1398        return _CONTAINER_WF_DESCRIPTION;
1399    }
1400
1401    /**
1402     * Get the course list workflow description.
1403     * @return A {@link ContentWorkflowDescription} containing informations about the course list workflow
1404     */
1405    protected ContentWorkflowDescription getCourseListWfDescription()
1406    {
1407        return _COURSELIST_WF_DESCRIPTION;
1408    }
1409
1410    /**
1411     * Get the course workflow description.
1412     * @return A {@link ContentWorkflowDescription} containing informations about the course workflow
1413     */
1414    protected ContentWorkflowDescription getCourseWfDescription()
1415    {
1416        return _COURSE_WF_DESCRIPTION;
1417    }
1418
1419    /**
1420     * Get the orgunit workflow description.
1421     * @return A {@link ContentWorkflowDescription} containing informations about the orgunit workflow
1422     */
1423    protected ContentWorkflowDescription getOrgUnitWfDescription()
1424    {
1425        return _ORGUNIT_WF_DESCRIPTION;
1426    }
1427
1428    /**
1429     * Get the person workflow description.
1430     * @return A {@link ContentWorkflowDescription} containing informations about the person workflow
1431     */
1432    protected ContentWorkflowDescription getPersonWfDescription()
1433    {
1434        return _PERSON_WF_DESCRIPTION;
1435    }
1436    
1437    /**
1438     * Internal object to describe content workflow elements.
1439     */
1440    protected static class ContentWorkflowDescription
1441    {
1442        private String _contentType;
1443        private String _workflowName;
1444        private int _initialActionId;
1445        private int _validationActionId;
1446        
1447        ContentWorkflowDescription(String contentType, String workflowName, int initialActionId, int validationActionId)
1448        {
1449            _contentType = contentType;
1450            _workflowName = workflowName;
1451            _initialActionId = initialActionId;
1452            _validationActionId = validationActionId;
1453        }
1454        
1455        /**
1456         * Get the content type.
1457         * @return the content type ID
1458         */
1459        public String getContentType()
1460        {
1461            return _contentType;
1462        }
1463        
1464        /**
1465         * Get the workflow name.
1466         * @return the workflow name
1467         */
1468        public String getWorkflowName()
1469        {
1470            return _workflowName;
1471        }
1472        
1473        /**
1474         * Get the initial action ID.
1475         * @return the initial action ID
1476         */
1477        public int getInitialActionId()
1478        {
1479            return _initialActionId;
1480        }
1481        
1482        /**
1483         * Get the validation action ID.
1484         * @return the validation action ID
1485         */
1486        public int getValidationActionId()
1487        {
1488            return _validationActionId;
1489        }
1490    }
1491    
1492    private static class DocbookPrefixResolver implements PrefixResolver
1493    {
1494        private Map<String, String> _ns = new HashMap<>();
1495        
1496        public DocbookPrefixResolver()
1497        {
1498            _ns.put("docbook", "http://docbook.org/ns/docbook");
1499        }
1500        
1501        @Override
1502        public String prefixToNamespace(String prefix)
1503        {
1504            return _ns.get(prefix);
1505        }
1506    }
1507    
1508    @Override
1509    public List<Expression> getExpressionsList(String lang, String idValue, String contentType, String catalog)
1510    {
1511        List<Expression> expList = new ArrayList<>();
1512        
1513        if (StringUtils.isNotBlank(contentType))
1514        {
1515            expList.add(new ContentTypeExpression(Operator.EQ, contentType));
1516        }
1517        
1518        if (StringUtils.isNotBlank(idValue))
1519        {
1520            expList.add(new StringExpression(getIdField(), Operator.EQ, idValue));
1521        }
1522        
1523        if (StringUtils.isNotBlank(catalog))
1524        {
1525            expList.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalog));
1526        }
1527        
1528        if (StringUtils.isNotBlank(lang))
1529        {
1530            expList.add(new LanguageExpression(Operator.EQ, lang));
1531        }
1532        
1533        return expList;
1534    }
1535
1536    @Override
1537    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
1538    {
1539        if (additionalParameters == null || !additionalParameters.containsKey("contentType"))
1540        {
1541            throw new IllegalArgumentException("Content type shouldn't be null.");
1542        }
1543
1544        String contentType = additionalParameters.get("contentType").toString();
1545        Set<String> syncFields = _syncFieldsByContentType.get(contentType);
1546        if (syncFields == null)
1547        {
1548            syncFields = new HashSet<>();
1549            _syncFieldsByContentType.put(contentType, syncFields);
1550        }
1551        return syncFields;
1552    }
1553}