001/* 002 * Copyright 2020 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.web.parameters; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang.StringUtils; 029 030import org.ametys.cms.data.holder.DataHolderDisableConditionsEvaluator; 031import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject; 032import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 033import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 034import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 035import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 036import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater; 037import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 038import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 039import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 040import org.ametys.plugins.repository.model.RepeaterDefinition; 041import org.ametys.plugins.repository.model.RepositoryDataContext; 042import org.ametys.runtime.i18n.I18nizableText; 043import org.ametys.runtime.model.ElementDefinition; 044import org.ametys.runtime.model.ModelHelper; 045import org.ametys.runtime.model.ModelItem; 046import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator; 047import org.ametys.runtime.model.type.DataContext; 048import org.ametys.runtime.model.type.ElementType; 049import org.ametys.runtime.parameter.ValidationResult; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051 052/** 053 * Manager to handle parameters 054 */ 055public class ParametersManager extends AbstractLogEnabled implements Component, Serviceable 056{ 057 /** Avalon Role */ 058 public static final String ROLE = ParametersManager.class.getName(); 059 060 /** Constant for untouched binary metadata. */ 061 protected static final String _PARAM_UNTOUCHED_BINARY = "untouched"; 062 /** Prefix for HTML form elements. */ 063 protected static final String _PARAM_METADATA_PREFIX = "_"; 064 065 /** The disable conditions evaluator */ 066 protected DisableConditionsEvaluator<ModelAwareDataHolder> _disableConditionsEvaluator; 067 068 @SuppressWarnings("unchecked") 069 public void service(ServiceManager manager) throws ServiceException 070 { 071 _disableConditionsEvaluator = (DisableConditionsEvaluator<ModelAwareDataHolder>) manager.lookup(DataHolderDisableConditionsEvaluator.ROLE); 072 } 073 074 /** 075 * Set parameters values to the data holder for each model items 076 * @param dataHolder the data holder 077 * @param modelItems the list of model items 078 * @param rawValues the values 079 * @return the map of possible errors 080 */ 081 public Map<String, List<I18nizableText>> setParameterValues(ModifiableModelAwareDataHolder dataHolder, Collection<? extends ModelItem> modelItems, Map<String, Object> rawValues) 082 { 083 // FIXME CMS-10275 084 Map<String, Object> values = _parseValues(modelItems, rawValues, StringUtils.EMPTY); 085 return _setParameterValues(modelItems, values, values, dataHolder, StringUtils.EMPTY); 086 } 087 088 /** 089 * Parses the given raw values 090 * @param modelItems the model items corresponding to the given values 091 * @param rawValues the raw values 092 * @param prefix the current prefix for data to parse 093 * @return the parsed values 094 */ 095 protected Map<String, Object> _parseValues(Collection<? extends ModelItem> modelItems, Map<String, Object> rawValues, String prefix) 096 { 097 Map<String, Object> values = new HashMap<>(); 098 099 for (ModelItem modelItem : modelItems) 100 { 101 String name = modelItem.getName(); 102 String dataPath = prefix + name; 103 104 if (modelItem instanceof ElementDefinition definition) 105 { 106 ElementType type = definition.getType(); 107 108 Object rawValue = rawValues.get(dataPath); 109 Object typedValue = type.fromJSONForClient(rawValue, DataContext.newInstance().withDataPath(dataPath)); 110 values.put(name, typedValue); 111 } 112 else if (modelItem instanceof RepeaterDefinition definition) 113 { 114 int size = (int) rawValues.get(_PARAM_METADATA_PREFIX + dataPath + "/size"); 115 116 List<Map<String, Object>> entries = new ArrayList<>(); 117 Map<Integer, Integer> mapping = new HashMap<>(); 118 for (int i = 1; i <= size; i++) 119 { 120 String updatedPrefix = dataPath + "[" + i + "]" + ModelItem.ITEM_PATH_SEPARATOR; 121 int previousPosition = (int) rawValues.get(_PARAM_METADATA_PREFIX + dataPath + "[" + i + "]/previous-position"); 122 if (previousPosition > 0) 123 { 124 mapping.put(previousPosition, i); 125 } 126 127 entries.add(_parseValues(definition.getModelItems(), rawValues, updatedPrefix)); 128 } 129 130 values.put(name, SynchronizableRepeater.replaceAll(entries, mapping)); 131 } 132 } 133 134 return values; 135 } 136 137 /** 138 * Set parameters values of the model items 139 * @param modelItems the model items 140 * @param currentValues the values to set for the current model items 141 * @param allValues the value all values to set 142 * @param dataHolder the data holder 143 * @param prefix the prefix to get the parameter values 144 * @return the map of possible errors 145 */ 146 protected Map<String, List<I18nizableText>> _setParameterValues(Collection<? extends ModelItem> modelItems, Map<String, Object> currentValues, Map<String, Object> allValues, ModifiableModelAwareDataHolder dataHolder, String prefix) 147 { 148 Map<String, List<I18nizableText>> allErrors = new HashMap<>(); 149 150 for (ModelItem modelItem : modelItems) 151 { 152 String name = modelItem.getName(); 153 String dataPath = prefix + name; 154 boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(modelItem, allValues); 155 boolean isDisabled = _disableConditionsEvaluator.evaluateDisableConditions(modelItem, dataPath, allValues); 156 157 if (isGroupSwitchOn && !isDisabled) 158 { 159 if (modelItem instanceof ElementDefinition definition) 160 { 161 Object value = currentValues.get(name); 162 163 // TODO RUNTIME-2897: call the validateValue without boolean when multiple values are managed in enumerators 164 ValidationResult validationResult = ModelHelper.validateValue(definition, value, false); 165 if (validationResult.hasErrors()) 166 { 167 allErrors.put(dataPath, validationResult.getErrors()); 168 } 169 170 String typeId = definition.getType().getId(); 171 boolean isUntouched = _PARAM_UNTOUCHED_BINARY.equals(value) 172 || value != null && value instanceof UntouchedValue 173 /* keeps the value of an empty password field */ 174 || value == null && org.ametys.runtime.model.type.ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(typeId); 175 if (!isUntouched && !allErrors.containsKey(dataPath)) 176 { 177 dataHolder.setValue(name, value); 178 } 179 } 180 else if (modelItem instanceof RepeaterDefinition definition) 181 { 182 ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(name, true); 183 SynchronizableRepeater repeaterValues = (SynchronizableRepeater) currentValues.get(name); 184 185 // First move the entries according to the given previous positions 186 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 187 repeater.moveEntries(repeaterValues.getPositionsMapping(), entriesValues.size()); 188 189 // Then save the entries' parameter values 190 for (ModifiableModelAwareRepeaterEntry entry : repeater.getEntries()) 191 { 192 int entryIndex = entry.getPosition() - 1; 193 Map<String, Object> entryValues = entriesValues.get(entryIndex); 194 String newPrefix = dataPath + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR; 195 allErrors.putAll(_setParameterValues(definition.getChildren(), entryValues, allValues, entry, newPrefix)); 196 } 197 } 198 } 199 } 200 201 return allErrors; 202 } 203 204 /** 205 * Get the parameters values 206 * @param items the list of model item 207 * @param dataHolder the data holder 208 * @param prefix prefix to get the parameter values 209 * @return the parameters values 210 */ 211 public Map<String, Object> getParametersValues(Collection<? extends ModelItem> items, ModelAwareDataHolder dataHolder, String prefix) 212 { 213 Map<String, Object> values = new HashMap<>(); 214 215 for (ModelItem item : items) 216 { 217 _addParameterValues(item, dataHolder, prefix, values); 218 } 219 220 return values; 221 } 222 223 /** 224 * Add the parameter values to all values 225 * @param item the model item 226 * @param dataHolder the data holder 227 * @param prefix prefix to get the parameter values 228 * @param values all the values 229 */ 230 protected void _addParameterValues(ModelItem item, ModelAwareDataHolder dataHolder, String prefix, Map<String, Object> values) 231 { 232 try 233 { 234 if (item instanceof ElementDefinition) 235 { 236 String dataPath = prefix + item.getName(); 237 // Put empty values in the values map in order make the difference between empty and not present values 238 if (dataHolder.hasValueOrEmpty(item.getName())) 239 { 240 ElementType type = ((ElementDefinition) item).getType(); 241 Object value = dataHolder.getValue(item.getName()); 242 243 RepositoryDataContext context = RepositoryDataContext.newInstance() 244 .withDataPath(dataPath); 245 if (dataHolder instanceof DataAwareAmetysObject ao) 246 { 247 context.withObject(ao); 248 } 249 250 values.put(dataPath, type.valueToJSONForEdition(value, context)); 251 } 252 } 253 else if (item instanceof RepeaterDefinition) 254 { 255 if (dataHolder.hasValue(item.getName())) 256 { 257 ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName()); 258 for (ModelAwareRepeaterEntry entry: repeater.getEntries()) 259 { 260 String newPrefix = prefix + item.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR; 261 values.putAll(getParametersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix)); 262 } 263 values.put(prefix + item.getName(), new ArrayList<>()); 264 } 265 } 266 } 267 catch (Exception e) 268 { 269 getLogger().error("Can't get values from parameter with name '{}'", item.getName(), e); 270 } 271 } 272 273 /** 274 * Get the repeaters values 275 * @param items the list of model item 276 * @param dataHolder the data holder 277 * @param prefix prefix to get the parameter values 278 * @return the repeaters values 279 */ 280 public List<Map<String, Object>> getRepeatersValues(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix) 281 { 282 List<Map<String, Object>> results = new ArrayList<>(); 283 284 for (ModelItem item : items) 285 { 286 if (item instanceof RepeaterDefinition) 287 { 288 Map<String, Object> result = new HashMap<>(); 289 290 result.put("name", item.getName()); 291 result.put("prefix", prefix); 292 293 if (dataHolder.hasValue(item.getName())) 294 { 295 ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName()); 296 result.put("count", repeater.getSize()); 297 for (ModelAwareRepeaterEntry entry: repeater.getEntries()) 298 { 299 StringBuilder newPrefix = new StringBuilder(); 300 newPrefix.append(prefix); 301 newPrefix.append(item.getName()).append("[").append(entry.getPosition()).append("]/"); 302 results.addAll(getRepeatersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix.toString())); 303 } 304 } 305 else 306 { 307 result.put("count", 0); 308 } 309 310 results.add(result); 311 } 312 } 313 314 return results; 315 } 316 317 /** 318 * True if the parameter has a value or a default value 319 * @param parameterPath the parameter path 320 * @param dataHolder the data holder 321 * @param defaultValue the default value 322 * @return true if the parameter has a value or a default value 323 */ 324 protected boolean _hasParameterValueOrDefaultValue(String parameterPath, ModelAwareDataHolder dataHolder, Object defaultValue) 325 { 326 if (dataHolder.hasValue(parameterPath)) 327 { 328 return true; 329 } 330 else 331 { 332 if (defaultValue != null) 333 { 334 return defaultValue instanceof String ? StringUtils.isNotEmpty((String) defaultValue) : true; 335 } 336 else 337 { 338 return false; 339 } 340 } 341 } 342 343 /** 344 * Get all parameters which start with prefix and change the name of the attribute removing this prefix 345 * @param parameterValues the parameter values 346 * @param prefix the prefix 347 * @return the map of filtered parameters 348 */ 349 public Map<String, Object> getParametersStartWithPrefix(Map<String, Object> parameterValues, String prefix) 350 { 351 Map<String, Object> newParameterValues = new HashMap<>(); 352 for (String name : parameterValues.keySet()) 353 { 354 if (name.startsWith(prefix)) 355 { 356 newParameterValues.put(StringUtils.substringAfter(name, prefix), parameterValues.get(name)); 357 } 358 } 359 360 return newParameterValues; 361 } 362 363 /** 364 * Add prefix to all parameters 365 * @param parameterValues the parameters values 366 * @param prefix the prefix 367 * @return the map of parameters with its prefix 368 */ 369 public Map<String, Object> addPrefixToParameters(Map<String, Object> parameterValues, String prefix) 370 { 371 Map<String, Object> newParameterValues = new HashMap<>(); 372 for (String name : parameterValues.keySet()) 373 { 374 newParameterValues.put(prefix + name, parameterValues.get(name)); 375 } 376 377 return newParameterValues; 378 } 379}