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