001/* 002 * Copyright 2018 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.plugins.repository.data.holder.impl; 017 018import java.util.List; 019import java.util.Map; 020import java.util.Optional; 021import java.util.stream.Collectors; 022 023import org.apache.commons.lang3.StringUtils; 024 025import org.ametys.plugins.repository.RepositoryConstants; 026import org.ametys.plugins.repository.data.UnknownDataException; 027import org.ametys.plugins.repository.data.holder.DataHolder; 028import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 029import org.ametys.plugins.repository.data.holder.group.ModelLessComposite; 030import org.ametys.plugins.repository.data.holder.group.ModifiableModelLessComposite; 031import org.ametys.plugins.repository.data.holder.group.impl.DefaultModifiableModelLessComposite; 032import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 033import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 034import org.ametys.plugins.repository.data.type.ModelItemTypeConstants; 035import org.ametys.plugins.repository.data.type.ModelItemTypeExtensionPoint; 036import org.ametys.plugins.repository.data.type.RepositoryElementType; 037import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType; 038import org.ametys.plugins.repository.data.type.RepositoryModelItemType; 039import org.ametys.runtime.model.ModelItem; 040import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 041import org.ametys.runtime.model.exception.BadItemTypeException; 042import org.ametys.runtime.model.exception.NotUniqueTypeException; 043import org.ametys.runtime.model.exception.UnknownTypeException; 044import org.ametys.runtime.model.type.ElementType; 045 046/** 047 * Default implementation for modifiable data holder without model 048 */ 049public class DefaultModifiableModelLessDataHolder extends DefaultModelLessDataHolder implements ModifiableModelLessDataHolder 050{ 051 /** Repository data to use to store data in the repository */ 052 protected ModifiableRepositoryData _modifiableRepositoryData; 053 054 /** Parent of the current {@link DataHolder} */ 055 protected Optional<? extends ModifiableModelLessDataHolder> _modifiableParent; 056 057 /** Root {@link DataHolder} */ 058 protected ModifiableModelLessDataHolder _modifiableRoot; 059 060 /** 061 * Creates a modifiable default model free data holder 062 * @param typeExtensionPoint the extension point to use to get available element types 063 * @param repositoryData the repository data to use 064 */ 065 public DefaultModifiableModelLessDataHolder(ModelItemTypeExtensionPoint typeExtensionPoint, ModifiableRepositoryData repositoryData) 066 { 067 this(typeExtensionPoint, repositoryData, Optional.empty(), Optional.empty()); 068 } 069 070 /** 071 * Creates a modifiable default model free data holder 072 * @param typeExtensionPoint the extension point to use to get available element types 073 * @param parent the parent of the created {@link DataHolder}, can be <code>null</code> if the created {@link DataHolder} is the root {@link DataHolder} 074 * @param root the root {@link DataHolder} 075 * @param repositoryData the repository data to use 076 */ 077 public DefaultModifiableModelLessDataHolder(ModelItemTypeExtensionPoint typeExtensionPoint, ModifiableRepositoryData repositoryData, Optional<? extends ModifiableModelLessDataHolder> parent, Optional<? extends ModifiableModelLessDataHolder> root) 078 { 079 super(typeExtensionPoint, repositoryData, parent, root); 080 _modifiableRepositoryData = repositoryData; 081 _modifiableParent = parent; 082 _modifiableRoot = root.map(ModifiableModelLessDataHolder.class::cast) 083 .or(() -> _modifiableParent.map(ModifiableModelLessDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root 084 .orElse(this); // if no root or parent is specified, the root is the current DataHolder 085 } 086 087 @Override 088 public ModifiableModelLessComposite getComposite(String compositePath) throws IllegalArgumentException, BadItemTypeException 089 { 090 return (ModifiableModelLessComposite) super.getComposite(compositePath); 091 } 092 093 @Override 094 protected ModelLessComposite _getComposite(String name) throws BadItemTypeException 095 { 096 return _getComposite(name, false); 097 } 098 099 public ModifiableModelLessComposite getComposite(String compositePath, boolean createNew) throws IllegalArgumentException, BadItemTypeException 100 { 101 if (!_typeExtensionPoint.hasExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID)) 102 { 103 throw new UnknownTypeException("The composites are not available for the extension point '" + _typeExtensionPoint + "'"); 104 } 105 106 String[] pathSegments = StringUtils.split(compositePath, ModelItem.ITEM_PATH_SEPARATOR); 107 108 if (pathSegments == null || pathSegments.length < 1) 109 { 110 throw new IllegalArgumentException("Unable to retrieve the composite at the given path. This path is empty."); 111 } 112 else if (pathSegments.length == 1) 113 { 114 // Simple path => get the composite 115 return _getComposite(compositePath, createNew); 116 } 117 else 118 { 119 // Path where current part is a data holder 120 ModifiableModelLessDataHolder parent = _getParentValue(this, compositePath); 121 if (parent == null) 122 { 123 return null; 124 } 125 else 126 { 127 String childName = compositePath.substring(compositePath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1); 128 return parent.getComposite(childName, createNew); 129 } 130 } 131 } 132 133 /** 134 * Retrieves the composite with the given name 135 * @param name name of the composite to retrieve 136 * @param createNew <code>true</code> to create the composite if it does not exist, <code>false</code> otherwise 137 * @return the composite 138 * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite 139 */ 140 protected ModifiableModelLessComposite _getComposite(String name, boolean createNew) throws BadItemTypeException 141 { 142 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) _typeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID); 143 RepositoryData compositeRepositoryData = type.read(_modifiableRepositoryData, name); 144 145 if (compositeRepositoryData != null) 146 { 147 return new DefaultModifiableModelLessComposite(_typeExtensionPoint, (ModifiableRepositoryData) compositeRepositoryData, this, _modifiableRoot); 148 } 149 else 150 { 151 if (createNew) 152 { 153 ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name); 154 return new DefaultModifiableModelLessComposite(_typeExtensionPoint, createdRepositoryData, this, _modifiableRoot); 155 } 156 else 157 { 158 return null; 159 } 160 } 161 } 162 163 @SuppressWarnings("unchecked") 164 public boolean synchronizeValues(Map<String, Object> values) throws UnknownTypeException, NotUniqueTypeException 165 { 166 boolean hasChanged = false; 167 168 for (Map.Entry<String, Object> entry : values.entrySet()) 169 { 170 String dataName = entry.getKey(); 171 Object value = entry.getValue(); 172 173 if (value instanceof Map) 174 { 175 ModifiableModelLessComposite composite = getComposite(dataName, true); 176 hasChanged = composite.synchronizeValues((Map<String, Object>) value) || hasChanged; 177 } 178 else 179 { 180 if (_repositoryData.hasValue(dataName + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)) 181 { 182 // The old value is set as empty via an internal boolean property 183 if (value != null) 184 { 185 // the new value is not empty 186 hasChanged = true; 187 setValue(dataName, value); 188 } 189 } 190 else if (hasValueOrEmpty(dataName)) 191 { 192 ElementType type = (ElementType) getType(dataName); 193 Object oldValue = getValue(dataName); 194 195 if (type.compareValues(value, oldValue).count() > 0) 196 { 197 hasChanged = true; 198 setValue(dataName, value); 199 } 200 } 201 else 202 { 203 // There is no old value. If the given value is null, there is no way to determine the type of the value to set 204 if (value != null) 205 { 206 hasChanged = true; 207 setValue(dataName, value); 208 } 209 } 210 } 211 } 212 213 return hasChanged; 214 } 215 216 public void setValue(String dataPath, Object value) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException, UnknownDataException 217 { 218 List<RepositoryModelItemType> compatibleTypes = _typeExtensionPoint.getExtensionsIds().stream() 219 .map(id -> _typeExtensionPoint.getExtension(id)) 220 .filter(RepositoryElementType.class::isInstance) 221 .map(RepositoryElementType.class::cast) 222 .filter(type -> type.isCompatible(value)) 223 .collect(Collectors.toList()); 224 225 if (compatibleTypes.isEmpty()) 226 { 227 // The type has not been found, throw an UnknownTypeException 228 String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", "); 229 throw new UnknownTypeException("Unable to retrieve the type of the data '" + dataPath + "'. No compatible type have been found. Available types are: '" + availableTypes + "'."); 230 } 231 else if (compatibleTypes.size() > 1) 232 { 233 // Many types have been found, throw an UnknownTypeException 234 List<String> compatibleTypesIds = compatibleTypes.stream().map(type -> type.getId()).collect(Collectors.toList()); 235 throw new NotUniqueTypeException("Unable to retrieve the type of the data '" + dataPath + "'. Many compatible types have been found, there is no way to determine which one is the good one. Compatible types found are: " + StringUtils.join(compatibleTypesIds, ", ")); 236 } 237 else 238 { 239 setValue(dataPath, value, compatibleTypes.get(0).getId()); 240 } 241 } 242 243 public void setValue(String dataPath, Object value, String dataTypeId) throws IllegalArgumentException, BadItemTypeException, UnknownDataException 244 { 245 if (!_typeExtensionPoint.hasExtension(dataTypeId)) 246 { 247 String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", "); 248 throw new UnknownTypeException("The type '" + dataTypeId + "' is not available for the extension point '" + _typeExtensionPoint + "'. Available types are: '" + availableTypes + "'."); 249 } 250 251 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 252 253 if (pathSegments == null || pathSegments.length < 1) 254 { 255 throw new IllegalArgumentException("Unable to set a value at the given path. This path is empty."); 256 } 257 else if (pathSegments.length == 1) 258 { 259 // Simple path => set the value 260 RepositoryModelItemType type = (RepositoryModelItemType) _typeExtensionPoint.getExtension(dataTypeId); 261 if (type instanceof RepositoryElementType) 262 { 263 ((RepositoryElementType) type).write(_modifiableRepositoryData, dataPath, value); 264 } 265 else 266 { 267 throw new BadItemTypeException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "' in the repository because it is a group item."); 268 } 269 } 270 else 271 { 272 // Path where current part is a data holder 273 ModifiableModelLessDataHolder parent = _getParentValue(this, dataPath); 274 if (parent == null) 275 { 276 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 277 throw new UnknownDataException("The data at path '" + parentPath + "' does not exist. It is not possible to get the data at path '" + dataPath + "'."); 278 } 279 else 280 { 281 String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1); 282 parent.setValue(childName, value, dataTypeId); 283 } 284 } 285 } 286 287 public void removeValue(String dataPath) throws IllegalArgumentException, BadItemTypeException 288 { 289 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 290 291 if (pathSegments == null || pathSegments.length < 1) 292 { 293 throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty."); 294 } 295 else if (pathSegments.length == 1) 296 { 297 if (_modifiableRepositoryData.hasValue(dataPath)) 298 { 299 _modifiableRepositoryData.removeValue(dataPath); 300 } 301 302 if (_modifiableRepositoryData.hasValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)) 303 { 304 _modifiableRepositoryData.removeValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 305 } 306 307 } 308 else 309 { 310 // Path where current part is a data holder 311 ModifiableModelLessDataHolder parent = _getParentValue(this, dataPath); 312 if (parent != null) 313 { 314 String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1); 315 parent.removeValue(childName); 316 } 317 } 318 } 319 320 /** 321 * Retrieves the modifiable data holder, last parent segment of the given data path 322 * Example : call this method with a path like 'my-composite1/my-composite2/my-data' will retrieve the composite 'my-composite2' in the composite 'my-composite1' 323 * @param modifiableDataHolder the modifiable data holder 324 * @param dataPath the data path 325 * @return the parent data holder 326 * @throws BadDataPathCardinalityException if the value of a part of the data path is multiple. Only the last part can be multiple 327 * @throws BadItemTypeException if the value at the given data path is not a composite 328 */ 329 protected static ModifiableModelLessDataHolder _getParentValue(ModifiableModelLessDataHolder modifiableDataHolder, String dataPath) throws BadDataPathCardinalityException, BadItemTypeException 330 { 331 return (ModifiableModelLessDataHolder) DefaultModelLessDataHolder._getParentValue(modifiableDataHolder, dataPath); 332 } 333 334 @Override 335 public ModifiableRepositoryData getRepositoryData() 336 { 337 return _modifiableRepositoryData; 338 } 339 340 @Override 341 public Optional<? extends ModifiableModelLessDataHolder> getParentDataHolder() 342 { 343 return _modifiableParent; 344 } 345 346 @Override 347 public ModifiableModelLessDataHolder getRootDataHolder() 348 { 349 return _modifiableRoot; 350 } 351}