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