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