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