001/*
002 *  Copyright 2020 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.cms.workflow;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang3.StringUtils;
028
029import org.ametys.cms.content.compare.ContentComparator;
030import org.ametys.cms.content.compare.ContentComparatorChange;
031import org.ametys.cms.data.ContentValue;
032import org.ametys.cms.data.type.ModelItemTypeConstants;
033import org.ametys.cms.repository.Content;
034import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
035import org.ametys.cms.repository.WorkflowAwareContent;
036import org.ametys.cms.workflow.history.VersionInformation;
037import org.ametys.core.user.UserIdentity;
038import org.ametys.core.util.I18nUtils;
039import org.ametys.plugins.repository.AmetysObjectResolver;
040import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
041import org.ametys.plugins.repository.lock.LockHelper;
042import org.ametys.plugins.repository.lock.LockableAmetysObject;
043import org.ametys.plugins.repository.model.RepositoryDataContext;
044import org.ametys.plugins.repository.version.VersionableAmetysObject;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.i18n.I18nizableTextParameter;
047import org.ametys.runtime.model.ElementDefinition;
048import org.ametys.runtime.model.ModelHelper;
049import org.ametys.runtime.model.View;
050import org.ametys.runtime.model.ViewItemContainer;
051import org.ametys.runtime.model.type.DataContext;
052
053import com.opensymphony.module.propertyset.PropertySet;
054import com.opensymphony.workflow.WorkflowException;
055
056/**
057 * OSWorkflow function to restore an old revision of a content.
058 * Builds a Map with the old content's attributes values, and passes it to the
059 * {@link EditContentFunction}, which does the real job.
060 */
061public class RestoreRevisionFunction extends AbstractContentFunction
062{
063    private AmetysObjectResolver _resolver;
064    private ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
065    private ContentComparator _contentComparator;
066    private I18nUtils _i18nUtils;
067    
068    @Override
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        super.service(manager);
072        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
073        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
074        _contentComparator = (ContentComparator) manager.lookup(ContentComparator.ROLE);
075        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
076    }
077    
078    @Override
079    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
080    {
081        WorkflowAwareContent content = getContent(transientVars);
082        UserIdentity user = getUser(transientVars);
083        
084        if (!(content instanceof ModifiableWorkflowAwareContent))
085        {
086            throw new IllegalArgumentException("The provided content " + content.getId() + " is not a ModifiableWorkflowAwareContent.");
087        }
088        
089        ModifiableWorkflowAwareContent modifiableContent = (ModifiableWorkflowAwareContent) content;
090        
091        if (content instanceof LockableAmetysObject)
092        {
093            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
094            if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user))
095            {
096                throw new WorkflowException ("The user '" + user + "' try to restore the content '" + content.getId() + "', but this content is locked by the user '" + user + "'");
097            }
098            else if (lockableContent.isLocked())
099            {
100                lockableContent.unlock();
101            }
102        }
103        
104        String contentVersion = (String) getContextParameters(transientVars).get("contentVersion");
105        
106        Content oldContent = _resolver.resolveById(content.getId());
107        if (oldContent instanceof VersionableAmetysObject versionAware)
108        {
109            versionAware.switchToRevision(contentVersion);
110        }
111        
112        Map<String, Object> results = getResultsMap(transientVars);
113        Map<String, Object> brokenReferences = new HashMap<>();
114        results.put("brokenReferences", brokenReferences);
115        
116        Map<String, Object> parameters = getContextParameters(transientVars);
117        Set<String> externalizableData = _externalizableDataProviderEP.getExternalizableDataPaths(content);
118        
119        // Get values from version to restore
120        DataContext context = RepositoryDataContext.newInstance()
121            .withExternalizableData(externalizableData)
122            .withEmptyValues(true);
123        Map<String, Object> newValues = oldContent.dataToMap(context);
124        
125        // CMS-11242 Only keep values with differences
126        String[] changingAttributes = _contentComparator.compare(content, oldContent)
127            .getChanges()
128            .stream()
129            .map(ContentComparatorChange::getAttributeDataPath)
130            .map(ModelHelper::getDefinitionPathFromDataPath)
131            .distinct()
132            .toArray(String[]::new);
133        
134        String language = oldContent.getLanguage();
135        
136        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
137        i18nParams.put("versionNumber", new I18nizableText(VersionInformation.getVersionNameFromRawVersion(contentVersion)));
138        String versionNumberComment = _i18nUtils.translate(new I18nizableText("plugin.cms", "CONTENT_RESTORE_COMMENT_VERSION_NUMBER", i18nParams), language);
139        
140        StringBuilder sb = new StringBuilder();
141        sb.append(versionNumberComment);
142        
143        String comment = (String) transientVars.get("comment");
144        if (StringUtils.isNotEmpty(comment))
145        {
146            sb.append("\n");
147            sb.append(comment);
148        }
149        
150        transientVars.put("comment", sb.toString());
151        
152        View view = View.of(content.getModel(), changingAttributes);
153        
154        // Exclude invalid links
155        newValues = _processContents(view, newValues, "", brokenReferences);
156        
157        parameters.put(EditContentFunction.VIEW, view);
158        parameters.put(EditContentFunction.VALUES_KEY, newValues);
159        parameters.put(EditContentFunction.GLOBAL_VALIDATION, false);
160        parameters.put(EditContentFunction.QUIT, Boolean.TRUE);
161        
162        modifiableContent.setLastContributor(user);
163        modifiableContent.setLastModified(ZonedDateTime.now());
164        
165        // Remove the proposal date.
166        modifiableContent.setProposalDate(null);
167        
168        // Commit changes
169        modifiableContent.saveChanges();
170    }
171    
172    private ContentValue _processContentValue(ContentValue value, String dataPath, ElementDefinition definition, Map<String, Object> brokenReferences)
173    {
174        if (_resolver.hasAmetysObjectForId(value.getContentId()))
175        {
176            return value;
177        }
178        else if (!brokenReferences.containsKey(dataPath))
179        {
180            brokenReferences.put(dataPath, definition.getLabel());
181        }
182        
183        return null;
184    }
185    
186    @SuppressWarnings("unchecked")
187    private Map<String, Object> _processContents(ViewItemContainer viewItemContainer, Map<String, Object> values, String dataPath, Map<String, Object> brokenReferences)
188    {
189        if (values == null)
190        {
191            return null;
192        }
193        
194        Map<String, Object> result = new HashMap<>();
195        
196        org.ametys.plugins.repository.model.ViewHelper.visitView(viewItemContainer,
197            (element, definition) -> {
198                // simple element
199                String name = definition.getName();
200                Object value = values.get(name);
201                
202                if (values.containsKey(name))
203                {
204                    if (definition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))
205                    {
206                        Object newValue = null;
207                        if (value instanceof ContentValue[])
208                        {
209                            List<ContentValue> validContents = new ArrayList<>();
210                            
211                            for (ContentValue contentValue : (ContentValue[]) value)
212                            {
213                                ContentValue newContentValue = _processContentValue(contentValue, dataPath + name, definition, brokenReferences);
214                                if (newContentValue != null)
215                                {
216                                    validContents.add(newContentValue);
217                                }
218                            }
219                            
220                            newValue = validContents;
221                        }
222                        else if (value instanceof ContentValue)
223                        {
224                            ContentValue newContentValue = _processContentValue((ContentValue) value, dataPath + name, definition, brokenReferences);
225                            newValue = newContentValue;
226                        }
227                        
228                        result.put(name, newValue);
229                    }
230                    else
231                    {
232                        result.put(name, value);
233                    }
234                }
235            },
236            (group, definition) -> {
237                // composite
238                String name = definition.getName();
239                if (values.containsKey(name))
240                {
241                    result.put(name, _processContents(group, (Map<String, Object>) values.get(name), dataPath + name + "/" , brokenReferences));
242                }
243            },
244            (group, definition) -> {
245                // repeater
246                String name = definition.getName();
247                if (values.containsKey(name))
248                {
249                    List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name);
250                    
251                    List<Map<String, Object>> newEntries = new ArrayList<>();
252                    for (int i = 0; i < entries.size(); i++)
253                    {
254                        newEntries.add(_processContents(group, entries.get(i), dataPath + name + "[" + (i + 1) + "]/" , brokenReferences));
255                    }
256                
257                    result.put(name, newEntries);
258                }
259            },
260            group -> result.putAll(_processContents(group, values, dataPath, brokenReferences)));
261        
262        return result;
263    }
264    
265    @Override
266    public FunctionType getFunctionExecType()
267    {
268        return FunctionType.PRE;
269    }
270    
271    @Override
272    public I18nizableText getLabel()
273    {
274        return new I18nizableText("plugin.cms", "PLUGINS_CMS_RESTORE_REVISION_FUNCTION_LABEL");
275    }
276}