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