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.SynchronizableContentsCollectionHelper;
089import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction;
090import org.ametys.plugins.odfsync.cdmfr.CDMFrSyncExtensionPoint;
091import org.ametys.plugins.odfsync.cdmfr.ImportCDMFrContext;
092import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractor;
093import org.ametys.plugins.odfsync.cdmfr.extractor.ImportCDMFrValuesExtractorFactory;
094import org.ametys.plugins.odfsync.cdmfr.transformers.CDMFrSyncTransformer;
095import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription;
096import org.ametys.plugins.repository.AmetysObjectIterable;
097import org.ametys.plugins.repository.AmetysObjectResolver;
098import org.ametys.plugins.repository.data.extractor.ModelAwareValuesExtractor;
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.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.setValue(getIdField(), syncCode);
479                content.saveChanges();
480                _importedContents.put(content.getId(), wfDescription.getValidationActionId());
481            }
482            
483            return content;
484        }
485        catch (WorkflowException | RepositoryException e)
486        {
487            context.getLogger().error("Failed to initialize workflow for content {} and language {}", title, context.getLang(), e);
488            _nbError++;
489            return null;
490        }
491    }
492
493    public ModifiableContent getContent(String contentType, String syncCode, ImportCDMFrContext context)
494    {
495        List<Expression> expList = _getExpressionsList(contentType, syncCode, context);
496        AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()]));
497        String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp);
498
499        AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery);
500
501        if (contents.getSize() > 0)
502        {
503            return contents.iterator().next();
504        }
505        
506        return null;
507    }
508    
509    /**
510     * Create the content from the workflow description and the import context.
511     * @param wfDescription The workflow description
512     * @param title The title
513     * @param context The import context
514     * @return the created content
515     * @throws WorkflowException if an error occurs while creating the content 
516     */
517    protected ModifiableContent _createContent(ContentWorkflowDescription wfDescription, String title, ImportCDMFrContext context) throws WorkflowException
518    {
519        String contentName = NameHelper.filterName(_contentPrefix + "-" + title + "-" + context.getLang());
520        
521        Map<String, Object> inputs = _getInputsForContentCreation(wfDescription, context);
522        Map<String, Object> result = _contentWorkflowHelper.createContent(
523                wfDescription.getWorkflowName(),
524                wfDescription.getInitialActionId(),
525                contentName,
526                title,
527                new String[] {wfDescription.getContentType()},
528                null,
529                context.getLang(),
530                null,
531                null,
532                inputs);
533        
534        return _resolver.resolveById((String) result.get("contentId"));
535    }
536    
537    /**
538     * Retrieves the inputs to give for content creation
539     * @param wfDescription The workflow description
540     * @param context The import context
541     * @return the inputs to give for content creation
542     */
543    protected Map<String, Object> _getInputsForContentCreation(ContentWorkflowDescription wfDescription, ImportCDMFrContext context)
544    {
545        Map<String, Object> inputs = new HashMap<>();
546        
547        ContentType contentType = _contentTypeEP.getExtension(wfDescription.getContentType());
548        if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG))
549        {
550            inputs.put(AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY, context.getCatalog());
551        }
552        
553        return inputs;
554    }
555    
556    /**
557     * Synchronize content 
558     * @param contentElement the DOM content element
559     * @param content The content to synchronize
560     * @param contentTypeId The content type ID
561     * @param syncCode The synchronization code
562     * @param context the import context
563     * @throws Exception if an error occurs while synchronizing the content values
564     */
565    protected void _synchronizeContent(Element contentElement, ModifiableContent content, String contentTypeId, String syncCode, ImportCDMFrContext context) throws Exception
566    {
567        Logger logger = context.getLogger();
568        
569        // Avoid a treatment twice or more
570        if (_synchronizedContents.add(content.getId()))
571        {
572            logger.info("Synchronization of the content '{}' with the content type '{}'", content.getTitle(), contentTypeId);
573            
574            if (content instanceof LockAwareAmetysObject && ((LockAwareAmetysObject) content).isLocked())
575            {
576                logger.warn("The content '{}' ({}) is currently locked by user {}: it cannot be synchronized", content.getTitle(), content.getId(), ((LockAwareAmetysObject) content).getLockOwner());
577            }
578            else if (content instanceof WorkflowAwareContent)
579            {
580                ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
581                ModelAwareValuesExtractor valuesExtractor = _valuesExtractorFactory.getValuesExtractor(contentElement, this, content, contentType, syncCode, context);
582                
583                // Extract the values
584                Map<String, Object> values = valuesExtractor.extractValues();
585                values.putAll(_getAdditionalValuesToSynchronize(content, syncCode, context));
586                
587                // Modify the content with the extracted values
588                boolean create = _importedContents.containsKey(content.getId());
589                Set<String> notSynchronizedContentIds = _getNotSynchronizedRelatedContentIds(content, syncCode, context);
590                _editContent((WorkflowAwareContent) content, Optional.empty(), values, create, notSynchronizedContentIds, context);
591                
592                if (content instanceof OrgUnit)
593                {
594                    _setOrgUnitParent((WorkflowAwareContent) content, context);
595                }
596                
597                // Create translation links
598                _linkTranslationsIfExist(content, contentTypeId, context);
599            }
600        }
601    }
602
603    /**
604     * Retrieves additional values to synchronize for the content
605     * @param content the content
606     * @param syncCode the content synchronization code
607     * @param context the import context
608     * @return the additional values
609     */
610    protected Map<String, Object> _getAdditionalValuesToSynchronize(ModifiableContent content, String syncCode, ImportCDMFrContext context)
611    {
612        Map<String, Object> additionalValues = new HashMap<>();
613        additionalValues.put(getIdField(), syncCode);
614        return additionalValues;
615    }
616    
617    /**
618     * Retrieves the ids of the contents related to the given content but that are not part of the synchronization 
619     * @param content the content
620     * @param syncCode the content synchronization code
621     * @param context the import context
622     * @return the not synchronized content ids
623     */
624    protected Set<String> _getNotSynchronizedRelatedContentIds(ModifiableContent content, String syncCode, ImportCDMFrContext context)
625    {
626        return new HashSet<>();
627    }
628    
629    /**
630     * Synchronize the content with given values.
631     * @param content The content to synchronize
632     * @param view the view containing the item to edit
633     * @param values the values
634     * @param create <code>true</code> if content is creating, false if it is updated
635     * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization
636     * @param context the import context 
637     * @throws WorkflowException if an error occurs
638     */
639    protected void _editContent(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, boolean create, Set<String> notSynchronizedContentIds, ImportCDMFrContext context) throws WorkflowException
640    {
641        Map<String, Object> inputs = new HashMap<>();
642        inputs.put(EditSynchronizedContentFunction.SCC_KEY, context.getSCC());
643        inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, context.getLogger());
644        inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds);
645        if (ignoreRights())
646        {
647            inputs.put(CheckRightsCondition.FORCE, true);
648        }
649
650        Map<String, Object> params = new HashMap<>();
651        // Remove catalog data, this value is forced at creation and should not be modified
652        values.remove(ProgramItem.CATALOG);
653        params.put(EditContentFunction.VALUES_KEY, values);
654        view.ifPresent(v -> params.put(EditContentFunction.VIEW, v));
655        params.put(EditContentFunction.QUIT, true);
656        params.put(EditSynchronizedContentFunction.IMPORT, create);
657        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, params);
658        
659        _contentWorkflowHelper.doAction(content, _SYNCHRONIZE_WORKFLOW_ACTION_ID, inputs);
660    }
661    
662    public ContentSynchronizationResult additionalOperations(ModifiableContent content, Map<String, Object> additionalParameters, Logger logger)
663    {
664        ContentSynchronizationResult result = new ContentSynchronizationResult();
665
666        if (content instanceof Program)
667        {
668            List<ModifiableContent> modifiedContents = _initializeShareableCoursesFields((Program) content);
669            
670            result.addModifiedContents(modifiedContents);
671            result.setHasChanged(!modifiedContents.isEmpty());
672        }
673        
674        return result;
675    }
676    
677    /**
678     * Initialize shareable fields for the courses under the given {@link ProgramItem}
679     * @param programItem the program item
680     * @return the list of contents that have been modified during the initialization
681     */
682    protected List<ModifiableContent> _initializeShareableCoursesFields(ProgramItem programItem)
683    {
684        List<ModifiableContent> modifiedContents = new ArrayList<>();
685
686        List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem);
687        for (ProgramItem child : children)
688        {
689            if (child instanceof Course && programItem instanceof CourseList)
690            {
691                if (_shareableCourseHelper.initializeShareableFields((Course) child, (CourseList) programItem,  UserPopulationDAO.SYSTEM_USER_IDENTITY, true))
692                {
693                    modifiedContents.add((Course) child);
694                }
695            }
696            
697            modifiedContents.addAll(_initializeShareableCoursesFields(child));
698        }
699
700        return modifiedContents;
701    }
702    
703    /**
704     * Search for translated contents
705     * @param importedContent The imported content
706     * @param contentType The content type
707     * @param context the import context
708     */
709    protected void _linkTranslationsIfExist(ModifiableContent importedContent, String contentType, ImportCDMFrContext context)
710    {
711        if (importedContent instanceof ProgramItem)
712        {
713            Expression expression = _getTranslationExpression(importedContent, contentType);
714            String xPathQuery = ContentQueryHelper.getContentXPathQuery(expression);
715    
716            AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery);
717            
718            Map<String, String> translations = new HashMap<>();
719            for (ModifiableContent content : contents)
720            {
721                translations.put(content.getLanguage(), content.getId());
722            }
723            
724            for (ModifiableContent content : contents)
725            {
726                TranslationHelper.setTranslations(content, translations);
727    
728                Map<String, Object> eventParams = new HashMap<>();
729                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
730                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
731                _observationManager.notify(new Event(OdfObservationConstants.ODF_CONTENT_TRANSLATED, _currentUserProvider.getUser(), eventParams));
732            }
733        }
734    }
735    
736    private Expression _getTranslationExpression(ModifiableContent content, String contentType)
737    {
738        List<Expression> expList = new ArrayList<>();
739        
740        if (StringUtils.isNotBlank(contentType))
741        {
742            expList.add(new ContentTypeExpression(Operator.EQ, contentType));
743        }
744        
745        String catalog = content.getValue(ProgramItem.CATALOG);
746        if (StringUtils.isNotBlank(catalog))
747        {
748            expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
749        }
750        
751        List<Expression> codeExpressionList = new ArrayList<>();
752        String syncValue = content.getValue(getIdField());
753        if (StringUtils.isNotBlank(syncValue))
754        {
755            codeExpressionList.add(new StringExpression(getIdField(), Operator.EQ, syncValue));
756        }
757
758        String code = content.getValue(ProgramItem.CODE);
759        if (StringUtils.isNotBlank(syncValue))
760        {
761            codeExpressionList.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code));
762        }
763        
764        if (!codeExpressionList.isEmpty())
765        {
766            expList.add(new OrExpression(codeExpressionList.toArray(Expression[]::new)));
767        }
768        
769        return new AndExpression(expList.toArray(Expression[]::new));
770    }
771
772    /**
773     * Set the orgUnit parent to rootOrgUnit.
774     * @param orgUnit The orgunit to link
775     * @param context the import context
776     * @throws Exception if an error occurs while synchronizing the content values
777     */
778    protected void _setOrgUnitParent(WorkflowAwareContent orgUnit, ImportCDMFrContext context) throws Exception
779    {
780        // Set the orgUnit parent (if no parent is set)
781        if (!orgUnit.hasValue(OrgUnit.PARENT_ORGUNIT))
782        {
783            OrgUnit rootOrgUnit = _rootOUProvider.getRoot();
784            Map<String, Object> values = new HashMap<>();
785            values.put(OrgUnit.PARENT_ORGUNIT, rootOrgUnit);
786            _editContent(orgUnit, Optional.empty(), values, false, Set.of(rootOrgUnit.getId()), context);
787        }
788    }
789    
790    /**
791     * Validates a content after import
792     * @param content The content to validate
793     * @param validationActionId Validation action ID to use for this content
794     * @param logger The logger
795     */
796    protected void validateContent(WorkflowAwareContent content, int validationActionId, Logger logger)
797    {
798        Map<String, Object> inputs = new HashMap<>();
799        if (ignoreRights())
800        {
801            inputs.put(CheckRightsCondition.FORCE, true);
802        }
803        
804        try
805        {
806            _contentWorkflowHelper.doAction(content, validationActionId, inputs);
807            logger.info("The content {} has been validated after import", content);
808        }
809        catch (WorkflowException | InvalidActionException e)
810        {
811            String failuresAsString = _getActionFailuresAsString(inputs);
812            logger.error("The content {} cannot be validated after import{}", content, failuresAsString, e);
813        }
814    }
815    
816    private String _getActionFailuresAsString(Map<String, Object> actionInputs)
817    {
818        String failuresAsString = "";
819        if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY))
820        {
821            @SuppressWarnings("unchecked")
822            List<String> failures = (List<String>) actionInputs.get(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY);
823            if (!failures.isEmpty())
824            {
825                failuresAsString = ", due to the following error(s):\n" + String.join("\n", failures);
826            }
827        }
828        
829        return failuresAsString;
830    }
831    
832    public String getIdFromCDMThenCode(String tableRefId, String cdmCode)
833    {
834        OdfReferenceTableEntry entry = _odfRefTableHelper.getItemFromCDM(tableRefId, cdmCode);
835        if (entry == null)
836        {
837            entry = _odfRefTableHelper.getItemFromCode(tableRefId, cdmCode);
838        }
839        return entry != null ? entry.getId() : null;
840    }
841    
842    private String _getXPathString(Node metadataNode, String xPath, String defaultValue)
843    {
844        String value = _xPathProcessor.evaluateAsString(metadataNode, xPath);
845        if (StringUtils.isEmpty(value))
846        {
847            value = defaultValue;
848        }
849        return value;
850    }
851
852    /**
853     * If true, bypass the rights check during the import process
854     * @return True if the rights check are bypassed during the import process
855     */
856    protected boolean ignoreRights()
857    {
858        return false;
859    }
860    
861    /**
862     * Construct the query to retrieve the content.
863     * @param contentTypeId The content type
864     * @param syncCode The synchronization code
865     * @param context the import context
866     * @return The {@link List} of {@link Expression}
867     */
868    protected List<Expression> _getExpressionsList(String contentTypeId, String syncCode, ImportCDMFrContext context)
869    {
870        List<Expression> expList = new ArrayList<>();
871        
872        if (StringUtils.isNotBlank(contentTypeId))
873        {
874            expList.add(new ContentTypeExpression(Operator.EQ, contentTypeId));
875            
876            if (StringUtils.isNotBlank(context.getCatalog()))
877            {
878                ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
879                if (contentType.hasModelItem(ProgramItem.CATALOG) || contentType.hasModelItem(CoursePart.CATALOG))
880                {
881                    expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, context.getCatalog()));
882                }
883            }
884        }
885        
886        if (StringUtils.isNotBlank(syncCode))
887        {
888            expList.add(new StringExpression(getIdField(), Operator.EQ, syncCode));
889        }
890        
891        if (StringUtils.isNotBlank(context.getLang()))
892        {
893            expList.add(new LanguageExpression(Operator.EQ, context.getLang()));
894        }
895        
896        return expList;
897    }
898
899    @Override
900    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
901    {
902        if (additionalParameters == null || !additionalParameters.containsKey("contentTypes"))
903        {
904            throw new IllegalArgumentException("Content types shouldn't be null.");
905        }
906
907        @SuppressWarnings("unchecked")
908        List<String> contentTypeIds = (List<String>) additionalParameters.get("contentTypes");
909        Set<String> allSyncFields = new HashSet<>();
910        
911        for (String contentTypeId : contentTypeIds)
912        {
913            Set<String> syncFields = _syncFieldsByContentType.computeIfAbsent(contentTypeId, k -> new HashSet<>());
914            allSyncFields.addAll(syncFields);
915        }
916        
917        return allSyncFields;
918    }
919}