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}