001/*
002 *  Copyright 2017 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.apogee.scc;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.math.BigDecimal;
023import java.sql.Clob;
024import java.sql.SQLException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashMap;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.Objects;
036import java.util.Optional;
037import java.util.Set;
038import java.util.stream.Collectors;
039import java.util.stream.Stream;
040
041import org.apache.avalon.framework.configuration.Configuration;
042import org.apache.avalon.framework.configuration.ConfigurationException;
043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
044import org.apache.avalon.framework.context.Context;
045import org.apache.avalon.framework.context.ContextException;
046import org.apache.avalon.framework.context.Contextualizable;
047import org.apache.avalon.framework.service.ServiceException;
048import org.apache.avalon.framework.service.ServiceManager;
049import org.apache.cocoon.Constants;
050import org.apache.cocoon.components.ContextHelper;
051import org.apache.cocoon.environment.Request;
052import org.apache.commons.collections4.ListUtils;
053import org.apache.commons.io.IOUtils;
054import org.apache.commons.lang.StringUtils;
055import org.apache.commons.lang3.tuple.Pair;
056import org.slf4j.Logger;
057
058import org.ametys.cms.contenttype.ContentType;
059import org.ametys.cms.data.ContentValue;
060import org.ametys.cms.repository.Content;
061import org.ametys.cms.repository.ModifiableContent;
062import org.ametys.core.util.JSONUtils;
063import org.ametys.odf.ODFHelper;
064import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection;
065import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
066import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDAO;
067import org.ametys.plugins.odfsync.apogee.ApogeeDAO;
068import org.ametys.plugins.odfsync.apogee.scc.impl.OrgUnitSynchronizableContentsCollection;
069import org.ametys.runtime.config.Config;
070import org.ametys.runtime.i18n.I18nizableText;
071import org.ametys.runtime.model.ModelItem;
072import org.ametys.runtime.model.type.ModelItemTypeConstants;
073
074import com.google.common.base.CharMatcher;
075
076/**
077 * Abstract class for Apogee synchronization
078 */
079public abstract class AbstractApogeeSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable, ApogeeSynchronizableContentsCollection
080{
081    /** Name of parameter holding the data source id */
082    public static final String PARAM_DATASOURCE_ID = "datasourceId";
083    /** Name of parameter holding the administrative year */
084    public static final String PARAM_YEAR = "year";
085    /** Name of parameter holding the adding unexisting children parameter  */
086    public static final String PARAM_ADD_UNEXISTING_CHILDREN = "add-unexisting-children";
087    /** Name of parameter holding the field ID column */
088    protected static final String __PARAM_ID_COLUMN = "idColumn";
089    /** Name of parameter holding the fields mapping */
090    protected static final String __PARAM_MAPPING = "mapping";
091    /** Name of parameter into mapping holding the synchronized property */
092    protected static final String __PARAM_MAPPING_SYNCHRO = "synchro";
093    /** Name of parameter into mapping holding the path of metadata */
094    protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref";
095    /** Name of parameter into mapping holding the remote attribute */
096    protected static final String __PARAM_MAPPING_ATTRIBUTE = "attribute";
097    /** Name of parameter holding the criteria */
098    protected static final String __PARAM_CRITERIA = "criteria";
099    /** Name of parameter into criteria holding a criterion */
100    protected static final String __PARAM_CRITERIA_CRITERION = "criterion";
101    /** Name of parameter into criterion holding the id */
102    protected static final String __PARAM_CRITERIA_CRITERION_ID = "id";
103    /** Name of parameter into criterion holding the label */
104    protected static final String __PARAM_CRITERIA_CRITERION_LABEL = "label";
105    /** Name of parameter into criterion holding the type */
106    protected static final String __PARAM_CRITERIA_CRITERION_TYPE = "type";
107    /** Name of paramter holding columns */
108    protected static final String __PARAM_COLUMNS = "columns";
109    /** Name of paramter into columns holding column */
110    protected static final String __PARAM_COLUMNS_COLUMN = "column";
111    
112    /** Default language configured for ODF */
113    protected String _odfLang;
114
115    /** Name of the Apogée column which contains the ID */
116    protected String _idColumn;
117    
118    /** Mapping between metadata and columns */
119    protected Map<String, List<String>> _mapping;
120    
121    /** Synchronized fields */
122    protected Set<String> _syncFields;
123    
124    /** Synchronized fields */
125    protected Set<String> _columns;
126    
127    /** Synchronized fields */
128    protected Set<ApogeeCriterion> _criteria;
129    
130    /** Context */
131    protected Context _context;
132    
133    /** The DAO for remote DB Apogee */
134    protected ApogeeDAO _apogeeDAO;
135    
136    /** The JSON utils */
137    protected JSONUtils _jsonUtils;
138    
139    /** SCC DAO */
140    protected SynchronizableContentsCollectionDAO _sccDAO;
141    
142    /** The ODF helper */
143    protected ODFHelper _odfHelper;
144    
145    /** The Apogee SCC helper */
146    protected ApogeeSynchronizableContentsCollectionHelper _apogeeSCCHelper;
147    
148    @Override
149    public void service(ServiceManager manager) throws ServiceException
150    {
151        super.service(manager);
152        _apogeeDAO = (ApogeeDAO) manager.lookup(ApogeeDAO.ROLE);
153        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
154        _sccDAO = (SynchronizableContentsCollectionDAO) manager.lookup(SynchronizableContentsCollectionDAO.ROLE);
155        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
156        _apogeeSCCHelper = (ApogeeSynchronizableContentsCollectionHelper) manager.lookup(ApogeeSynchronizableContentsCollectionHelper.ROLE);
157    }
158
159    @Override
160    public void contextualize(Context context) throws ContextException
161    {
162        _context = context;
163    }
164    
165    @Override
166    protected void configureDataSource(Configuration configuration) throws ConfigurationException
167    {
168        _odfLang = Config.getInstance().getValue("odf.programs.lang");
169        try
170        {
171            org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
172            File apogeeMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/apogee-mapping.xml"));
173            
174            try (InputStream is = apogeeMapping.isFile()
175                    ? new FileInputStream(apogeeMapping)
176                    : getClass().getResourceAsStream("/org/ametys/plugins/odfsync/apogee/apogee-mapping.xml"))
177            {
178                Configuration cfg = new DefaultConfigurationBuilder().build(is);
179                Configuration child = cfg.getChild(getMappingName());
180                if (child != null)
181                {
182                    _criteria = new LinkedHashSet<>();
183                    _columns = new LinkedHashSet<>();
184                    _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue();
185                    _mapping = new HashMap<>();
186                    _syncFields = new HashSet<>();
187                    String mappingAsString = child.getChild(__PARAM_MAPPING).getValue();
188                    _mapping.put(getIdField(), List.of(getIdColumn()));
189                    if (StringUtils.isNotEmpty(mappingAsString))
190                    {
191                        List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString);
192                        for (Object object : mappingAsList)
193                        {
194                            @SuppressWarnings("unchecked")
195                            Map<String, Object> field = (Map<String, Object>) object;
196                            
197                            String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF);
198                            
199                            String[] attributes = ((String) field.get(__PARAM_MAPPING_ATTRIBUTE)).split(",");
200                            _mapping.put(metadataRef, Arrays.asList(attributes));
201        
202                            boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false;
203                            if (isSynchronized)
204                            {
205                                _syncFields.add(metadataRef);
206                            }
207                        }
208                    }
209    
210                    Configuration[] criteria = child.getChild(__PARAM_CRITERIA).getChildren(__PARAM_CRITERIA_CRITERION);
211                    for (Configuration criterion : criteria)
212                    {
213                        String id = criterion.getChild(__PARAM_CRITERIA_CRITERION_ID).getValue();
214                        I18nizableText label = _getCriterionLabel(criterion.getChild(__PARAM_CRITERIA_CRITERION_LABEL), id);
215                        String type = criterion.getChild(__PARAM_CRITERIA_CRITERION_TYPE).getValue("STRING");
216                        
217                        _criteria.add(new ApogeeCriterion(id, label, type));
218                    }
219    
220                    Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN);
221                    for (Configuration column : columns)
222                    {
223                        _columns.add(column.getValue());
224                    }
225                }
226            }
227        }
228        catch (Exception e)
229        {
230            throw new ConfigurationException("Error while parsing apogee-mapping.xml", e);
231        }
232    }
233    
234    private I18nizableText _getCriterionLabel(Configuration configuration, String defaultValue)
235    {
236        if (configuration.getAttributeAsBoolean("i18n", false))
237        {
238            return new I18nizableText("plugin.odf-sync", configuration.getValue(defaultValue));
239        }
240        else
241        {
242            return new I18nizableText(configuration.getValue(defaultValue));
243        }
244    }
245    
246    @Override
247    public List<ModifiableContent> populate(Logger logger)
248    {
249        boolean isRequestAttributeOwner = false;
250        
251        Request request = ContextHelper.getRequest(_context);
252        if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null)
253        {
254            request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>());
255            isRequestAttributeOwner = true;
256        }
257        
258        List<ModifiableContent> populatedContents = super.populate(logger);
259        
260        if (isRequestAttributeOwner)
261        {
262            request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS);
263        }
264
265        return populatedContents;
266    }
267    
268    @Override
269    protected List<ModifiableContent> _internalPopulate(Logger logger)
270    {
271        return _importOrSynchronizeContents(Map.of("isGlobalSync", true), false, logger);
272    }
273    
274    @Override
275    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
276    {
277        Map<String, Object> searchParams = new HashMap<>(searchParameters);
278        if (offset > 0)
279        {
280            searchParams.put("__offset", offset);
281        }
282        if (limit < Integer.MAX_VALUE)
283        {
284            searchParams.put("__limit", offset + limit);
285        }
286        searchParams.put("__order", _getSort(sort));
287
288        // We don't use session.selectMap which reorder data
289        List<Map<String, Object>> requestValues = _search(searchParams, logger);
290
291        // Transform CLOBs to String
292        Set<String> clobColumns = getClobColumns();
293        if (!clobColumns.isEmpty())
294        {
295            for (Map<String, Object> contentValues : requestValues)
296            {
297                String idValue = contentValues.get(getIdColumn()).toString();
298                for (String clobKey : getClobColumns())
299                {
300                    // Get the old values for the CLOB
301                    @SuppressWarnings("unchecked")
302                    Optional<List<Object>> oldValues = Optional.of(clobKey)
303                        .map(contentValues::get)
304                        .map(obj -> (List<Object>) obj);
305                    
306                    if (oldValues.isPresent())
307                    {
308                        // Get the new values for the CLOB
309                        List<Object> newValues = oldValues.get()
310                            .stream()
311                            .map(value -> _transformClobToString(value, idValue, logger))
312                            .filter(Objects::nonNull)
313                            .collect(Collectors.toList());
314                        
315                        // Set the transformed CLOB values
316                        contentValues.put(clobKey, newValues);
317                    }
318                }
319            }
320        }
321        
322        // Reorganize results
323        String idColumn = getIdColumn();
324        Map<String, Map<String, Object>> results = new LinkedHashMap<>();
325        for (Map<String, Object> contentValues : requestValues)
326        {
327            results.put(contentValues.get(idColumn).toString(), contentValues);
328        }
329        
330        for (Map<String, Object> result : results.values())
331        {
332            result.put(SCC_UNIQUE_ID, result.get(getIdColumn()));
333        }
334        
335        return results;
336    }
337    
338    @Override
339    protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger)
340    {
341        Map<String, Map<String, List<Object>>> remoteValues = new HashMap<>();
342        
343        Map<String, Map<String, Object>> results = internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger);
344        
345        if (results != null)
346        {
347            remoteValues = _sccHelper.organizeRemoteValuesByAttribute(results, _mapping);
348        }
349        
350        return remoteValues;
351    }
352    
353    @Override
354    public List<String> getLanguages()
355    {
356        return List.of(_odfLang);
357    }
358    
359    @Override
360    public List<ModifiableContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception
361    {
362        boolean isRequestAttributeOwner = false;
363        
364        Request request = ContextHelper.getRequest(_context);
365        if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null)
366        {
367            request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>());
368            isRequestAttributeOwner = true;
369        }
370
371        List<ModifiableContent> createdContents = super.importContent(idValue, additionalParameters, logger);
372        
373        if (isRequestAttributeOwner)
374        {
375            request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS);
376        }
377
378        return createdContents;
379    }
380    
381    @Override
382    public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception
383    {
384        boolean isRequestAttributeOwner = false;
385        
386        Request request = ContextHelper.getRequest(_context);
387        if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null)
388        {
389            request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>());
390            isRequestAttributeOwner = true;
391        }
392        
393        super.synchronizeContent(content, logger);
394        
395        if (isRequestAttributeOwner)
396        {
397            request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS);
398        }
399    }
400    
401    @Override
402    protected Map<String, Object> putIdParameter(String idValue)
403    {
404        Map<String, Object> parameters = new HashMap<>();
405        parameters.put(getIdField(), idValue);
406        return parameters;
407    }
408    
409    /**
410     * Search the contents with the search parameters. Use id parameter to search an unique content.
411     * @param searchParameters Search parameters
412     * @param logger The logger
413     * @return A Map of mapped metadatas extract from Apogée database ordered by content unique Apogée ID
414     */
415    protected abstract List<Map<String, Object>> _search(Map<String, Object> searchParameters, Logger logger);
416    
417    /**
418     * Convert the {@link BigDecimal} values retrieved from database into long values
419     * @param searchResults The initial search results from database
420     * @return The converted search results
421     */
422    protected List<Map<String, Object>> _convertBigDecimal(List<Map<String, Object>> searchResults)
423    {
424        List<Map<String, Object>> convertedSearchResults = new ArrayList<>();
425        
426        for (Map<String, Object> searchResult : searchResults)
427        {
428            for (String key : searchResult.keySet())
429            {
430                searchResult.put(key, _convertBigDecimal(getContentType(), key, searchResult.get(key))); 
431            }
432            
433            convertedSearchResults.add(searchResult);
434        }
435        
436        return convertedSearchResults;
437    }
438    
439    /**
440     * Convert the object in parameter to a long if it's a {@link BigDecimal}, otherwise return the object itself.
441     * @param contentTypeId The content type of the parent content
442     * @param attributeName The metadata name
443     * @param objectToConvert The object to convert if necessary
444     * @return The converted object
445     */
446    protected Object _convertBigDecimal(String contentTypeId, String attributeName, Object objectToConvert)
447    {
448        if (objectToConvert instanceof BigDecimal)
449        {
450            ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
451            if (contentType.hasModelItem(attributeName))
452            {
453                ModelItem definition = contentType.getModelItem(attributeName);    
454                String typeId = definition.getType().getId();
455                switch (typeId)
456                {
457                    case ModelItemTypeConstants.DOUBLE_TYPE_ID:
458                        return ((BigDecimal) objectToConvert).doubleValue();
459                    case ModelItemTypeConstants.LONG_TYPE_ID:
460                        return ((BigDecimal) objectToConvert).longValue();
461                    default:
462                        // Do nothing
463                        break;
464                }
465            }
466            return ((BigDecimal) objectToConvert).toString();
467        }
468        return objectToConvert;
469    }
470    
471    /**
472     * Transform CLOB value to String value.
473     * @param value The input value
474     * @param idValue The identifier of the program
475     * @param logger The logger
476     * @return the same value, with CLOB transformed to String.
477     */
478    protected Object _transformClobToString(Object value, String idValue, Logger logger)
479    {
480        if (value instanceof Clob)
481        {
482            Clob clob = (Clob) value;
483            try
484            {
485                String strValue = IOUtils.toString(clob.getCharacterStream());
486                return CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue);
487            }
488            catch (SQLException | IOException e)
489            {
490                logger.error("Unable to get education add elements from the program '{}'.", idValue, e);
491                return null;
492            }
493            finally
494            {
495                try
496                {
497                    clob.free();
498                }
499                catch (SQLException e)
500                {
501                    // Ignore the exception.
502                }
503            }
504        }
505        
506        return value;
507    }
508
509    /**
510     * Get the list of CLOB column's names.
511     * @return The list of the CLOB column's names to transform to {@link String}
512     */
513    protected Set<String> getClobColumns()
514    {
515        return Set.of();
516    }
517    
518    /**
519     * Transform a {@link List} of {@link Map} to a {@link Map} of {@link List} computed by keys (lines to columns).
520     * @param lines {@link List} to reorganize
521     * @return {@link Map} of {@link List}
522     */
523    protected Map<String, List<Object>> _transformListOfMap2MapOfList(List<Map<String, Object>> lines)
524    {
525        return lines.stream()
526            .filter(Objects::nonNull)
527            .map(Map::entrySet)
528            .flatMap(Collection::stream)
529            .filter(e -> Objects.nonNull(e.getValue()))
530            .collect(
531                Collectors.toMap(
532                    Entry::getKey,
533                    e -> List.of(e.getValue()),
534                    (l1, l2) -> ListUtils.union(l1, l2)
535                )
536            );
537    }
538    
539    /**
540     * Get the name of the mapping.
541     * @return the mapping name
542     */
543    protected abstract String getMappingName();
544    
545    /**
546     * Get the id of data source
547     * @return The id of data source
548     */
549    protected String getDataSourceId()
550    {
551        return (String) getParameterValues().get(PARAM_DATASOURCE_ID);
552    }
553    
554    /**
555     * Get the administrative year
556     * @return The administrative year
557     */
558    protected String getYear()
559    {
560        return (String) getParameterValues().get(PARAM_YEAR);
561    }
562    
563    /**
564     * Check if unexisting contents in Ametys should be added from data source
565     * @return <code>true</code> if unexisting contents in Ametys should be added from data source, default value is <code>false</code>
566     */
567    protected boolean addUnexistingChildren()
568    {
569        return Boolean.valueOf((String) getParameterValues().getOrDefault(PARAM_ADD_UNEXISTING_CHILDREN, Boolean.TRUE.toString()));
570    }
571    
572    /**
573     * Get the identifier column (can be a concatened column).
574     * @return the column id
575     */
576    protected String getIdColumn()
577    {
578        return _idColumn;
579    }
580    
581    @Override
582    public String getIdField()
583    {
584        return "apogeeSyncCode";
585    }
586
587    @Override
588    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
589    {
590        return _syncFields;
591    }
592    
593    /**
594     * Get the list of rich text fields of the imported content.
595     * @return The list of the rich text fields metadata name
596     */
597    protected Set<String> getRichTextFields()
598    {
599        return new HashSet<>();
600    }
601    
602    @Override
603    protected void configureSearchModel()
604    {
605        for (ApogeeCriterion criterion : _criteria)
606        {
607            _searchModelConfiguration.addCriterion(criterion.getId(), criterion.getLabel(), criterion.getType());
608        }
609        for (String columnName : _columns)
610        {
611            _searchModelConfiguration.addColumn(columnName);
612        }
613    }
614    
615    @Override
616    protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger)
617    {
618        Map<String, Object> additionalValues = super.getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger);
619        
620        // Handle parents
621        getParentFromAdditionalParameters(additionalParameters)
622            .map(this::getParentAttribute)
623            .ifPresent(attribute -> additionalValues.put(attribute.getKey(), attribute.getValue()));
624        
625        // Handle children
626        List<ModifiableContent> children = handleChildren(idValue, content, create, logger);
627        additionalValues.put(getChildrenAttributeName(), children.toArray(new ModifiableContent[children.size()]));
628        
629        return additionalValues;
630    }
631    
632    /**
633     * Retrieves the attribute to synchronize for the given parent (as a {@link Pair} of name and value)
634     * @param parent the parent content
635     * @return the parent attribute
636     */
637    protected Pair<String, Object> getParentAttribute(ModifiableContent parent)
638    {
639        return null;
640    }
641    
642    /**
643     * Import and synchronize children of the given content
644     * In the usual case, if we are in import mode (create to true) or if we trust the source (removalSync to true) we just synchronize structure but we don't synchronize child contents
645     * @param idValue The current content synchronization code
646     * @param content The current content
647     * @param create <code>true</code> if the content has been newly created
648     * @param logger The logger
649     * @return The handled children
650     */
651    protected List<ModifiableContent> handleChildren(String idValue, Content content, boolean create, Logger logger)
652    {
653        return importOrSynchronizeChildren(idValue, content, getChildrenSCCModelId(), getChildrenAttributeName(), create, logger);
654    }
655    
656    @Override
657    protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger)
658    {
659        Set<String> contentIds = super.getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, lang, logger);
660        
661        getParentIdFromAdditionalParameters(additionalParameters)
662            .ifPresent(contentIds::add);
663        
664        return contentIds;
665    }
666    
667    /**
668     * Retrieves the parent id, extracted from additional parameters
669     * @param additionalParameters the additional parameters
670     * @return the parent id
671     */
672    protected Optional<ModifiableContent> getParentFromAdditionalParameters(Map<String, Object> additionalParameters)
673    {
674        return getParentIdFromAdditionalParameters(additionalParameters)
675                .map(_resolver::resolveById);
676    }
677
678    /**
679     * Retrieves the parent id, extracted from additional parameters
680     * @param additionalParameters the additional parameters
681     * @return the parent id
682     */
683    protected Optional<String> getParentIdFromAdditionalParameters(Map<String, Object> additionalParameters)
684    {
685        return Optional.ofNullable(additionalParameters)
686                .filter(params -> params.containsKey("parentId"))
687                .map(params -> params.get("parentId"))
688                .filter(String.class::isInstance)
689                .map(String.class::cast);
690    }
691    
692    /**
693     * Get the children SCC model id. Can be null if no implementation is defined.
694     * @return the children SCC model id
695     */
696    protected String getChildrenSCCModelId()
697    {
698        // Default implementation
699        return null;
700    }
701    
702    /**
703     * Get the attribute name to get children
704     * @return the attribute name to get children
705     */
706    protected abstract String getChildrenAttributeName();
707
708    /**
709     * Transform the given {@link List} of {@link Object} to a {@link String} representing the ordered fields for SQL.
710     * @param sortList The sort list object to transform to the list of ordered fields compatible with SQL.
711     * @return A string representing the list of ordered fields
712     */
713    @SuppressWarnings("unchecked")
714    protected String _getSort(List<Object> sortList)
715    {
716        if (sortList != null)
717        {
718            StringBuilder sort = new StringBuilder();
719            
720            for (Object sortValueObj : sortList)
721            {
722                Map<String, Object> sortValue = (Map<String, Object>) sortValueObj;
723                
724                sort.append(sortValue.get("property"));
725                if (sortValue.containsKey("direction"))
726                {
727                    sort.append(" ");
728                    sort.append(sortValue.get("direction"));
729                    sort.append(",");
730                }
731                else
732                {
733                    sort.append(" ASC,");
734                }
735            }
736            
737            sort.deleteCharAt(sort.length() - 1);
738            
739            return sort.toString();
740        }
741        
742        return null;
743    }
744    
745    @Override
746    public int getTotalCount(Map<String, Object> searchParameters, Logger logger)
747    {
748        // Remove empty parameters
749        Map<String, Object> searchParams = new HashMap<>();
750        for (String parameterName : searchParameters.keySet())
751        {
752            Object parameterValue = searchParameters.get(parameterName);
753            if (parameterValue != null && !parameterValue.toString().isEmpty())
754            {
755                searchParams.put(parameterName, parameterValue);
756            }
757        }
758        
759        searchParams.put("__count", true);
760        
761        List<Map<String, Object>> results = _search(searchParams, logger);
762        if (results != null && !results.isEmpty())
763        {
764            return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue();
765        }
766        
767        return 0;
768    }
769    
770    @Override
771    protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
772    {
773        ModifiableContent content = super._importContent(idValue, additionalParameters, lang, _transformOrgUnitAttribute(remoteValues, logger), logger);
774        if (content != null)
775        {
776            _apogeeSCCHelper.addToHandleContents(content.getId());
777        }
778        return content;
779    }
780    
781    @Override
782    protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
783    {
784        if (!_apogeeSCCHelper.addToHandleContents(content.getId()))
785        {
786            return content;
787        }
788        return super._synchronizeContent(content, _transformOrgUnitAttribute(remoteValues, logger), logger);
789    }
790    
791    /**
792     * Import and synchronize children of the given content
793     * In the usual case, if we are in import mode (create to true) or if we trust the source (removalSync to true) we just synchronize structure but we don't synchronize child contents
794     * @param idValue The parent content synchronization code
795     * @param content The parent content
796     * @param sccModelId SCC model ID
797     * @param attributeName The name of the attribute containing children
798     * @param create <code>true</code> if the content has been newly created
799     * @param logger The logger
800     * @return The imported or synchronized children
801     */
802    protected List<ModifiableContent> importOrSynchronizeChildren(String idValue, Content content, String sccModelId, String attributeName, boolean create, Logger logger)
803    {
804        // Get the SCC for children
805        SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId);
806        
807        return create
808                // Import mode
809                ? _importChildren(idValue, scc, logger)
810                // Synchronization mode
811                : _synchronizeChildren(content, scc, attributeName, logger);
812    }
813    
814    /**
815     * Import children
816     * @param idValue The parent content synchronization code
817     * @param scc The SCC
818     * @param logger The logger
819     * @return The imported children
820     */
821    protected List<ModifiableContent> _importChildren(String idValue, SynchronizableContentsCollection scc, Logger logger)
822    {
823        // If SCC exists, search for children
824        if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection)
825        {
826            // Synchronize or import children content
827            return ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(Map.of("parentCode", idValue), logger);
828        }
829        
830        return List.of();
831    }
832
833    /**
834     * Synchronize children
835     * @param content Parent content
836     * @param scc The SCC
837     * @param attributeName The name of the attribute containing children
838     * @param logger The logger
839     * @return <code>true</code> if there are changes
840     */
841    protected List<ModifiableContent> _synchronizeChildren(Content content, SynchronizableContentsCollection scc, String attributeName, Logger logger)
842    {
843        // Get the children remote sync codes if needed
844        // These remote sync codes are needed if we want to add unexisting contents in Ametys from Apogee or delete obsolete contents 
845        Set<String> childrenRemoteSyncCode = (removalSync() || addUnexistingChildren())
846            ? _getChildrenRemoteSyncCode(content, scc, logger)
847            : null;
848        
849        // First we get the existing children in Ametys
850        List<ModifiableContent> ametysChildren = Stream.of(content.getValue(attributeName, false, new ContentValue[0]))
851            .map(ContentValue::getContentIfExists)
852            .filter(Optional::isPresent)
853            .map(Optional::get)
854            .collect(Collectors.toList());
855        
856        // Remove obsolete children if removeSync is active
857        if (childrenRemoteSyncCode != null && removalSync())
858        {
859            ametysChildren = ametysChildren.stream()
860                .filter(c -> !_isChildWillBeRemoved(c, scc, childrenRemoteSyncCode, logger))
861                .collect(Collectors.toList());
862        }
863        
864        // Then we synchronize the existing children in Ametys
865        for (ModifiableContent childContent : ametysChildren)
866        {
867            _apogeeSCCHelper.synchronizeContent(childContent, logger);
868        }
869        
870        // Then we add unexisting children if needed
871        if (childrenRemoteSyncCode != null && addUnexistingChildren())
872        {
873            ametysChildren.addAll(_addUnexistingChildren(scc, childrenRemoteSyncCode, logger));
874        }
875        
876        return ametysChildren;
877    }
878
879    /**
880     * Get the remote sync codes
881     * @param content Parent content
882     * @param scc the scc
883     * @param logger The logger
884     * @return the remote sync codes or null if the scc is not from Apogee
885     */
886    protected Set<String> _getChildrenRemoteSyncCode(Content content, SynchronizableContentsCollection scc, Logger logger)
887    {
888        if (scc != null && scc instanceof AbstractApogeeSynchronizableContentsCollection)
889        {
890            String syncCode = content.getValue(getIdField());
891            return ((AbstractApogeeSynchronizableContentsCollection) scc)
892                        .search(Map.of("parentCode", syncCode), 0, Integer.MAX_VALUE, null, logger)
893                        .keySet();
894        }
895        
896        return null;
897    }
898
899    /**
900     * Add unexisting children in Ametys from Apogee
901     * @param scc the scc to import contents
902     * @param childrenRemoteSyncCode the remote sync codes
903     * @param logger the logger
904     * @return the list of unexisting contents
905     */
906    protected List<ModifiableContent> _addUnexistingChildren(SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger)
907    {
908        return childrenRemoteSyncCode
909            .stream()
910            .filter(code -> scc.getContent(_odfLang, code) == null) // get only remote content values which doesn't exist in Ametys
911            .flatMap(code -> _importUnexistingChildren(scc, code, null, logger))
912            .collect(Collectors.toList());
913    }
914
915    /**
916     * Import an unexisting child in Ametys
917     * @param scc the scc to import the content
918     * @param syncCode the sync code of the content
919     * @param additionalParameters the additional params
920     * @param logger the logger
921     * @return the list of created children
922     */
923    @SuppressWarnings("unchecked")
924    protected Stream<ModifiableContent> _importUnexistingChildren(SynchronizableContentsCollection scc, String syncCode, Map<String, List<Object>> additionalParameters, Logger logger)
925    {
926        try
927        {
928            return scc.importContent(syncCode, (Map<String, Object>) (Object) additionalParameters, logger).stream();
929        }
930        catch (Exception e)
931        {
932            logger.error("An error occured while importing a new children content with syncCode '{}' from SCC '{}'", syncCode, scc.getId(), e);
933        }
934        
935        return Stream.empty();
936    }
937    
938    /**
939     * True if the content will be removed from the structure
940     * @param content the content
941     * @param scc the scc
942     * @param childrenRemoteSyncCode the remote sync codes
943     * @param logger the logger
944     * @return <code>true</code> if the content will be removed from the structure
945     */
946    protected boolean _isChildWillBeRemoved(ModifiableContent content, SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger)
947    {
948        String syncCode = content.getValue(scc.getIdField());
949        return !childrenRemoteSyncCode.contains(syncCode);
950    }
951    
952    @Override
953    public List<ModifiableContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger)
954    {
955        return _importOrSynchronizeContents(searchParams, true, logger);
956    }
957    
958    @SuppressWarnings("unchecked")
959    private Map<String, List<Object>> _transformOrgUnitAttribute(Map<String, List<Object>> remoteValues, Logger logger)
960    {
961        // Transform orgUnit values and import content if necessary (useful for Course and SubProgram)
962        SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID);
963
964        List<Object> orgUnitCodes = remoteValues.get("orgUnit");
965        if (orgUnitCodes != null && !orgUnitCodes.isEmpty())
966        {
967            List<?> orgUnitContents = null;
968            String orgUnitCode = orgUnitCodes.get(0).toString();
969            
970            if (scc != null)
971            {
972                try
973                {
974                    ModifiableContent orgUnitContent = scc.getContent(_odfLang, orgUnitCode);
975                    if (orgUnitContent == null)
976                    {
977                        orgUnitContents = scc.importContent(orgUnitCode, null, logger);
978                    }
979                    else
980                    {
981                        orgUnitContents = List.of(orgUnitContent);
982                    }
983                }
984                catch (Exception e)
985                {
986                    logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e);
987                }
988            }
989            
990            if (orgUnitContents == null)
991            {
992                // Impossible link to orgUnit
993                remoteValues.remove("orgUnit");
994                logger.warn("Impossible to import the OrgUnit with the synchronization code '{}', check if you set the OrgUnit SCC with the following model ID: '{}'", orgUnitCode, OrgUnitSynchronizableContentsCollection.MODEL_ID);
995            }
996            else
997            {
998                remoteValues.put("orgUnit", (List<Object>) orgUnitContents);
999            }
1000        }
1001        
1002        return remoteValues;
1003    }
1004    
1005    @Override
1006    public boolean handleRightAssignmentContext()
1007    {
1008        // Rights on ODF contents are handled by ODFRightAssignmentContext
1009        return false;
1010    }
1011}