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}