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