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