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