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