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