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