001/* 002 * Copyright 2025 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.util.ArrayList; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022import java.util.stream.Collectors; 023 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.commons.lang3.StringUtils; 027 028import org.ametys.cms.data.type.impl.BooleanRepositoryElementType; 029import org.ametys.cms.repository.Content; 030import org.ametys.cms.repository.ModifiableContent; 031import org.ametys.cms.workflow.EditContentFunction; 032import org.ametys.core.right.RightManager; 033import org.ametys.core.right.RightManager.RightResult; 034import org.ametys.odf.EducationalPathHelper; 035import org.ametys.odf.data.EducationalPath; 036import org.ametys.odf.rights.ODFRightHelper.ContextualizedContent; 037import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 038import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 039import org.ametys.plugins.repository.model.RepeaterDefinition; 040import org.ametys.plugins.repository.model.ViewHelper; 041import org.ametys.runtime.model.ModelHelper; 042import org.ametys.runtime.model.ModelItem; 043import org.ametys.runtime.model.ModelItemGroup; 044import org.ametys.runtime.model.View; 045import org.ametys.runtime.model.ViewItemContainer; 046import org.ametys.runtime.model.disableconditions.DisableCondition; 047import org.ametys.runtime.model.disableconditions.DisableConditions; 048import org.ametys.runtime.model.type.DataContext; 049 050import com.opensymphony.module.propertyset.PropertySet; 051import com.opensymphony.workflow.WorkflowException; 052 053/** 054 * Edit content function restricted to contextualized data, ie. data into repeater with educational path 055 * This function allows to edit data by educational path for which the user is responsible. 056 */ 057public class EditContextualizedDataFunction extends EditContentFunction 058{ 059 /** The workflow action id to edit repeater with educational path only */ 060 public static final int EDIT_WORKFLOW_ACTION_ID = 20; 061 062 /** Name of educational path attribute in a repeater with education path */ 063 public static final String REPEATER_EDUCATIONAL_PATH_ATTRIBUTE_NAME = "path"; 064 /** Name of common attribute in a repeater with education path (means that entry is common to all educational paths)*/ 065 public static final String REPEATER_COMMON_ATTRIBUTE_NAME = "common"; 066 067 private EducationalPathHelper _educationalPathHelper; 068 private RightManager _rightManager; 069 private String _rightId; 070 071 @Override 072 public void service(ServiceManager smanager) throws ServiceException 073 { 074 super.service(smanager); 075 _educationalPathHelper = (EducationalPathHelper) smanager.lookup(EducationalPathHelper.ROLE); 076 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 077 } 078 079 @Override 080 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 081 { 082 _rightId = (String) args.get("right"); 083 super.execute(transientVars, args, ps); 084 } 085 086 @Override 087 protected boolean canWriteModelItem(ModelItem modelItem, Content content, Map transientVars) 088 { 089 // Only repeater with educational path and its child model items are writable 090 // Always enable boolean "common" item into a repeater with educational path (whatever model item's restrictions) 091 return _inRepeaterWithEducationalPath(modelItem) && (_isCommonBooleanItem(modelItem) || super.canWriteModelItem(modelItem, content, transientVars)); 092 } 093 094 private boolean _isCommonBooleanItem(ModelItem modelItem) 095 { 096 return modelItem.getType() instanceof BooleanRepositoryElementType 097 && modelItem.getName().equals(REPEATER_COMMON_ATTRIBUTE_NAME) 098 && modelItem.getParent() != null 099 && modelItem.getParent() instanceof RepeaterDefinition; 100 } 101 102 private boolean _inRepeaterWithEducationalPath(ModelItem modelItem) 103 { 104 if (modelItem instanceof RepeaterDefinition repeaterDefinition && _educationalPathHelper.isRepeaterWithEducationalPath(repeaterDefinition)) 105 { 106 return true; 107 } 108 109 ModelItemGroup parentModelItem = modelItem.getParent(); 110 return parentModelItem != null ? _inRepeaterWithEducationalPath(parentModelItem) : false; 111 } 112 113 @Override 114 protected Map<String, Object> parseValues(View view, ModifiableContent content, Map<String, Object> rawValues, boolean localOnly, Map transientVars) 115 { 116 Map<String, Object> values = super.parseValues(view, content, rawValues, localOnly, transientVars); 117 118 _parseRepeaterWithPathValues(content, view, values); 119 120 return values; 121 } 122 123 @SuppressWarnings("unchecked") 124 private void _parseRepeaterWithPathValues(Content content, ViewItemContainer viewItemContainer, Map<String, Object> values) 125 { 126 ViewHelper.visitView(viewItemContainer, 127 (element, definition) -> { 128 // nothing 129 }, 130 (group, definition) -> { 131 // composite 132 String name = definition.getName(); 133 Map<String, Object> composite = (Map<String, Object>) values.get(name); 134 if (composite != null) 135 { 136 _parseRepeaterWithPathValues(content, group, composite); 137 } 138 }, 139 (group, definition) -> { 140 // repeater 141 String name = definition.getName(); 142 List<Map<String, Object>> entries = ((SynchronizableRepeater) values.get(name)).getEntries(); 143 if (entries != null) 144 { 145 if (_educationalPathHelper.isRepeaterWithEducationalPath(definition)) 146 { 147 values.put(name, _updateRepeaterWithPathValues(content, entries, definition)); 148 } 149 else 150 { 151 for (Map<String, Object> entry : entries) 152 { 153 _parseRepeaterWithPathValues(content, group, entry); 154 } 155 } 156 } 157 }, 158 group -> _parseRepeaterWithPathValues(content, group, values)); 159 } 160 161 162 // Update repeater values. Only entries with allowed path will be really updated 163 private List<Map<String, Object>> _updateRepeaterWithPathValues(Content content, List<Map<String, Object>> repeaterValues, RepeaterDefinition repeaterDefinition) 164 { 165 List<Map<String, Object>> updatedValues = new ArrayList<>(); 166 167 String repeaterPath = repeaterDefinition.getPath(); 168 169 if (content.hasValue(repeaterPath)) 170 { 171 // Get common entries and entries with unauthorized path from content 172 List<Map<String, Object>> entries = _getRepeaterEntries(content, repeaterDefinition); 173 for (Map<String, Object> entry : entries) 174 { 175 boolean commonEntry = _isCommonEntry(repeaterDefinition, entry); 176 EducationalPath educationalPath = (EducationalPath) entry.get(REPEATER_EDUCATIONAL_PATH_ATTRIBUTE_NAME); 177 if (commonEntry || educationalPath != null && _rightManager.currentUserHasRight(_rightId, new ContextualizedContent(content, educationalPath)) != RightResult.RIGHT_ALLOW) 178 { 179 // user is not allowed to update its entries 180 updatedValues.add(entry); 181 } 182 } 183 } 184 185 // Get from client only entries with authorized path and force common attribute to false 186 for (Map<String, Object> entryValues : repeaterValues) 187 { 188 if (entryValues.containsKey(REPEATER_EDUCATIONAL_PATH_ATTRIBUTE_NAME)) 189 { 190 boolean commonEntry = _isCommonEntry(repeaterDefinition, entryValues); 191 Object value = entryValues.get(REPEATER_EDUCATIONAL_PATH_ATTRIBUTE_NAME); 192 value = value instanceof SynchronizableValue synchronizableValue ? synchronizableValue.getLocalValue() : value; 193 194 if (!commonEntry && value instanceof EducationalPath educationalPath) 195 { 196 if (_rightManager.currentUserHasRight(_rightId, new ContextualizedContent(content, educationalPath)) == RightResult.RIGHT_ALLOW) 197 { 198 if (repeaterDefinition.hasModelItem(REPEATER_COMMON_ATTRIBUTE_NAME)) 199 { 200 entryValues.put(REPEATER_COMMON_ATTRIBUTE_NAME, false); // Force common to false (required to pass disable condition) 201 } 202 updatedValues.add(entryValues); 203 } 204 } 205 } 206 } 207 208 return updatedValues; 209 } 210 211 private boolean _isCommonEntry(RepeaterDefinition repeaterDefinition, Map<String, Object> entryValues) 212 { 213 boolean commonEntry = repeaterDefinition.hasModelItem(REPEATER_COMMON_ATTRIBUTE_NAME); // default true if repeater supports common entries 214 if (entryValues.containsKey(REPEATER_COMMON_ATTRIBUTE_NAME)) 215 { 216 Object commonValue = entryValues.get(REPEATER_COMMON_ATTRIBUTE_NAME); 217 commonValue = commonValue instanceof SynchronizableValue synchronizableValue ? synchronizableValue.getLocalValue() : commonValue; 218 if (commonValue != null) 219 { 220 commonEntry = (boolean) commonValue; 221 } 222 } 223 return commonEntry; 224 } 225 226 private List<Map<String, Object>> _getRepeaterEntries(Content content, RepeaterDefinition repeaterDefinition) 227 { 228 List<String> itemPaths = new ArrayList<>(); 229 itemPaths.add(repeaterDefinition.getPath()); 230 231 // Add items needed to evaluate disabled conditions 232 itemPaths.addAll(_getDisabledConditionPaths(repeaterDefinition)); 233 234 View view = View.of(content.getModel(), itemPaths.toArray(String[]::new)); 235 236 Map<String, Object> dataToMap = content.dataToMap(view, DataContext.newInstance().withDisabledValues(true).withEmptyValues(false)); 237 238 return _getRepeaterEntries(dataToMap, repeaterDefinition.getPath()); 239 } 240 241 @SuppressWarnings("unchecked") 242 private List<Map<String, Object>> _getRepeaterEntries(Map<String, Object> data, String path) 243 { 244 String[] pathSegments = StringUtils.split(path, ModelItem.ITEM_PATH_SEPARATOR); 245 if (pathSegments.length == 1) 246 { 247 return (List<Map<String, Object>>) data.get(pathSegments[0]); 248 } 249 else 250 { 251 Map<String, Object> subData = (Map<String, Object>) data.get(pathSegments[0]); 252 String remainPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 253 return _getRepeaterEntries(subData, remainPath); 254 } 255 } 256 257 private Set<String> _getDisabledConditionPaths(ModelItem modelItem) 258 { 259 DisableConditions<? extends DisableCondition> disableConditions = modelItem.getDisableConditions(); 260 if (disableConditions != null) 261 { 262 Set<String> disableConditionPaths = disableConditions.getConditions() 263 .stream() 264 .map(c -> ModelHelper.getDisableConditionAbsolutePath(c, modelItem.getPath())) 265 .collect(Collectors.toSet()); 266 267 if (modelItem instanceof ModelItemGroup group) 268 { 269 for (ModelItem childModelItem : group.getChildren()) 270 { 271 disableConditionPaths.addAll(_getDisabledConditionPaths(childModelItem)); 272 } 273 } 274 275 return disableConditionPaths; 276 } 277 return Set.of(); 278 } 279}