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