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