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