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