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