001/*
002 *  Copyright 2021 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.workflow;
017
018import java.util.HashMap;
019import java.util.Map;
020import java.util.Optional;
021import java.util.Set;
022
023import org.slf4j.Logger;
024
025import org.ametys.cms.ObservationConstants;
026import org.ametys.cms.data.ContentSynchronizationContext;
027import org.ametys.cms.data.ContentSynchronizationResult;
028import org.ametys.cms.repository.Content;
029import org.ametys.cms.repository.ModifiableContent;
030import org.ametys.cms.workflow.AbstractContentFunction;
031import org.ametys.cms.workflow.EditContentFunction;
032import org.ametys.core.observation.Event;
033import org.ametys.core.user.UserIdentity;
034import org.ametys.core.util.AvalonLoggerAdapter;
035import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
036import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDataProvider;
037import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
038import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
039import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper.SynchronizableValuesStatusInfos;
040import org.ametys.plugins.repository.data.holder.values.SynchronizationResult;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.runtime.model.ModelItem;
043import org.ametys.runtime.model.View;
044import org.ametys.runtime.parameter.ValidationResult;
045import org.ametys.runtime.parameter.ValidationResults;
046
047import com.opensymphony.workflow.WorkflowException;
048
049/**
050 * OSWorkflow function to edit (import or synchronize) a synchronized content.
051 */
052public class EditSynchronizedContentFunction extends EditContentFunction
053{
054    /** Constant for storing the action id for editing invert relations on synchronized contents. */
055    public static final String SYNCHRO_INVERT_EDIT_ACTION_ID_KEY = EditSynchronizedContentFunction.class.getName() + "$synchroInvertEditActionId";
056    /** Constant for storing the action id for editing invert relations. */
057    public static final String INVERT_EDIT_ACTION_ID_KEY = EditSynchronizedContentFunction.class.getName() + "$invertEditActionId";
058    
059    /** Constant for storing the ids of the contents related to the current content but that are not part of the synchronization **/
060    public static final String NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY = EditSynchronizedContentFunction.class.getName() + "$notSynchronizedContents";
061    
062    /** Constant for storing the scc into the transient variables map. */
063    public static final String SCC_KEY = SynchronizableContentsCollection.class.getName();
064    /** Constant for storing the scc logger into the transient variables map. */
065    public static final String SCC_LOGGER_KEY = SynchronizableContentsCollection.class.getName() + "$logger";
066    /** Constant for storing the import parameters into the transient variables map. */
067    public static final String ADDITIONAL_PARAMS_KEY = SynchronizableContentsCollection.class.getName() + "$additionalParams";
068    
069    /** Default action id of editing invert relations on synchronized contents. */
070    public static final int DEFAULT_SYNCHRO_INVERT_EDIT_ACTION_ID = 800;
071    /** Default action id of editing revert relations. */
072    public static final int DEFAULT_INVERT_EDIT_ACTION_ID = 22;
073    
074    /** 
075     * Import mode parameter.
076     * If the parameter is present and set to <code>true</code>, the content is considered as imported.
077     * Otherwise, it is considered as synchronized.
078     */
079    public static final String IMPORT = "import";
080    
081    @Override
082    protected ContentSynchronizationContext getSynchronizationContext(Map transientVars)
083    {
084        ContentSynchronizationContext synchronizationContext = super.getSynchronizationContext(transientVars)
085                .withStatus(ExternalizableDataStatus.EXTERNAL);
086        
087        getSynchronizableContentsCollection(transientVars)
088            .map(SynchronizableContentsCollection::getId)
089            .ifPresent(sccId -> synchronizationContext.withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, sccId));
090        
091        return synchronizationContext;
092    }
093    
094    @Override
095    protected Map<String, Object> _convertValues(ModifiableContent content, View view, Map<String, Object> values, Map transientVars)
096    {
097        Map<String, Object> externalizableContext = new HashMap<>(); 
098        getSynchronizableContentsCollection(transientVars)
099                .map(SynchronizableContentsCollection::getId)
100                .ifPresent(sccId -> externalizableContext.put(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, sccId));
101        
102        SynchronizableValuesStatusInfos statusInfos = new SynchronizableValuesStatusInfos(content, externalizableContext);
103        
104        return DataHolderHelper.convertValuesWithSynchronizableValues(view, values, DataHolderHelper::convertValueIgnoringIncompatibleOnes, Optional.of(statusInfos));
105    }
106    
107    @Override
108    protected ValidationResults validateValues(View view, ModifiableContent content, Map<String, Object> values, Map transientVars) throws WorkflowException
109    {
110        // Do not validate synchronized values.
111        // Sometimes want to be able to get invalid data at the end of the synchronization.
112        // The validation workflow will fail and the content won't be valid anymore. That's normal
113        return new ValidationResults();
114    }
115    
116    @Override
117    protected ValidationResult globalValidate(View view, Content content, Map<String, Object> values)
118    {
119        // Do not validate synchronized values.
120        // Sometimes want to be able to get invalid data at the end of the synchronization.
121        // The validation workflow will fail and the content won't be valid anymore. That's normal
122        return ValidationResult.empty();
123    }
124    
125    @Override
126    protected ContentSynchronizationResult additionalOperations(ModifiableContent content, Map transientVars) throws WorkflowException
127    {
128        ContentSynchronizationResult result = super.additionalOperations(content, transientVars);
129
130        Optional<SynchronizableContentsCollection> scc = getSynchronizableContentsCollection(transientVars);
131        // There is no SCC in transient var when the modification is due to an invert relation
132        if (scc.isPresent())
133        {
134            Map<String, Object> additionalParams = getAdditionalParameters(transientVars);
135            ContentSynchronizationResult additionalResult;
136
137            // Do the SCC additional operations
138            if (isImportMode(transientVars))
139            {
140                additionalResult = scc.get().additionalImportOperations(content, additionalParams, getLogger(transientVars));
141            }
142            else
143            {
144                additionalResult = scc.get().additionalSynchronizeOperations(content, additionalParams, getLogger(transientVars));
145            }
146            
147            result.aggregateResult(additionalResult);
148        }
149        
150        return result;
151    }
152    
153    /**
154     * Determine if the content is being imported or synchronized
155     * @param transientVars the parameters from the call.
156     * @return <code>true</code> if the content is being imported, <code>false</code> otherwise
157     * @throws WorkflowException if the object model or the request is not present
158     */
159    protected boolean isImportMode(Map transientVars) throws WorkflowException
160    {
161        Map<String, Object> parameters = getContextParameters(transientVars);
162        Boolean importContent = (Boolean) parameters.get(IMPORT);
163        return Boolean.TRUE.equals(importContent);
164    }
165    
166    /**
167     * Retrieve the scc associated with the workflow.
168     * @param transientVars the parameters from the call.
169     * @return the scc.
170     */
171    protected Optional<SynchronizableContentsCollection> getSynchronizableContentsCollection(Map transientVars)
172    {
173        return Optional.of(transientVars)
174                .map(vars -> vars.get(SCC_KEY))
175                .filter(SynchronizableContentsCollection.class::isInstance)
176                .map(SynchronizableContentsCollection.class::cast);
177    }
178    
179    /**
180     * Retrieve the scc logger
181     * @param transientVars the parameters from the call.
182     * @return the scc logger
183     */
184    protected Logger getLogger(Map transientVars)
185    {
186        return Optional.of(transientVars)
187                .map(vars -> vars.get(SCC_LOGGER_KEY))
188                .filter(Logger.class::isInstance)
189                .map(Logger.class::cast)
190                .orElseGet(() -> new AvalonLoggerAdapter(_logger));
191    }
192    
193    /**
194     * Retrieve the synchronization additional parameters
195     * @param transientVars the parameters from the call.
196     * @return the synchronization additional parameters
197     */
198    protected Map<String, Object> getAdditionalParameters(Map transientVars)
199    {
200        return Optional.of(transientVars)
201                .map(vars -> vars.get(ADDITIONAL_PARAMS_KEY))
202                .filter(Map.class::isInstance)
203                .map(Map.class::cast)
204                .orElseGet(HashMap::new);
205    }
206    
207    @Override
208    protected int getInvertEditActionId(Map transientVars, Content referencedContent)
209    {
210        int synchroInvertActionId = (int) transientVars.getOrDefault(SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, DEFAULT_SYNCHRO_INVERT_EDIT_ACTION_ID);
211        int invertActionId = (int) transientVars.getOrDefault(INVERT_EDIT_ACTION_ID_KEY, DEFAULT_INVERT_EDIT_ACTION_ID);
212        
213        @SuppressWarnings("unchecked")
214        Set<String> notSynchronizedContents = (Set<String>) transientVars.getOrDefault(NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, Set.of());
215        return notSynchronizedContents.contains(referencedContent.getId()) ? invertActionId : synchroInvertActionId;
216    }
217    
218    @Override
219    protected void notifyContentModifying(Content content, Map<String, Object> values, Map transientVars) throws WorkflowException
220    {
221        // Do nothing
222    }
223    
224    @Override
225    protected void updateCommonMetadata(ModifiableContent content, UserIdentity user, SynchronizationResult synchronizationResult) throws WorkflowException
226    {
227        if (synchronizationResult.hasChanged())
228        {
229            super.updateCommonMetadata(content, user, synchronizationResult);
230        }
231    }
232    
233    @Override
234    protected void extractOutgoingReferences(ModifiableContent content, SynchronizationResult synchronizationResult)
235    {
236        if (synchronizationResult.hasChanged())
237        {
238            super.extractOutgoingReferences(content, synchronizationResult);
239        }
240    }
241    
242    @Override
243    protected void prepareOrNotifyContentModified(Content content, Map transientVars, Map args, SynchronizationResult synchronizationResult) throws WorkflowException
244    {
245        if (synchronizationResult.hasChanged())
246        {
247            boolean notify = Boolean.parseBoolean((String) args.getOrDefault(KEY_NOTIFY_ARGUMENTS, "true"));
248            if (notify)
249            {
250                Map<String, Object> eventParams = new HashMap<>();
251                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
252                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
253                
254                _observationManager.notify(new Event(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, getUser(transientVars), eventParams));
255            }
256            else // if notify is false, just prepare the event to send. A post notifyFunction will notify this event...
257            {
258                transientVars.put(AbstractContentFunction.EVENT_TO_NOTIFY_KEY, org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED);
259            }
260        }
261    }
262    
263    @Override
264    protected boolean _canWriteModelItem(ModelItem modelItem, Content content, Map transientVars)
265    {
266        if (getSynchronizableContentsCollection(transientVars)
267                .map(SynchronizableContentsCollection::ignoreRestrictions)
268                .orElse(false))
269        {
270            return true;
271        }
272        return super._canWriteModelItem(modelItem, content, transientVars);
273    }
274    
275    @Override
276    public I18nizableText getLabel()
277    {
278        return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_EDIT_CONTENT_FUNCTION_LABEL");
279    }
280}