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.contentio.synchronize;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.commons.lang3.StringUtils;
036import org.slf4j.Logger;
037
038import org.ametys.cms.content.ContentHelper;
039import org.ametys.cms.contenttype.ContentType;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.repository.ModifiableContent;
042import org.ametys.cms.repository.WorkflowAwareContent;
043import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
044import org.ametys.cms.workflow.EditContentFunction;
045import org.ametys.core.util.MapUtils;
046import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
049import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
050import org.ametys.plugins.repository.data.holder.values.ValueContext;
051import org.ametys.plugins.repository.version.VersionableAmetysObject;
052import org.ametys.plugins.workflow.AbstractWorkflowComponent;
053import org.ametys.runtime.model.View;
054
055import com.opensymphony.workflow.WorkflowException;
056
057/**
058 * Abstract implementation of {@link SynchronizableContentsCollection}.
059 */
060public abstract class AbstractSimpleSynchronizableContentsCollection extends AbstractSynchronizableContentsCollection
061{
062    /** The extension point for Synchronizing Content Operators */
063    protected SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP;
064    
065    /** The content helper */
066    protected ContentHelper _contentHelper;
067    
068    private List<String> _handledContents;
069    
070    @Override
071    public void service(ServiceManager manager) throws ServiceException
072    {
073        super.service(manager);
074        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
075        _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) manager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE);
076    }
077    
078    @Override
079    public void configure(Configuration configuration) throws ConfigurationException
080    {
081        super.configure(configuration);
082        _handledContents = new ArrayList<>();
083    }
084
085    @Override
086    public List<ModifiableContent> populate(Logger logger)
087    {
088        _handledContents.clear();
089        List<ModifiableContent> populatedContents = super.populate(logger);
090        _handledContents.clear();
091        return populatedContents;
092    }
093    
094    @Override
095    protected List<ModifiableContent> _internalPopulate(Logger logger)
096    {
097        return _importOrSynchronizeContents(new HashMap<>(), false, logger);
098    }
099    
100    /**
101     * Adds the given content as handled (i.e. will not be removed if _removalSync is true) 
102     * @param id The id of the content
103     */
104    protected void _handleContent(String id)
105    {
106        _handledContents.add(id);
107    }
108    
109    /**
110     * Returns true if the given content is handled
111     * @param id The content to test
112     * @return true if the given content is handled
113     */
114    protected boolean _isHandled(String id)
115    {
116        return _handledContents.contains(id);
117    }
118    
119    /**
120     * Imports or synchronizes a content for each available language
121     * @param idValue The unique identifier of the content
122     * @param remoteValues The remote values
123     * @param forceImport To force import and ignoring the synchronize existing contents only option
124     * @param logger The logger
125     * @return The list of synchronized or imported contents
126     */
127    protected List<ModifiableContent> _importOrSynchronizeContent(String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
128    {
129        List<ModifiableContent> contents = new ArrayList<>();
130        
131        for (String lang : getLanguages())
132        {
133            _importOrSynchronizeContent(idValue, lang, remoteValues, forceImport, logger)
134                .ifPresent(contents::add);
135        }
136        
137        return contents;
138    }
139    
140    /**
141     * Imports or synchronizes a content for a given language
142     * @param idValue The unique identifier of the content
143     * @param lang The language of content to import or synchronize
144     * @param remoteValues The remote values
145     * @param forceImport To force import and ignoring the synchronize existing contents only option
146     * @param logger The logger
147     * @return The imported or synchronized content
148     */
149    protected Optional<ModifiableContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
150    {
151        try
152        {
153            ModifiableContent content = getContent(lang, idValue);
154            if (content != null)
155            {
156                return Optional.of(_synchronizeContent(content, remoteValues, logger));
157            }
158            else if (forceImport || !synchronizeExistingContentsOnly())
159            {
160                return Optional.ofNullable(_importContent(idValue, null, lang, remoteValues, logger));
161            }
162        }
163        catch (Exception e)
164        {
165            _nbError++;
166            logger.error("An error occurred while importing or synchronizing content", e);
167        }
168        
169        return Optional.empty();
170    }
171
172    @Override
173    public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception
174    {
175        String idValue = content.getValue(getIdField());
176        
177        Map<String, Object> searchParameters = putIdParameter(idValue);
178        Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(searchParameters, logger);
179        if (!results.isEmpty())
180        {
181            try
182            {
183                _synchronizeContent(content, results.get(idValue), logger);
184            }
185            catch (Exception e)
186            {
187                _nbError++;
188                logger.error("An error occurred while importing or synchronizing content", e);
189                throw e;
190            }
191        }
192        else
193        {
194            logger.warn("The content {} ({}) with synchronization code '{}' doesn't exist anymore in the datasource from SCC '{}'", content.getTitle(), content.getId(), idValue, getId());
195        }
196    }
197
198    /**
199     * Synchronize a content with remove values.
200     * @param content The content to synchronize
201     * @param remoteValues Values to synchronize
202     * @param logger The logger
203     * @return The synchronized content
204     * @throws Exception if an error occurs
205     */
206    protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
207    {
208        long startTime = System.currentTimeMillis();
209        
210        String contentTitle = content.getTitle();
211        String lang = content.getLanguage();
212        
213        // Update content
214        logger.info("Start synchronizing content '{}' for language {}", contentTitle, lang);
215
216        _ensureTitleIsPresent(content, remoteValues, logger);
217        
218        boolean hasChanged = _fillContent(remoteValues, content, Map.of(), false, logger);
219        if (hasChanged)
220        {
221            _nbSynchronizedContents++;
222            logger.info("Some changes were detected for content '{}' and language {}", contentTitle, lang);
223        }
224        else
225        {
226            _nbNotChangedContents++;
227            logger.info("No changes detected for content '{}' and language {}", contentTitle, lang);
228        }
229        
230        // Do additional operation on the content
231        SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator());
232        if (synchronizingContentOperator != null)
233        {
234            synchronizingContentOperator.additionalOperation(content, remoteValues, logger);
235        }
236        else
237        {
238            logger.warn("Cannot find synchronizing content operator with id '{}'. No additional operation has been done.", getSynchronizingContentOperator());
239        }
240        
241        long endTime = System.currentTimeMillis();
242        logger.info("End synchronization of content '{}' for language {} in {} ms", contentTitle, lang, endTime - startTime);
243        
244        return content;
245    }
246    
247    @Override
248    public List<ModifiableContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception
249    {
250        List<ModifiableContent> createdContents = new ArrayList<>();
251        
252        Map<String, Object> searchParameters = putIdParameter(idValue);
253        Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(searchParameters, logger);
254        if (!results.isEmpty())
255        {
256            for (String lang : getLanguages())
257            {
258                try
259                {
260                    createdContents.add(_importContent(idValue, additionalParameters, lang, results.get(idValue), logger));
261                }
262                catch (Exception e)
263                {
264                    _nbError++;
265                    logger.error("An error occurred while importing or synchronizing content", e);
266                }
267            }
268        }
269        
270        return createdContents;
271    }
272    
273    /**
274     * Set search parameters for the ID value.
275     * @param idValue Value to search
276     * @return Map with the search parameters
277     */
278    protected abstract Map<String, Object> putIdParameter(String idValue);
279
280    /**
281     * Import a content from remote values.
282     * @param idValue Id (for import/synchronization) of the content to import
283     * @param additionalParameters Specific parameters for import
284     * @param lang Lang of the content
285     * @param remoteValues Values of the content
286     * @param logger The logger
287     * @return The content created by import, or null
288     * @throws Exception if an error occurs.
289     */
290    protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
291    {
292        long startTime = System.currentTimeMillis();
293
294        // Calculate contentTitle
295        String contentTitle = Optional.ofNullable(remoteValues.get(Content.ATTRIBUTE_TITLE))
296                .map(Collection::stream)
297                .orElseGet(Stream::empty)
298                .filter(String.class::isInstance)
299                .map(String.class::cast)
300                .filter(StringUtils::isNotBlank)
301                .findFirst()
302                .orElse(idValue);
303
304        // Create new content
305        logger.info("Start importing content '{}' for language {}", contentTitle, lang);
306
307        ModifiableContent content = createContentAction(lang, contentTitle, logger);
308        if (content != null)
309        {
310            _sccHelper.updateSCCProperty(content, getId());
311            
312            // Force syncCode as soon as possible
313            String syncCode = remoteValues.get(getIdField()).get(0).toString();
314            ValueContext context = ValueContext.newInstance();
315            if (getLocalAndExternalFields(Map.of("contentTypes", Arrays.asList(content.getTypes()))).contains(getIdField()))
316            {
317                context.withStatus(ExternalizableDataStatus.EXTERNAL);
318            }
319            DataHolderHelper.setValue(content, getIdField(), syncCode, context, true);
320            content.saveChanges();
321            
322            _fillContent(remoteValues, content, additionalParameters, true, logger);
323
324            if (content instanceof WorkflowAwareContent)
325            {
326                // Validate content if allowed
327                validateContent((WorkflowAwareContent) content, logger);
328            }
329
330            _nbCreatedContents++;
331
332            // Do additional operation on the content
333            SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator());
334            synchronizingContentOperator.additionalOperation(content, remoteValues, logger);
335
336            long endTime = System.currentTimeMillis();
337            logger.info("End import of content '{}' for language {} in {} ms", content.getId(), lang, endTime - startTime);
338        }
339        
340        return content;
341    }
342    
343    private void _ensureTitleIsPresent(Content content, Map<String, List<Object>> remoteValues, Logger logger)
344    {
345        if (remoteValues.containsKey(Content.ATTRIBUTE_TITLE))
346        {
347            boolean atLeastOneTitle =  remoteValues.get(Content.ATTRIBUTE_TITLE)
348                                                   .stream()
349                                                   .filter(String.class::isInstance)
350                                                   .map(String.class::cast)
351                                                   .anyMatch(StringUtils::isNotBlank);
352            if (atLeastOneTitle)
353            {
354                return;
355            }
356        }
357        
358        // Force to current title
359        logger.warn("The remote value of '{}' is empty for the content {}. The '{}' attribute is mandatory, the current title will remain.", Content.ATTRIBUTE_TITLE, content, Content.ATTRIBUTE_TITLE);
360        remoteValues.put(Content.ATTRIBUTE_TITLE, List.of(content.getTitle()));
361    }
362
363    @Override
364    public ModifiableContent getContent(String lang, String idValue)
365    {
366        String query = _getContentPathQuery(lang, idValue, getContentType());
367        AmetysObjectIterable<ModifiableContent> contents = _resolver.query(query);
368        
369        if (contents.getSize() > 0)
370        {
371            return contents.iterator().next();
372        }
373        return null;
374    }
375    
376    /**
377     * Creates content action with result from request
378     * @param lang The language
379     * @param contentTitle The content title
380     * @param logger The logger
381     * @return The content id, or null of a workflow error occurs
382     */
383    protected ModifiableContent createContentAction(String lang, String contentTitle, Logger logger)
384    {
385        return createContentAction(getContentType(), getWorkflowName(), getInitialActionId(), lang, contentTitle, logger);
386    }
387    
388    /**
389     * Fill the content with remote values.
390     * @param remoteValues The remote values
391     * @param content The content to synchronize
392     * @param additionalParameters Additional parameters
393     * @param create <code>true</code> if content is creating, false if it is updated
394     * @param logger The logger
395     * @return <code>true</code> if the content has been modified, <code>false</code> otherwise
396     * @throws Exception if an error occurs
397     */
398    protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableContent content, Map<String, Object> additionalParameters, boolean create, Logger logger) throws Exception
399    {
400        if (content instanceof WorkflowAwareContent)
401        {
402            // Transform remote value to get values with the cardinality corresponding to the model
403            Map<String, Object> contentValues = _transformRemoteValuesCardinality(remoteValues, getContentType());
404            // Remove the id field because it should be already set before calling this method
405            String idValue = (String) contentValues.remove(getIdField());
406            
407            // Add additional values
408            contentValues.putAll(getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger));
409
410            // Remove title from values if it is empty
411            if (StringUtils.isEmpty((String) contentValues.get(Content.ATTRIBUTE_TITLE)))
412            {
413                contentValues.remove(Content.ATTRIBUTE_TITLE);
414            }
415            
416            Set<String> notSynchronizedContentIds = getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, content.getLanguage(), logger);
417
418            // Get nested values supported by the EditContentFunction
419            Map<String, Object> nestedValues = _getNestedValues(contentValues);
420            return _editContent((WorkflowAwareContent) content, Optional.empty(), nestedValues, additionalParameters, create, notSynchronizedContentIds, logger);
421        }
422        
423        return false;
424    }
425    
426    /**
427     * Synchronize the content with given values.
428     * @param content The content to synchronize
429     * @param view the view containing the item to edit
430     * @param values the values
431     * @param additionalParameters Additional parameters
432     * @param create <code>true</code> if content is creating, <code>false</code> if it is updated
433     * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization
434     * @param logger The logger
435     * @return <code>true</code> if the content has been modified, <code>false</code> otherwise
436     * @throws WorkflowException if an error occurs
437     */
438    protected boolean _editContent(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, Map<String, Object> additionalParameters, boolean create, Set<String> notSynchronizedContentIds, Logger logger) throws WorkflowException
439    {
440        Map<String, Object> inputs = _getEditInputs(content, view, values, additionalParameters, create, notSynchronizedContentIds, logger);
441        Map<String, Object> actionResult = _contentWorkflowHelper.doAction(content, getSynchronizeActionId(), inputs);
442        return (boolean) actionResult.getOrDefault(AbstractContentWorkflowComponent.HAS_CHANGED_KEY, false);
443    }
444    
445    @SuppressWarnings("unchecked")
446    private static Map<String, Object> _getNestedValues(Map<String, Object> values)
447    {
448        Map<String, Object> nestedValues = new HashMap<>();
449        for (String key : values.keySet())
450        {
451            nestedValues = (Map<String, Object>) MapUtils.deepMerge(nestedValues, _getNestedValue(key, values.get(key)));
452        }
453        return nestedValues;
454    }
455    
456    @SuppressWarnings("unchecked")
457    private static Map<String, Object> _getNestedValue(String currentPath, Object currentValue)
458    {
459        Map<String, Object> nestedValues = new HashMap<>();
460        int separatorIndex = currentPath.indexOf('/');
461        if (separatorIndex < 0)
462        {
463            if (currentValue instanceof Map)
464            {
465                nestedValues.put(currentPath, _getNestedValues((Map<String, Object>) currentValue));
466            }
467            else
468            {
469                nestedValues.put(currentPath, currentValue);
470            }
471        }
472        else
473        {
474            nestedValues.put(currentPath.substring(0, separatorIndex), _getNestedValue(currentPath.substring(separatorIndex + 1), currentValue));
475        }
476        return nestedValues;
477    }
478    
479    /**
480     * Get the inputs for edit content function.
481     * @param content The content to synchronize
482     * @param view the view containing the item to edit
483     * @param values the values
484     * @param additionalParameters Additional parameters
485     * @param create <code>true</code> if content is creating, <code>false</code> if it is updated
486     * @param notSynchronizedContentIds the ids of the contents related to the given content but that are not part of the synchronization
487     * @param logger The logger
488     * @return the input parameters
489     */
490    protected Map<String, Object> _getEditInputs(WorkflowAwareContent content, Optional<View> view, Map<String, Object> values, Map<String, Object> additionalParameters, boolean create, Set<String> notSynchronizedContentIds, Logger logger)
491    {
492        Map<String, Object> inputs = new HashMap<>();
493        _addEditInputsForSCC(inputs, content, logger);
494        inputs.put(EditSynchronizedContentFunction.ADDITIONAL_PARAMS_KEY, additionalParameters);
495        inputs.put(EditSynchronizedContentFunction.SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, getSynchronizeActionId());
496        inputs.put(EditSynchronizedContentFunction.NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, notSynchronizedContentIds);
497        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, _getEditInputsContextParameters(view, values, create));
498        return inputs;
499    }
500
501    /**
502     * Add the inputs specific for the SCC to the inputs for edit content function.
503     * @param inputs The inputs to complete
504     * @param content The content to synchronize
505     * @param logger The logger
506     */
507    protected void _addEditInputsForSCC(Map<String, Object> inputs, WorkflowAwareContent content, Logger logger)
508    {
509        inputs.put(EditSynchronizedContentFunction.SCC_KEY, this);
510        inputs.put(EditSynchronizedContentFunction.SCC_LOGGER_KEY, logger);
511    }
512    
513    /**
514     * Get the context parameters to add to inputs for edit content function
515     * @param view the view containing the item to edit
516     * @param values the values
517     * @param create <code>true</code> if content is creating, <code>false</code> if it is updated
518     * @return the context parameters
519     */
520    protected Map<String, Object> _getEditInputsContextParameters(Optional<View> view, Map<String, Object> values, boolean create)
521    {
522        Map<String, Object> parameters = new HashMap<>();
523        parameters.put(EditContentFunction.VALUES_KEY, values);
524        view.ifPresent(v -> parameters.put(EditContentFunction.VIEW, v));
525        parameters.put(EditContentFunction.QUIT, true);
526        parameters.put(EditSynchronizedContentFunction.IMPORT, create);
527        return parameters;
528    }
529    
530    /**
531     * Validates a content after import
532     * @param content The content to validate
533     * @param logger The logger
534     */
535    protected void validateContent(WorkflowAwareContent content, Logger logger)
536    {
537        if (validateAfterImport())
538        {
539            validateContent(content, getValidateActionId(), logger);
540        }
541    }
542    
543    @Override
544    public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
545    {
546        // Search
547        Map<String, Map<String, Object>> results = internalSearch(_removeEmptyParameters(searchParameters), offset, limit, sort, logger);
548                
549        return results;
550    }
551
552    /**
553     * Search values and return the result without any treatment.
554     * @param searchParameters Search parameters to restrict the search
555     * @param offset Begin of the search
556     * @param limit Number of results
557     * @param sort Sort of results (ignored for LDAP results)
558     * @param logger The logger
559     * @return Map of results without any treatment.
560     */
561    protected abstract Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger);
562
563    /**
564     * Search values and return the result organized by attributes and transformed by the {@link SynchronizingContentOperator} if exists.
565     * @param searchParameters Search parameters to restrict the search
566     * @param logger The logger
567     * @return Map of results organized by attributes.
568     */
569    protected Map<String, Map<String, List<Object>>> getTransformedRemoteValues(Map<String, Object> searchParameters, Logger logger)
570    {
571        Map<String, Map<String, List<Object>>> remoteValues = getRemoteValues(searchParameters, logger);
572        return _transformRemoteValues(remoteValues, logger);
573    }
574    
575    /**
576     * Search values and return the result organized by attributes
577     * @param searchParameters Search parameters to restrict the search
578     * @param logger The logger
579     * @return Map of results organized by attributes.
580     */
581    protected abstract Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger);
582
583    /**
584     * Transform the given remote values by the {@link SynchronizingContentOperator} if exists.
585     * @param remoteValues The remote values
586     * @param logger The logger
587     * @return the transformed values
588     */
589    protected Map<String, Map<String, List<Object>>> _transformRemoteValues(Map<String, Map<String, List<Object>>> remoteValues, Logger logger)
590    {
591        SynchronizingContentOperator synchronizingContentOperator = _synchronizingContentOperatorEP.getExtension(getSynchronizingContentOperator());
592        if (synchronizingContentOperator != null)
593        {
594            Map<String, Map<String, List<Object>>> transformedRemoteValues = new LinkedHashMap<>();
595            ContentType contentType = _contentTypeEP.getExtension(getContentType());
596            for (String key : remoteValues.keySet())
597            {
598                transformedRemoteValues.put(key, synchronizingContentOperator.transform(contentType, remoteValues.get(key), logger));
599            }
600            
601            return transformedRemoteValues;
602        }
603        else
604        {
605            logger.warn("Cannot find synchronizing content operator with id '{}'. No transformation has applied on remote values", getSynchronizingContentOperator());
606            return remoteValues; // no transformation
607        }
608    }
609    
610    /**
611     * Retrieves additional values to synchronize for a content
612     * @param idValue id value of the content
613     * @param content The content
614     * @param additionalParameters Additional parameters
615     * @param create <code>true</code> if the content has been newly created, <code>false</code> otherwise
616     * @param logger The logger
617     * @return the values to add
618     */
619    protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger)
620    {
621        // No additional values by default
622        return new LinkedHashMap<>();
623    }
624    
625    /**
626     * Retrieves the ids of the contents related to the given content but that are not part of the synchronization
627     * @param content content
628     * @param contentValues the content values that will be set
629     * @param additionalParameters Additional parameters
630     * @param lang Language of the content
631     * @param logger The logger
632     * @return the ids of the contents that are not part of the synchronization
633     */
634    protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger)
635    {
636        // All contents are synchronized by default
637        return new HashSet<>();
638    }
639    
640    @Override
641    public void updateSyncInformations(ModifiableContent content, String syncCode, Logger logger) throws Exception
642    {
643        _sccHelper.updateSCCProperty(content, getId());
644        content.setValue(getIdField(), syncCode);
645        content.saveChanges();
646        
647        if (content instanceof VersionableAmetysObject)
648        {
649            ((VersionableAmetysObject) content).checkpoint();
650        }
651    }
652
653    @Override
654    public int getTotalCount(Map<String, Object> searchParameters, Logger logger)
655    {
656        return search(searchParameters, 0, Integer.MAX_VALUE, null, logger).size();
657    }
658
659    /**
660     * Import or synchronize several contents from search params.
661     * @param searchParameters Search parameters
662     * @param forceImport To force import and ignoring the synchronize existing contents only option
663     * @param logger The logger
664     * @return The {@link List} of imported or synchronized {@link ModifiableContent}
665     */
666    protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParameters, boolean forceImport, Logger logger)
667    {
668        List<ModifiableContent> contents = new ArrayList<>();
669        
670        Map<String, Map<String, List<Object>>> remoteValuesByContent = getTransformedRemoteValues(searchParameters, logger);
671        for (String idValue : remoteValuesByContent.keySet())
672        {
673            Map<String, List<Object>> remoteValues = remoteValuesByContent.get(idValue);
674            _handleContent(idValue);
675            contents.addAll(_importOrSynchronizeContent(idValue, remoteValues, forceImport, logger));
676        }
677        
678        return contents;
679    }
680    
681    @Override
682    protected List<Content> _getContentsToRemove(AmetysObjectIterable<ModifiableContent> contents)
683    {
684        return contents.stream()
685            .filter(content -> !_isHandled(content.getValue(getIdField())))
686            .collect(Collectors.toList());
687    }
688}