001/*
002 *  Copyright 2022 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.odf.workflow.task;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.stream.Collectors;
027import java.util.stream.Stream;
028
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.ProcessingException;
034import org.apache.cocoon.xml.XMLUtils;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.CmsConstants;
039import org.ametys.cms.content.compare.ContentComparatorChange;
040import org.ametys.cms.content.version.CompareVersionHelper;
041import org.ametys.cms.repository.Content;
042import org.ametys.core.util.JSONUtils;
043import org.ametys.plugins.repository.AmetysRepositoryException;
044import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
045import org.ametys.runtime.model.DefinitionContext;
046import org.ametys.runtime.model.ModelItem;
047
048/**
049 * Override {@link AbstractOdfWorkflowTasksComponent} to add the change informations
050 */
051public abstract class AbstractOdfWorkflowTasksWithChangesComponent extends AbstractOdfWorkflowTasksComponent
052{
053
054    /** The JSON util */
055    protected JSONUtils _jsonUtils;
056    /** The helper for comparing versions */
057    protected CompareVersionHelper _compareVersionHelper;
058    /** Map of show changes indexed by task id */
059    protected Map<String, Boolean> _showChanges = new HashMap<>();
060    /** Map of show important changes indexed by task id */
061    protected Map<String, Boolean> _showImportantChanges = new HashMap<>();
062    /** The (defined in configuration) important attributes by task */
063    protected Map<String, Collection<String>> _importantAttributes = new HashMap<>();
064
065    @Override
066    public void service(ServiceManager manager) throws ServiceException
067    {
068        super.service(manager);
069        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
070        _compareVersionHelper = (CompareVersionHelper) manager.lookup(CompareVersionHelper.ROLE);
071    }
072
073    @Override
074    protected void _configureAdditional(Task task, Configuration taskConf) throws ConfigurationException
075    {
076        _configureChanges(taskConf);
077    }
078
079    /**
080     * Determines if changes have to be shown for the given task
081     * @param task the task
082     * @return <code>true</code> if changes are configured to be SAXed
083     */
084    public boolean showChanges(Task task)
085    {
086        return _showChanges.containsKey(task.getId()) && _showChanges.get(task.getId());
087    }
088
089    /**
090     * Determines if important changes have to be shown for the given task
091     * @param task the task
092     * @return <code>true</code> if changes are configured to be SAXed
093     */
094    public boolean showImportantChanges(Task task)
095    {
096        return _showImportantChanges.containsKey(task.getId()) && _showImportantChanges.get(task.getId());
097    }
098
099    /**
100     * Configures the changes to SAX
101     * @param taskConfiguration The task configuration
102     * @throws ConfigurationException  If the configuration is invalid.
103     */
104    protected void _configureChanges(Configuration taskConfiguration) throws ConfigurationException
105    {
106        String taskId = taskConfiguration.getAttribute("id", "").trim();
107        
108        _showChanges.put(taskId, taskConfiguration.getChild("show-changes", false) != null);
109        
110        Configuration importantChangesConf = taskConfiguration.getChild("show-important-changes", false);
111        _showImportantChanges.put(taskId, importantChangesConf != null);
112        
113        if (importantChangesConf != null)
114        {
115            Configuration[] attsConf = importantChangesConf.getChildren("attribute");
116            Collection<String> importantAttributes = Stream.of(attsConf)
117                    .map(attr -> attr.getAttribute("ref", null))
118                    .filter(Objects::nonNull)
119                    .collect(Collectors.toList());
120            
121            _importantAttributes.put(taskId, importantAttributes);
122        }
123    }
124
125    @Override
126    protected void _saxAdditionalData(ContentHandler ch, Content content, Task task) throws SAXException
127    {
128        super._saxAdditionalData(ch, content, task);
129        
130        if (content instanceof VersionAwareAmetysObject && (showChanges(task) || showImportantChanges(task)))
131        {
132            try
133            {
134                List<ContentComparatorChange> changes = _getChanges((Content & VersionAwareAmetysObject) content);
135                _saxAttributeChanges(ch, content, task, changes);
136            }
137            catch (AmetysRepositoryException | IOException e)
138            {
139                throw new SAXException("Cannot determine the change of attributes for Content '" + content + "'", e);
140            }
141        }
142    }
143
144    /**
145     * Gets the changes
146     * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content}
147     * @param content The content
148     * @return The changes
149     * @throws AmetysRepositoryException repository exception
150     * @throws IOException IO exception
151     */
152    protected <C extends Content & VersionAwareAmetysObject> List<ContentComparatorChange> _getChanges(C content) throws AmetysRepositoryException, IOException
153    {
154        Optional<String> baseVersion = _getBaseVersion(content);
155        if (baseVersion.isEmpty())
156        {
157            // No target version => no change
158            return Collections.emptyList();
159        }
160        return _compareVersionHelper.compareVersions(content.getId(), _getTargetVersion(content), baseVersion.get())
161                .getChanges();
162    }
163
164    /**
165     * Gets the source version of comparison. By default, it is the current one.
166     * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content}
167     * @param versionable The {@link Content}
168     * @return the source version
169     */
170    protected <C extends Content & VersionAwareAmetysObject> String _getTargetVersion(C versionable)
171    {
172        return _compareVersionHelper.getCurrentVersion(versionable);
173    }
174
175    /**
176     * Gets the base version of comparison. By default, it is the last validated one.
177     * @param <C> The type of the {@link VersionAwareAmetysObject} {@link Content}
178     * @param versionable The {@link Content}
179     * @return the target version
180     */
181    protected <C extends Content & VersionAwareAmetysObject> Optional<String> _getBaseVersion(C versionable)
182    {
183        return _compareVersionHelper.getLastVersionWithLabel(versionable, CmsConstants.LIVE_LABEL);
184    }
185
186    /**
187     * SAX attributes of the given content which changed since the last validation
188     * @param ch The content handler
189     * @param content The content
190     * @param task the current task
191     * @param changes The changes
192     * @throws SAXException If an error occurred
193     */
194    protected void _saxAttributeChanges(ContentHandler ch, Content content, Task task, List<ContentComparatorChange> changes) throws SAXException
195    {
196        /*
197         * SAX as stringified JSON as ExtJS XML reader seem to not be able to read multiple values
198         * if format is 
199         * <change name="degree" isImportant="true">
200         *   <label>
201         *     <i18n:text key="" catalogue=""/>
202         *   </label>
203         * </change>
204         * <change name="domain" isImportant="false">
205         *   <label>
206         *     <i18n:text key="" catalogue=""/>
207         *   </label>
208         * </change>
209         * 
210         * for instance.
211         * 
212         * So use JSON for easier deserialization
213         */
214        List<Map<String, Object>> changesAsJson = _getAttributeChanges(task, changes);
215        String changesAsString = _jsonUtils.convertObjectToJson(changesAsJson);
216        XMLUtils.createElement(ch, "attribute-changes", changesAsString);
217    }
218
219    /**
220     * Gets the attribute changes (as JSON) of the given content which changed since the last validation
221     * @param task the current task
222     * @param changes The changes
223     * @return The changes as JSON
224     */
225    protected List<Map<String, Object>> _getAttributeChanges(Task task, List<ContentComparatorChange> changes)
226    {
227        return _compareVersionHelper.getChangedModelItems(changes)
228                .map(attributeChange ->
229                {
230                    boolean isImportant = _isImportant(task, attributeChange);
231                    return _toJson(attributeChange, isImportant);
232                })
233                .collect(Collectors.toList());
234    }
235
236    private Map<String, Object> _toJson(ModelItem modelItem, boolean isImportant)
237    {
238        try
239        {
240            return Map.of(
241                    "modelItem", modelItem.toJSON(DefinitionContext.newInstance()),
242                    "isImportant", isImportant);
243        }
244        catch (ProcessingException e)
245        {
246            throw new IllegalStateException("Cannot transform modelItem to JSON", e);
247        }
248    }
249
250    /**
251     * Determines if a change is important
252     * @param task the current task
253     * @param changedModelItem The changed {@link ModelItem}
254     * @return <code>true</code> if the change is important
255     */
256    protected boolean _isImportant(Task task, ModelItem changedModelItem)
257    {
258        if (_importantAttributes.containsKey(task.getId()))
259        {
260            return _importantAttributes.get(task.getId()).contains(changedModelItem.getPath());
261        }
262        return false;
263    }
264
265}