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