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