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.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.Set;
029
030import org.apache.avalon.framework.activity.Initializable;
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
036import org.apache.avalon.framework.context.ContextException;
037import org.apache.avalon.framework.context.Contextualizable;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.Constants;
042import org.apache.cocoon.ProcessingException;
043import org.apache.cocoon.environment.Context;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.excalibur.xml.dom.DOMParser;
046import org.apache.excalibur.xml.xpath.XPathProcessor;
047import org.slf4j.Logger;
048import org.w3c.dom.Document;
049import org.w3c.dom.Element;
050import org.w3c.dom.Node;
051import org.w3c.dom.NodeList;
052import org.xml.sax.InputSource;
053import org.xml.sax.SAXException;
054
055import org.ametys.cms.ObservationConstants;
056import org.ametys.cms.contenttype.ContentType;
057import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
058import org.ametys.cms.data.ContentSynchronizationResult;
059import org.ametys.cms.repository.ContentQueryHelper;
060import org.ametys.cms.repository.ContentTypeExpression;
061import org.ametys.cms.repository.LanguageExpression;
062import org.ametys.cms.repository.ModifiableContent;
063import org.ametys.cms.repository.WorkflowAwareContent;
064import org.ametys.cms.workflow.ContentWorkflowHelper;
065import org.ametys.cms.workflow.EditContentFunction;
066import org.ametys.core.observation.Event;
067import org.ametys.core.observation.ObservationManager;
068import org.ametys.core.user.CurrentUserProvider;
069import org.ametys.core.user.population.UserPopulationDAO;
070import org.ametys.odf.ODFHelper;
071import org.ametys.odf.ProgramItem;
072import org.ametys.odf.catalog.CatalogsManager;
073import org.ametys.odf.course.Course;
074import org.ametys.odf.course.ShareableCourseHelper;
075import org.ametys.odf.courselist.CourseList;
076import org.ametys.odf.coursepart.CoursePart;
077import org.ametys.odf.enumeration.OdfReferenceTableEntry;
078import org.ametys.odf.enumeration.OdfReferenceTableHelper;
079import org.ametys.odf.observation.OdfObservationConstants;
080import org.ametys.odf.orgunit.OrgUnit;
081import org.ametys.odf.orgunit.RootOrgUnitProvider;
082import org.ametys.odf.program.Program;
083import org.ametys.odf.translation.TranslationHelper;
084import org.ametys.odf.workflow.AbstractCreateODFContentFunction;
085import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
086import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDataProvider;
087import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionHelper;
088import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction;
089import org.ametys.plugins.odfsync.cdmfr.CDMFrSyncExtensionPoint;
090import org.ametys.plugins.odfsync.cdmfr.ImportCDMFrContext;
091import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractor;
092import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractorFactory;
093import org.ametys.plugins.odfsync.cdmfr.transformers.CDMFrSyncTransformer;
094import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription;
095import org.ametys.plugins.repository.AmetysObjectIterable;
096import org.ametys.plugins.repository.AmetysObjectResolver;
097import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
098import org.ametys.plugins.repository.data.extractor.ModelAwareValuesExtractor;
099import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
100import org.ametys.plugins.repository.jcr.NameHelper;
101import org.ametys.plugins.repository.lock.LockAwareAmetysObject;
102import org.ametys.plugins.repository.query.expression.AndExpression;
103import org.ametys.plugins.repository.query.expression.Expression;
104import org.ametys.plugins.repository.query.expression.Expression.Operator;
105import org.ametys.plugins.repository.query.expression.OrExpression;
106import org.ametys.plugins.repository.query.expression.StringExpression;
107import org.ametys.plugins.workflow.AbstractWorkflowComponent;
108import org.ametys.plugins.workflow.AbstractWorkflowComponent.ConditionFailure;
109import org.ametys.plugins.workflow.component.CheckRightsCondition;
110import org.ametys.runtime.config.Config;
111import org.ametys.runtime.model.ModelItem;
112import org.ametys.runtime.model.View;
113
114import com.opensymphony.workflow.InvalidActionException;
115import com.opensymphony.workflow.WorkflowException;
116
117/**
118 * Abstract class of a component to import a CDM-fr input stream.
119 */
120public abstract class AbstractImportCDMFrComponent implements ImportCDMFrComponent, Serviceable, Initializable, Contextualizable, Configurable, Component
121{
122    /** Tag to identify a program */
123    protected static final String _TAG_PROGRAM = "program";
124
125    /** Tag to identify a subprogram */
126    protected static final String _TAG_SUBPROGRAM = "subProgram";
127    
128    /** Tag to identify a container */
129    protected static final String _TAG_CONTAINER = "container";
130    
131    /** Tag to identify a courseList */
132    protected static final String _TAG_COURSELIST = "coursesReferences";
133    
134    /** Tag to identify a coursePart */
135    protected static final String _TAG_COURSEPART = "coursePart";
136
137    /** The synchronize workflow action id */
138    protected static final int _SYNCHRONIZE_WORKFLOW_ACTION_ID = 800;
139    
140    /** The Cocoon context */
141    protected Context _cocoonContext;
142    
143    /** The DOM parser */
144    protected DOMParser _domParser;
145
146    /** The XPath processor */
147    protected XPathProcessor _xPathProcessor;
148
149    /** Extension point to transform CDM-fr */
150    protected CDMFrSyncExtensionPoint _cdmFrSyncExtensionPoint;
151
152    /** Default language configured for ODF */
153    protected String _odfLang;
154    
155    /** The catalog manager */
156    protected CatalogsManager _catalogsManager;
157
158    /** The ametys object resolver */
159    protected AmetysObjectResolver _resolver;
160
161    /** The ODF TableRef Helper */
162    protected OdfReferenceTableHelper _odfRefTableHelper;
163
164    /** The content type extension point */
165    protected ContentTypeExtensionPoint _contentTypeEP;
166
167    /** The current user provider */
168    protected CurrentUserProvider _currentUserProvider;
169
170    /** The observation manager */
171    protected ObservationManager _observationManager;
172    
173    /** The root orgunit provider */
174    protected RootOrgUnitProvider _rootOUProvider;
175
176    /** The ODF Helper */
177    protected ODFHelper _odfHelper;
178    
179    /** The SCC helper */
180    protected SynchronizableContentsCollectionHelper _sccHelper;
181    
182    /** The content workflow helper */
183    protected ContentWorkflowHelper _contentWorkflowHelper;
184    
185    /** The shareable course helper */
186    protected ShareableCourseHelper _shareableCourseHelper;
187    
188    /** the {@link ImportCDMFrValuesExtractor} factory */
189    protected ImportCDMFrValuesExtractorFactory _valuesExtractorFactory;
190    
191    
192    /** List of imported contents */
193    protected Map<String, Integer> _importedContents;
194    
195    /** List of synchronized contents */
196    protected Set<String> _synchronizedContents;
197
198    /** Number of errors encountered */
199    protected int _nbError;
200    /** The prefix of the contents */
201    protected String _contentPrefix;
202    /** Synchronized fields by content type */
203    protected Map<String, Set<String>> _syncFieldsByContentType;
204
205    public void initialize() throws Exception
206    {
207        _odfLang = Config.getInstance().getValue("odf.programs.lang");
208    }
209
210    @Override
211    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
212    {
213        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
214    }
215
216    public void configure(Configuration configuration) throws ConfigurationException
217    {
218        _parseSynchronizedFields();
219    }
220    
221    @Override
222    public void service(ServiceManager manager) throws ServiceException
223    {
224        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
225        _domParser = (DOMParser) manager.lookup(DOMParser.ROLE);
226        _xPathProcessor = (XPathProcessor) manager.lookup(XPathProcessor.ROLE);
227        _cdmFrSyncExtensionPoint = (CDMFrSyncExtensionPoint) manager.lookup(CDMFrSyncExtensionPoint.ROLE);
228        _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE);
229        _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
230        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
231        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
232        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
233        _rootOUProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
234        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
235        _sccHelper = (SynchronizableContentsCollectionHelper) manager.lookup(SynchronizableContentsCollectionHelper.ROLE);
236        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
237        _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE);
238        _valuesExtractorFactory = (ImportCDMFrValuesExtractorFactory) manager.lookup(ImportCDMFrValuesExtractorFactory.ROLE);
239    }
240
241    @Override
242    public String getIdField()
243    {
244        return "cdmfrSyncCode";
245    }
246
247    /**
248     * Get the synchronized metadata from the configuration file
249     * @throws ConfigurationException if the configuration is not valid.
250     */
251    private void _parseSynchronizedFields() throws ConfigurationException
252    {
253        _syncFieldsByContentType = new HashMap<>();
254        
255        File cdmfrMapping = new File(_cocoonContext.getRealPath("/WEB-INF/param/odf-synchro.xml"));
256        try (InputStream is = !cdmfrMapping.isFile()
257                            ? getClass().getResourceAsStream("/org/ametys/plugins/odfsync/cdmfr/odf-synchro.xml")
258                            : new FileInputStream(cdmfrMapping))
259        {
260            Configuration cfg = new DefaultConfigurationBuilder().build(is);
261
262            Configuration[] cTypesConf = cfg.getChildren("content-type");
263            for (Configuration cTypeConf : cTypesConf)
264            {
265                String contentType = cTypeConf.getAttribute("id");
266                Set<String> syncAttributes = _configureSynchronizedFields(cTypeConf, StringUtils.EMPTY);
267                _syncFieldsByContentType.put(contentType, syncAttributes);
268            }
269        }
270        catch (Exception e)
271        {
272            throw new ConfigurationException("Error while parsing odf-synchro.xml", e);
273        }
274    }
275
276    private Set<String> _configureSynchronizedFields(Configuration configuration, String prefix) throws ConfigurationException
277    {
278        Set<String> syncAttributes = new HashSet<>();
279        Configuration[] attributesConf = configuration.getChildren("attribute");
280        
281        if (attributesConf.length > 0)
282        {
283            for (Configuration attributeConf : attributesConf)
284            {
285                if (attributeConf.getChildren("attribute").length > 0)
286                {
287                    // composite
288                    syncAttributes.addAll(_configureSynchronizedFields(attributeConf, prefix + attributeConf.getAttribute("name") + ModelItem.ITEM_PATH_SEPARATOR));
289                }
290                else
291                {
292                    syncAttributes.add(prefix + attributeConf.getAttribute("name"));
293                }
294            }
295        }
296        else if (configuration.getAttribute("name", null) != null)
297        {
298            syncAttributes.add(prefix + configuration.getAttribute("name"));
299        }
300        
301        return syncAttributes;
302    }
303    
304    @Override
305    @SuppressWarnings("unchecked")
306    public synchronized Map<String, Object> handleInputStream(InputStream input, Map<String, Object> parameters, SynchronizableContentsCollection scc, Logger logger) throws ProcessingException
307    {
308        List<ModifiableContent> importedPrograms = new ArrayList<>();
309        
310        _importedContents = new HashMap<>();
311        _synchronizedContents = (Set<String>) parameters.getOrDefault("updatedContents", new HashSet<>());
312        int nbCreatedContents = (int) parameters.getOrDefault("nbCreatedContents", 0);
313        int nbSynchronizedContents = (int) parameters.getOrDefault("nbSynchronizedContents", 0);
314        _nbError = (int) parameters.getOrDefault("nbError", 0);
315        _contentPrefix = (String) parameters.getOrDefault("contentPrefix", "cdmfr-");
316        additionalParameters(parameters);
317
318        Map<String, Object> resultMap = new HashMap<>();
319        
320        try
321        {
322            Document doc = _domParser.parseDocument(new InputSource(input));
323            doc = transformDocument(doc, new HashMap<>(), logger);
324            
325            if (doc != null)
326            {
327                String defaultLang = _getXPathString(doc, "CDM/@language", _odfLang);
328                
329                NodeList nodes = doc.getElementsByTagName(_TAG_PROGRAM);
330                
331                for (int i = 0; i < nodes.getLength(); i++)
332                {
333                    Element contentElement = (Element) nodes.item(i);
334                    String syncCode = _xPathProcessor.evaluateAsString(contentElement, "@CDMid");
335                    String contentLang = _getXPathString(contentElement, "@language", defaultLang);
336                    contentLang = StringUtils.substring(contentLang, 0, 2).toLowerCase(); // on keep the language from the locale
337                    
338                    String catalog = getCatalogName(contentElement);
339
340                    ImportCDMFrContext context = new ImportCDMFrContext(scc, doc, contentLang, catalog, logger);
341                    importedPrograms.add(importOrSynchronizeContent(contentElement, ContentWorkflowDescription.PROGRAM_WF_DESCRIPTION, syncCode, syncCode, context));
342                }
343                
344                // Validate newly imported contents
345                if (validateAfterImport())
346                {
347                    for (String contentId : _importedContents.keySet())
348                    {
349                        WorkflowAwareContent content = _resolver.resolveById(contentId);
350                        Integer validationActionId = _importedContents.get(contentId);
351                        if (validationActionId > 0)
352                        {
353                            validateContent(content, validationActionId, logger);
354                        }
355                    }
356                }
357            }
358        }
359        catch (IOException | ProcessingException e)
360        {
361            throw new ProcessingException("An error occured while transforming the stream.", e);
362        }
363        catch (SAXException e)
364        {
365            throw new ProcessingException("An error occured while parsing the stream.", e);
366        }
367        catch (Exception e)
368        {
369            throw new ProcessingException("An error occured while synchronizing values on contents.", e);
370        }
371
372        resultMap.put("importedContents", _importedContents.keySet());
373        resultMap.put("nbCreatedContents", nbCreatedContents + _importedContents.size());
374        resultMap.put("updatedContents", _synchronizedContents);
375        resultMap.put("nbSynchronizedContents", nbSynchronizedContents + _synchronizedContents.size());
376        resultMap.put("nbError", _nbError);
377        resultMap.put("importedPrograms", importedPrograms);
378
379        return resultMap;
380    }
381
382    /**
383     * True to validate the contents after import
384     * @return True to validate the contents after import
385     */
386    protected abstract boolean validateAfterImport();
387
388    /**
389     * When returns true, a content created by a previous synchro will be removed if it does not exist anymore during the current synchro.
390     * @return true if a content created by a previous synchro has to be removed if it does not exist anymore during the current synchro.
391     */
392    protected abstract boolean removalSync();
393    
394    /**
395     * Additional parameters for specific treatments.
396     * @param parameters The parameters map to get
397     */
398    protected abstract void additionalParameters(Map<String, Object> parameters);
399    
400    /**
401     * Transform the document depending of it structure.
402     * @param document Document to transform.
403     * @param parameters Optional parameters for transformation
404     * @param logger The logger
405     * @return The transformed document.
406     * @throws IOException if an error occurs.
407     * @throws SAXException if an error occurs.
408     * @throws ProcessingException if an error occurs.
409     */
410    protected Document transformDocument(Document document, Map<String, Object> parameters, Logger logger) throws IOException, SAXException, ProcessingException
411    {
412        Optional<CDMFrSyncTransformer> transformer = _cdmFrSyncExtensionPoint.getFirstSupportingExtension(document);
413        
414        if (transformer.isEmpty())
415        {
416            logger.error("Cannot match a CDM-fr transformer to this file structure.");
417            return null;
418        }
419        
420        return transformer.get().transform(document, parameters);
421    }
422
423    public String getCatalogName(Element contentElement)
424    {
425        String defaultCatalog = _catalogsManager.getDefaultCatalogName();
426        
427        String contentCatalog = _getXPathString(contentElement, "catalog", defaultCatalog);
428        if (_catalogsManager.getCatalog(contentCatalog) == null)
429        {
430            // Catalog is empty or do not exist, use the default catalog
431            return defaultCatalog;
432        }
433        
434        return contentCatalog;
435    }
436    
437    public ModifiableContent importOrSynchronizeContent(Element contentElement, ContentWorkflowDescription wfDescription, String title, String syncCode, ImportCDMFrContext context)
438    {
439        ModifiableContent content = _getOrCreateContent(wfDescription, title, syncCode, context);
440        
441        if (content != null)
442        {
443            try
444            {
445                _sccHelper.updateLastSynchronizationProperties(content);
446                content.saveChanges();
447                _synchronizeContent(contentElement, content, wfDescription.getContentType(), syncCode, context);
448            }
449            catch (Exception e)
450            {
451                _nbError++;
452                context.getLogger().error("Failed to synchronize data for content {} and language {}.", content, context.getLang(), e);
453            }
454        }
455        
456        return content;
457    }
458    
459    /**
460     * Get or create the content from the workflow description, the synchronization code and the import context.
461     * @param wfDescription The workflow description
462     * @param title The title
463     * @param syncCode The synchronization code
464     * @param context The import context
465     * @return the retrieved or created content
466     */
467    protected ModifiableContent _getOrCreateContent(ContentWorkflowDescription wfDescription, String title, String syncCode, ImportCDMFrContext context)
468    {
469        ModifiableContent receivedContent = getContent(wfDescription.getContentType(), syncCode, context);
470        if (receivedContent != null)
471        {
472            return receivedContent;
473        }
474        
475        try
476        {
477            context.getLogger().info("Creating content '{}' with the content type '{}' for language {}", title, wfDescription.getContentType(), context.getLang());
478            
479            ModifiableContent content = _createContent(wfDescription, title, context);
480            if (content != null)
481            {
482                _sccHelper.updateSCCProperty(content, context.getSCC().getId());
483                content.setValue(getIdField(), syncCode);
484                content.saveChanges();
485                _importedContents.put(content.getId(), wfDescription.getValidationActionId());
486            }
487            
488            return content;
489        }
490        catch (WorkflowException e)
491        {
492            context.getLogger().error("Failed to initialize workflow for content {} and language {}", title, context.getLang(), e);
493            _nbError++;
494            return null;
495        }
496    }
497
498    public ModifiableContent getContent(String contentType, String syncCode, ImportCDMFrContext context)
499    {
500        List<Expression> expList = _getExpressionsList(contentType, syncCode, context);
501        AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()]));
502        String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp);
503
504        AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery);
505
506        if (contents.getSize() > 0)
507        {
508            return contents.iterator().next();
509        }
510        
511        return null;
512    }
513    
514    /**
515     * Create the content from the workflow description and the import context.
516     * @param wfDescription The workflow description
517     * @param title The title
518     * @param context The import context
519     * @return the created content
520     * @throws WorkflowException if an error occurs while creating the content
521     */
522    protected ModifiableContent _createContent(ContentWorkflowDescription wfDescription, String title, ImportCDMFrContext context) throws WorkflowException
523    {
524        String contentName = NameHelper.filterName(_contentPrefix + "-" + title + "-" + context.getLang());
525        
526        Map<String, Object> inputs = _getInputsForContentCreation(wfDescription, context);
527        Map<String, Object> result = _contentWorkflowHelper.createContent(
528                wfDescription.getWorkflowName(),
529                wfDescription.getInitialActionId(),
530                contentName,
531                title,
532                new String[] {wfDescription.getContentType()},
533                null,
534                context.getLang(),
535                inputs);
536        
537        return _resolver.resolveById((String) result.get("contentId"));
538    }
539    
540    /**
541     * Retrieves the inputs to give for content creation
542     * @param wfDescription The workflow description
543     * @param context The import context
544     * @return the inputs to give for content creation
545     */
546    protected Map<String, Object> _getInputsForContentCreation(ContentWorkflowDescription wfDescription, ImportCDMFrContext context)
547    {
548        Map<String, Object> inputs = new HashMap<>();
549        
550        ContentType contentType = _contentTypeEP.getExtension(wfDescription.getContentType());
551        if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG))
552        {
553            inputs.put(AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY, context.getCatalog());
554        }
555        
556        return inputs;
557    }
558    
559    /**
560     * Synchronize content
561     * @param contentElement the DOM content element
562     * @param content The content to synchronize
563     * @param contentTypeId The content type ID
564     * @param syncCode The synchronization code
565     * @param context the import context
566     * @throws Exception if an error occurs while synchronizing the content values
567     */
568    protected void _synchronizeContent(Element contentElement, ModifiableContent content, String contentTypeId, String syncCode, ImportCDMFrContext context) throws Exception
569    {
570        Logger logger = context.getLogger();
571        
572        // Avoid a treatment twice or more
573        if (_synchronizedContents.add(content.getId()))
574        {
575            logger.info("Synchronization of the content '{}' with the content type '{}'", content.getTitle(), contentTypeId);
576            
577            if (content instanceof LockAwareAmetysObject && ((LockAwareAmetysObject) content).isLocked())
578            {
579                logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), ((LockAwareAmetysObject) content).getLockOwner());
580            }
581            else if (content instanceof WorkflowAwareContent)
582            {
583                ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
584                ModelAwareValuesExtractor valuesExtractor = _valuesExtractorFactory.getValuesExtractor(contentElement, this, content, contentType, syncCode, context);
585                
586                // Extract the values
587                Map<String, Object> values = valuesExtractor.extractValues();
588                values.putAll(_getAdditionalValuesToSynchronize(content, syncCode, context));
589                
590                // Modify the content with the extracted values
591                boolean create = _importedContents.containsKey(content.getId());
592                Set<String> notSynchronizedContentIds = _getNotSynchronizedRelatedContentIds(content, syncCode, context);
593                _editContent((WorkflowAwareContent) content, Optional.empty(), values, create, notSynchronizedContentIds, context);
594                
595                if (content instanceof OrgUnit)
596                {
597                    _setOrgUnitParent((WorkflowAwareContent) content, context);
598                }
599                
600                // Create translation links
601                _linkTranslationsIfExist(content, contentTypeId, context);
602            }
603        }
604    }
605
606    /**
607     * Retrieves additional values to synchronize for the content
608     * @param content the content
609     * @param syncCode the content synchronization code
610     * @param context the import context
611     * @return the additional values
612     */
613    protected Map<String, Object> _getAdditionalValuesToSynchronize(ModifiableContent content, String syncCode, ImportCDMFrContext context)
614    {
615        Map<String, Object> additionalValues = new HashMap<>();
616        additionalValues.put(getIdField(), syncCode);
617        return additionalValues;
618    }
619    
620    /**
621     * Retrieves the ids of the contents related to the given content but that are not part of the synchronization
622     * @param content the content
623     * @param syncCode the content synchronization code
624     * @param context the import context
625     * @return the not synchronized content ids
626     */
627    protected Set<String> _getNotSynchronizedRelatedContentIds(ModifiableContent content, String syncCode, ImportCDMFrContext context)
628    {
629        return new HashSet<>();
630    }
631    
632    /**
633     * Synchronize the content with given values.
634     * @param content The content to synchronize
635     * @param view the view containing the item to edit
636     * @param values the values
637     * @param create <code>true</code> if content is creating, false if it is updated
638     * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization
639     * @param context the import context
640     * @throws WorkflowException if an error occurs
641     */
642    protected void _editContent(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, boolean create, Set<String> notSynchronizedContentIds, ImportCDMFrContext context) throws WorkflowException
643    {
644        SynchronizationContext synchronizationContext = SynchronizationContext.newInstance()
645                                                                              .withStatus(ExternalizableDataStatus.EXTERNAL)
646                                                                              .withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, context.getSCC().getId())
647                                                                              .withIncompatibleValuesIgnored(true);
648
649        if (view.map(v -> content.hasDifferences(v, values, synchronizationContext))
650                .orElseGet(() -> content.hasDifferences(values, synchronizationContext)))
651        {
652            context.getLogger().info("Some changes were detected for content '{}' and language {}", content.getTitle(), context.getLang());
653            
654            Map<String, Object> inputs = new HashMap<>();
655            inputs.put(EditSynchronizedContentFunction.SCC_KEY, context.getSCC());
656            inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, context.getLogger());
657            inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds);
658            if (ignoreRights())
659            {
660                inputs.put(CheckRightsCondition.FORCE, true);
661            }
662    
663            Map<String, Object> params = new HashMap<>();
664            // Remove catalog data, this value is forced at creation and should not be modified
665            values.remove(ProgramItem.CATALOG);
666            params.put(EditContentFunction.VALUES_KEY, values);
667            view.ifPresent(v -> params.put(EditContentFunction.VIEW, v));
668            params.put(EditContentFunction.QUIT, true);
669            params.put(EditSynchronizedContentFunction.IMPORT, create);
670            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, params);
671            
672            _contentWorkflowHelper.doAction(content, _SYNCHRONIZE_WORKFLOW_ACTION_ID, inputs);
673        }
674        else
675        {
676            context.getLogger().info("No changes detected for content '{}' and language {}", content.getTitle(), context.getLang());
677        }
678    }
679    
680    public ContentSynchronizationResult additionalOperations(ModifiableContent content, Map<String, Object> additionalParameters, Logger logger)
681    {
682        ContentSynchronizationResult result = new ContentSynchronizationResult();
683
684        if (content instanceof Program)
685        {
686            List<ModifiableContent> modifiedContents = _initializeShareableCoursesFields((Program) content);
687            
688            result.addModifiedContents(modifiedContents);
689            result.setHasChanged(!modifiedContents.isEmpty());
690        }
691        
692        return result;
693    }
694    
695    /**
696     * Initialize shareable fields for the courses under the given {@link ProgramItem}
697     * @param programItem the program item
698     * @return the list of contents that have been modified during the initialization
699     */
700    protected List<ModifiableContent> _initializeShareableCoursesFields(ProgramItem programItem)
701    {
702        List<ModifiableContent> modifiedContents = new ArrayList<>();
703
704        List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem);
705        for (ProgramItem child : children)
706        {
707            if (child instanceof Course && programItem instanceof CourseList)
708            {
709                if (_shareableCourseHelper.initializeShareableFields((Course) child, (CourseList) programItem,  UserPopulationDAO.SYSTEM_USER_IDENTITY, true))
710                {
711                    modifiedContents.add((Course) child);
712                }
713            }
714            
715            modifiedContents.addAll(_initializeShareableCoursesFields(child));
716        }
717
718        return modifiedContents;
719    }
720    
721    /**
722     * Search for translated contents
723     * @param importedContent The imported content
724     * @param contentType The content type
725     * @param context the import context
726     */
727    protected void _linkTranslationsIfExist(ModifiableContent importedContent, String contentType, ImportCDMFrContext context)
728    {
729        if (importedContent instanceof ProgramItem)
730        {
731            Expression expression = _getTranslationExpression(importedContent, contentType);
732            String xPathQuery = ContentQueryHelper.getContentXPathQuery(expression);
733    
734            AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery);
735            
736            Map<String, String> translations = new HashMap<>();
737            for (ModifiableContent content : contents)
738            {
739                translations.put(content.getLanguage(), content.getId());
740            }
741            
742            for (ModifiableContent content : contents)
743            {
744                TranslationHelper.setTranslations(content, translations);
745    
746                Map<String, Object> eventParams = new HashMap<>();
747                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
748                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
749                _observationManager.notify(new Event(OdfObservationConstants.ODF_CONTENT_TRANSLATED, _currentUserProvider.getUser(), eventParams));
750            }
751        }
752    }
753    
754    private Expression _getTranslationExpression(ModifiableContent content, String contentType)
755    {
756        List<Expression> expList = new ArrayList<>();
757        
758        if (StringUtils.isNotBlank(contentType))
759        {
760            expList.add(new ContentTypeExpression(Operator.EQ, contentType));
761        }
762        
763        String catalog = content.getValue(ProgramItem.CATALOG);
764        if (StringUtils.isNotBlank(catalog))
765        {
766            expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
767        }
768        
769        List<Expression> codeExpressionList = new ArrayList<>();
770        String syncValue = content.getValue(getIdField());
771        if (StringUtils.isNotBlank(syncValue))
772        {
773            codeExpressionList.add(new StringExpression(getIdField(), Operator.EQ, syncValue));
774        }
775
776        String code = content.getValue(ProgramItem.CODE);
777        if (StringUtils.isNotBlank(syncValue))
778        {
779            codeExpressionList.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code));
780        }
781        
782        if (!codeExpressionList.isEmpty())
783        {
784            expList.add(new OrExpression(codeExpressionList.toArray(Expression[]::new)));
785        }
786        
787        return new AndExpression(expList.toArray(Expression[]::new));
788    }
789
790    /**
791     * Set the orgUnit parent to rootOrgUnit.
792     * @param orgUnit The orgunit to link
793     * @param context the import context
794     * @throws Exception if an error occurs while synchronizing the content values
795     */
796    protected void _setOrgUnitParent(WorkflowAwareContent orgUnit, ImportCDMFrContext context) throws Exception
797    {
798        // Set the orgUnit parent (if no parent is set)
799        if (!orgUnit.hasValue(OrgUnit.PARENT_ORGUNIT))
800        {
801            OrgUnit rootOrgUnit = _rootOUProvider.getRoot();
802            Map<String, Object> values = new HashMap<>();
803            values.put(OrgUnit.PARENT_ORGUNIT, rootOrgUnit);
804            _editContent(orgUnit, Optional.empty(), values, false, Set.of(rootOrgUnit.getId()), context);
805        }
806    }
807    
808    /**
809     * Validates a content after import
810     * @param content The content to validate
811     * @param validationActionId Validation action ID to use for this content
812     * @param logger The logger
813     */
814    protected void validateContent(WorkflowAwareContent content, int validationActionId, Logger logger)
815    {
816        Map<String, Object> inputs = new HashMap<>();
817        if (ignoreRights())
818        {
819            inputs.put(CheckRightsCondition.FORCE, true);
820        }
821        
822        try
823        {
824            _contentWorkflowHelper.doAction(content, validationActionId, inputs);
825            logger.info("The content {} has been validated after import", content);
826        }
827        catch (WorkflowException | InvalidActionException e)
828        {
829            String failuresAsString = _getActionFailuresAsString(inputs);
830            logger.error("The content {} cannot be validated after import{}", content, failuresAsString, e);
831        }
832    }
833    
834    private String _getActionFailuresAsString(Map<String, Object> actionInputs)
835    {
836        String failuresAsString = "";
837        if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY))
838        {
839            @SuppressWarnings("unchecked")
840            List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.get(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY);
841            if (!failures.isEmpty())
842            {
843                failuresAsString = ", due to the following error(s):\n" + String.join("\n", failures.stream().map(ConditionFailure::text).toList());
844            }
845        }
846        
847        return failuresAsString;
848    }
849    
850    public String getIdFromCDMThenCode(String tableRefId, String cdmCode)
851    {
852        OdfReferenceTableEntry entry = _odfRefTableHelper.getItemFromCDM(tableRefId, cdmCode);
853        if (entry == null)
854        {
855            entry = _odfRefTableHelper.getItemFromCode(tableRefId, cdmCode);
856        }
857        return entry != null ? entry.getId() : null;
858    }
859    
860    private String _getXPathString(Node metadataNode, String xPath, String defaultValue)
861    {
862        String value = _xPathProcessor.evaluateAsString(metadataNode, xPath);
863        if (StringUtils.isEmpty(value))
864        {
865            value = defaultValue;
866        }
867        return value;
868    }
869
870    /**
871     * If true, bypass the rights check during the import process
872     * @return True if the rights check are bypassed during the import process
873     */
874    protected boolean ignoreRights()
875    {
876        return false;
877    }
878    
879    /**
880     * Construct the query to retrieve the content.
881     * @param contentTypeId The content type
882     * @param syncCode The synchronization code
883     * @param context the import context
884     * @return The {@link List} of {@link Expression}
885     */
886    protected List<Expression> _getExpressionsList(String contentTypeId, String syncCode, ImportCDMFrContext context)
887    {
888        List<Expression> expList = new ArrayList<>();
889        
890        if (StringUtils.isNotBlank(contentTypeId))
891        {
892            expList.add(new ContentTypeExpression(Operator.EQ, contentTypeId));
893            
894            if (StringUtils.isNotBlank(context.getCatalog()))
895            {
896                ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
897                if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG))
898                {
899                    expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, context.getCatalog()));
900                }
901            }
902        }
903        
904        if (StringUtils.isNotBlank(syncCode))
905        {
906            expList.add(new StringExpression(getIdField(), Operator.EQ, syncCode));
907        }
908        
909        if (StringUtils.isNotBlank(context.getLang()))
910        {
911            expList.add(new LanguageExpression(Operator.EQ, context.getLang()));
912        }
913        
914        return expList;
915    }
916
917    @Override
918    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
919    {
920        if (additionalParameters == null || !additionalParameters.containsKey("contentTypes"))
921        {
922            throw new IllegalArgumentException("Content types shouldn't be null.");
923        }
924
925        @SuppressWarnings("unchecked")
926        List<String> contentTypeIds = (List<String>) additionalParameters.get("contentTypes");
927        Set<String> allSyncFields = new HashSet<>();
928        
929        for (String contentTypeId : contentTypeIds)
930        {
931            Set<String> syncFields = _syncFieldsByContentType.computeIfAbsent(contentTypeId, k -> new HashSet<>());
932            allSyncFields.addAll(syncFields);
933        }
934        
935        return allSyncFields;
936    }
937}