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