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