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