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