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