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