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.cms.data.holder.impl; 017 018import java.lang.reflect.Array; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025 026import org.apache.commons.lang3.StringUtils; 027import org.apache.commons.lang3.tuple.Pair; 028 029import org.ametys.cms.data.holder.ModifiableIndexableDataHolder; 030import org.ametys.cms.data.holder.group.ModifiableIndexableComposite; 031import org.ametys.cms.data.holder.group.ModifiableIndexableRepeater; 032import org.ametys.cms.data.holder.group.impl.DefaultModifiableModelAwareComposite; 033import org.ametys.cms.data.holder.group.impl.DefaultModifiableModelAwareRepeater; 034import org.ametys.core.util.DateUtils; 035import org.ametys.plugins.repository.RepositoryConstants; 036import org.ametys.plugins.repository.data.DataComment; 037import org.ametys.plugins.repository.data.UnknownDataException; 038import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 039import org.ametys.plugins.repository.data.holder.DataHolder; 040import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 041import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareComposite; 042import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater; 043import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 044import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 045import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 046import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 047import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 048import org.ametys.plugins.repository.data.holder.values.SynchronizationResult; 049import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 050import org.ametys.plugins.repository.data.holder.values.ValueContext; 051import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 052import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 053import org.ametys.plugins.repository.data.type.RepositoryElementType; 054import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType; 055import org.ametys.plugins.repository.data.type.RepositoryModelItemType; 056import org.ametys.plugins.repository.model.CompositeDefinition; 057import org.ametys.plugins.repository.model.RepeaterDefinition; 058import org.ametys.runtime.model.ElementDefinition; 059import org.ametys.runtime.model.ModelHelper; 060import org.ametys.runtime.model.ModelItem; 061import org.ametys.runtime.model.ModelItemContainer; 062import org.ametys.runtime.model.ModelItemGroup; 063import org.ametys.runtime.model.ModelViewItem; 064import org.ametys.runtime.model.ModelViewItemGroup; 065import org.ametys.runtime.model.ViewElement; 066import org.ametys.runtime.model.ViewHelper; 067import org.ametys.runtime.model.ViewItem; 068import org.ametys.runtime.model.ViewItemContainer; 069import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 070import org.ametys.runtime.model.exception.BadItemTypeException; 071import org.ametys.runtime.model.exception.UndefinedItemPathException; 072import org.ametys.runtime.model.type.ElementType; 073 074/** 075 * Default implementation for modifiable data holder with model 076 */ 077public class DefaultModifiableModelAwareDataHolder extends DefaultModelAwareDataHolder implements ModifiableIndexableDataHolder 078{ 079 private static final String __TEMP_SUFFIX = "__temp"; 080 081 /** Repository data to use to store data in the repository */ 082 protected ModifiableRepositoryData _modifiableRepositoryData; 083 084 /** Parent of the current {@link DataHolder} */ 085 protected Optional<? extends ModifiableIndexableDataHolder> _modifiableParent; 086 087 /** Root {@link DataHolder} */ 088 protected ModifiableIndexableDataHolder _modifiableRoot; 089 090 /** 091 * Creates a modifiable default model aware data holder 092 * @param repositoryData the repository data to use 093 * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types. 094 */ 095 public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, ModelItemContainer... itemContainers) 096 { 097 this(repositoryData, Optional.empty(), Optional.empty(), Arrays.asList(itemContainers)); 098 } 099 100 /** 101 * Creates a modifiable default model aware data holder 102 * @param repositoryData the repository data to use 103 * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types. 104 */ 105 public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers) 106 { 107 this(repositoryData, Optional.empty(), Optional.empty(), itemContainers); 108 } 109 110 /** 111 * Creates a modifiable default model aware data holder 112 * @param repositoryData the repository data to use 113 * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 114 * @param root the root {@link DataHolder} 115 * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types. 116 */ 117 public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, Optional<? extends ModifiableIndexableDataHolder> parent, Optional<? extends ModifiableIndexableDataHolder> root, ModelItemContainer... itemContainers) 118 { 119 this(repositoryData, parent, root, Arrays.asList(itemContainers)); 120 } 121 122 123 /** 124 * Creates a modifiable default model aware data holder 125 * @param repositoryData the repository data to use 126 * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 127 * @param root the root {@link DataHolder} 128 * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types. 129 */ 130 public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, Optional<? extends ModifiableIndexableDataHolder> parent, Optional<? extends ModifiableIndexableDataHolder> root, Collection<? extends ModelItemContainer> itemContainers) 131 { 132 super(repositoryData, parent, root, itemContainers); 133 _modifiableRepositoryData = repositoryData; 134 135 _modifiableParent = parent; 136 _modifiableRoot = root.map(ModifiableIndexableDataHolder.class::cast) 137 .or(() -> _modifiableParent.map(ModifiableIndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root 138 .orElse(this); // if no root or parent is specified, the root is the current DataHolder 139 } 140 141 @Override 142 public ModifiableIndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 143 { 144 return (ModifiableIndexableComposite) super.getComposite(compositePath); 145 } 146 147 @Override 148 public ModifiableIndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 149 { 150 return (ModifiableIndexableComposite) super.getLocalComposite(compositePath); 151 } 152 153 @Override 154 public ModifiableIndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 155 { 156 return (ModifiableIndexableComposite) super.getExternalComposite(compositePath); 157 } 158 159 @Override 160 protected ModifiableIndexableComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException 161 { 162 return _getComposite(name, compositeDefinition, false); 163 } 164 165 @Override 166 public ModifiableIndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 167 { 168 return (ModifiableIndexableRepeater) super.getRepeater(repeaterPath); 169 } 170 171 @Override 172 public ModifiableIndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 173 { 174 return (ModifiableIndexableRepeater) super.getLocalRepeater(repeaterPath); 175 } 176 177 @Override 178 public ModifiableIndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 179 { 180 return (ModifiableIndexableRepeater) super.getExternalRepeater(repeaterPath); 181 } 182 183 @Override 184 protected ModifiableIndexableRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException 185 { 186 return _getRepeater(name, repeaterDefinition, false); 187 } 188 189 public ModifiableIndexableComposite getComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 190 { 191 return _getComposite(compositePath, createNew, Optional.empty()); 192 } 193 194 public ModifiableIndexableComposite getLocalComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 195 { 196 return _getComposite(compositePath, createNew, Optional.of(ExternalizableDataStatus.LOCAL)); 197 } 198 199 public ModifiableIndexableComposite getExternalComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 200 { 201 return _getComposite(compositePath, createNew, Optional.of(ExternalizableDataStatus.EXTERNAL)); 202 } 203 204 private ModifiableIndexableComposite _getComposite(String compositePath, boolean createNew, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 205 { 206 String[] pathSegments = StringUtils.split(compositePath, ModelItem.ITEM_PATH_SEPARATOR); 207 208 if (pathSegments == null || pathSegments.length < 1) 209 { 210 throw new IllegalArgumentException("Unable to retrieve the composite at the given path. This path is empty."); 211 } 212 else if (pathSegments.length == 1) 213 { 214 // Simple path => get composite value 215 ModelItem modelItem = getDefinition(compositePath); 216 if (modelItem instanceof CompositeDefinition) 217 { 218 String compositeName = _getFinalDataName(compositePath, status); 219 return _getComposite(compositeName, (CompositeDefinition) modelItem, createNew); 220 } 221 else 222 { 223 throw new BadItemTypeException("The data at path '" + compositePath + "' is not a composite."); 224 } 225 } 226 else 227 { 228 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 229 230 // Multiple items are allowed only at the last segment of the data path 231 if (isMultiple(parentPath)) 232 { 233 throw new BadDataPathCardinalityException("Unable to retrieve the composite at path '" + compositePath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 234 } 235 236 Object parentValue = getValue(parentPath); 237 String childName = pathSegments[pathSegments.length - 1]; 238 if (parentValue != null && parentValue instanceof ModifiableIndexableDataHolder parent) 239 { 240 return status.isPresent() 241 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 242 ? getExternalComposite(compositePath, createNew) 243 : getLocalComposite(compositePath, createNew) 244 : parent.getComposite(childName, createNew); 245 } 246 else 247 { 248 throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a data holder. It can not contain the data named '" + childName + "'."); 249 } 250 } 251 } 252 253 /** 254 * Retrieves the composite with the given name 255 * @param name name of the composite to retrieve 256 * @param compositeDefinition the definition of the composite to retrieve 257 * @param createNew <code>true</code> to create the repeater if it does not exist, <code>false</code> otherwise 258 * @return the composite 259 * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite 260 */ 261 protected ModifiableIndexableComposite _getComposite(String name, CompositeDefinition compositeDefinition, boolean createNew) throws BadItemTypeException 262 { 263 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType(); 264 RepositoryData compositeRepositoryData = type.read(_modifiableRepositoryData, name); 265 266 if (compositeRepositoryData != null) 267 { 268 return new DefaultModifiableModelAwareComposite((ModifiableRepositoryData) compositeRepositoryData, this, _modifiableRoot, compositeDefinition); 269 } 270 else 271 { 272 if (createNew) 273 { 274 ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name); 275 return new DefaultModifiableModelAwareComposite(createdRepositoryData, this, _modifiableRoot, compositeDefinition); 276 } 277 else 278 { 279 return null; 280 } 281 } 282 } 283 284 public ModifiableIndexableRepeater getRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 285 { 286 return _getRepeater(repeaterPath, createNew, Optional.empty()); 287 } 288 289 public ModifiableIndexableRepeater getLocalRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 290 { 291 return _getRepeater(repeaterPath, createNew, Optional.of(ExternalizableDataStatus.LOCAL)); 292 } 293 294 public ModifiableIndexableRepeater getExternalRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 295 { 296 return _getRepeater(repeaterPath, createNew, Optional.of(ExternalizableDataStatus.EXTERNAL)); 297 } 298 299 private ModifiableIndexableRepeater _getRepeater(String repeaterPath, boolean createNew, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 300 { 301 String[] pathSegments = StringUtils.split(repeaterPath, ModelItem.ITEM_PATH_SEPARATOR); 302 303 if (pathSegments == null || pathSegments.length < 1) 304 { 305 throw new IllegalArgumentException("Unable to retrieve the repeater at the given path. This path is empty."); 306 } 307 else if (pathSegments.length == 1) 308 { 309 // Simple path => get composite value 310 ModelItem modelItem = getDefinition(repeaterPath); 311 if (modelItem instanceof RepeaterDefinition) 312 { 313 String repeaterName = _getFinalDataName(repeaterPath, status); 314 return _getRepeater(repeaterName, (RepeaterDefinition) modelItem, createNew); 315 } 316 else 317 { 318 throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater."); 319 } 320 } 321 else 322 { 323 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 324 325 // Multiple items are allowed only at the last segment of the data path 326 if (isMultiple(parentPath)) 327 { 328 throw new BadDataPathCardinalityException("Unable to retrieve the repeater at path '" + repeaterPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 329 } 330 331 Object parentValue = getValue(parentPath); 332 String childName = pathSegments[pathSegments.length - 1]; 333 if (parentValue != null && parentValue instanceof ModifiableIndexableDataHolder parent) 334 { 335 return status.isPresent() 336 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 337 ? parent.getExternalRepeater(repeaterPath, createNew) 338 : parent.getExternalRepeater(repeaterPath, createNew) 339 : parent.getRepeater(childName, createNew); 340 } 341 else 342 { 343 throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'."); 344 } 345 } 346 } 347 348 /** 349 * Retrieves the repeater with the given name 350 * @param name name of the repeater to retrieve 351 * @param repeaterDefinition the definition of the repeater to retrieve 352 * @param createNew <code>true</code> to create the repeater if it does not exist, <code>false</code> otherwise 353 * @return the repeater 354 * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater 355 */ 356 protected ModifiableIndexableRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition, boolean createNew) throws BadItemTypeException 357 { 358 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType(); 359 RepositoryData repeaterRepositoryData = type.read(_modifiableRepositoryData, name); 360 361 if (repeaterRepositoryData != null) 362 { 363 return new DefaultModifiableModelAwareRepeater((ModifiableRepositoryData) repeaterRepositoryData, this, _modifiableRoot, repeaterDefinition); 364 } 365 else 366 { 367 if (createNew) 368 { 369 ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name); 370 return new DefaultModifiableModelAwareRepeater(createdRepositoryData, this, _modifiableRoot, repeaterDefinition); 371 } 372 else 373 { 374 return null; 375 } 376 } 377 } 378 379 @Override 380 protected Class _getRepeaterEntryClass() 381 { 382 return ModifiableModelAwareRepeaterEntry.class; 383 } 384 385 @Override 386 protected Class _getRepeaterClass() 387 { 388 return ModifiableModelAwareRepeater.class; 389 } 390 391 @Override 392 protected Class _getCompositeClass() 393 { 394 return ModifiableModelAwareComposite.class; 395 } 396 397 public <T extends SynchronizationResult> T synchronizeValues(Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException 398 { 399 return synchronizeValues(values, _createSynchronizationContextInstance()); 400 } 401 402 public <T extends SynchronizationResult> T synchronizeValues(Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 403 { 404 ViewItemContainer viewItemContainer = ViewHelper.createEmptyViewItemAccessor(_itemContainers); 405 _fillViewItemContainerFromValues(values, viewItemContainer, _itemContainers); 406 return synchronizeValues(viewItemContainer, values, context); 407 } 408 409 private void _fillViewItemContainerFromValues(Map<String, Object> values, ViewItemContainer viewItemContainer, ModelItemContainer modelItemContainer) 410 { 411 _fillViewItemContainerFromValues(values, viewItemContainer, List.of(modelItemContainer)); 412 } 413 414 @SuppressWarnings("unchecked") 415 private void _fillViewItemContainerFromValues(Map<String, Object> values, ViewItemContainer viewItemContainer, Collection<? extends ModelItemContainer> modelItemContainers) 416 { 417 for (String dataName : values.keySet()) 418 { 419 ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers); 420 if (modelItem instanceof ModelItemGroup) 421 { 422 Object value = values.get(dataName); 423 if (modelItem instanceof RepeaterDefinition) 424 { 425 if (value instanceof List) 426 { 427 if (!((List<Map<String, Object>>) value).isEmpty()) 428 { 429 Map<String, Object> newValues = ((List<Map<String, Object>>) value).get(0); 430 ModelViewItemGroup modelViewItemGroup = new ModelViewItemGroup(); 431 _fillViewItemContainerFromValues(newValues, modelViewItemGroup, (ModelItemGroup) modelItem); 432 modelViewItemGroup.setDefinition((ModelItemGroup) modelItem); 433 viewItemContainer.addViewItem(modelViewItemGroup); 434 } 435 } 436 else 437 { 438 throw new BadItemTypeException("Unable to synchronize the repeater named '" + dataName + "': the given value should be a list containing its entries"); 439 } 440 } 441 else 442 { 443 if (value instanceof Map) 444 { 445 Map<String, Object> newValues = (Map<String, Object>) value; 446 ModelViewItemGroup modelViewItemGroup = new ModelViewItemGroup(); 447 _fillViewItemContainerFromValues(newValues, modelViewItemGroup, (ModelItemGroup) modelItem); 448 modelViewItemGroup.setDefinition((ModelItemGroup) modelItem); 449 viewItemContainer.addViewItem(modelViewItemGroup); 450 } 451 else 452 { 453 throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items"); 454 } 455 } 456 } 457 else 458 { 459 ViewElement viewElement = new ViewElement(); 460 viewElement.setDefinition((ElementDefinition) modelItem); 461 viewItemContainer.addViewItem(viewElement); 462 } 463 464 } 465 } 466 467 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException 468 { 469 return synchronizeValues(viewItemContainer, values, _createSynchronizationContextInstance()); 470 } 471 472 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 473 { 474 return _synchronizeValues(viewItemContainer, values, context); 475 } 476 477 private <T extends SynchronizationResult> T _synchronizeValues(ViewItemContainer viewItemContainer, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 478 { 479 T result = _createSetValueResultInstance(); 480 481 for (ViewItem viewItem : viewItemContainer.getViewItems()) 482 { 483 SynchronizationResult itemResult = null; 484 if (viewItem instanceof ModelViewItem) 485 { 486 if (viewItem instanceof ModelViewItemGroup) 487 { 488 itemResult = _synchronizeGroup((ModelViewItemGroup) viewItem, values, context); 489 } 490 else if (viewItem instanceof ViewElement) 491 { 492 itemResult = _synchronizeElement((ViewElement) viewItem, values, context); 493 } 494 } 495 else if (viewItem instanceof ViewItemContainer) 496 { 497 itemResult = _synchronizeValues((ViewItemContainer) viewItem, values, context); 498 } 499 500 result.aggregateResult(itemResult); 501 } 502 503 return result; 504 } 505 506 @SuppressWarnings("unchecked") 507 private <T extends SynchronizationResult> T _synchronizeGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) 508 { 509 ModelItem modelItem = modelViewItemGroup.getDefinition(); 510 String dataName = modelItem.getName(); 511 Object value = values.get(dataName); 512 T result = null; 513 514 if (value == null) 515 { 516 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 517 result = _createSetValueResultInstance(); 518 if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext)) 519 { 520 _removeValueForSynchronize(dataName, valueContext); 521 result.setHasChanged(true); 522 } 523 } 524 else if (modelItem instanceof RepeaterDefinition) 525 { 526 if (value instanceof SynchronizableRepeater || value instanceof List) 527 { 528 ModifiableModelAwareRepeater repeater = getRepeater(dataName, true); 529 SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null); 530 result = repeater.synchronizeValues(modelViewItemGroup, repeaterValues, synchronizationContext); 531 } 532 else if (value instanceof UntouchedValue) 533 { 534 result = _createSetValueResultInstance(); 535 } 536 else 537 { 538 throw new BadItemTypeException("Unable to synchronize the repeater named '" + dataName + "': the given value should be a list containing its entries"); 539 } 540 } 541 else 542 { 543 if (value instanceof Map) 544 { 545 ModifiableModelAwareComposite composite = getComposite(dataName, true); 546 result = composite.synchronizeValues(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext); 547 } 548 else 549 { 550 throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items"); 551 } 552 } 553 554 if (!DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), modelItem)) 555 { 556 _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataName); 557 } 558 559 return result; 560 } 561 562 private <T extends SynchronizationResult> T _synchronizeElement(ViewElement viewElement, Map<String, Object> values, SynchronizationContext synchronizationContext) 563 { 564 ElementDefinition definition = viewElement.getDefinition(); 565 String dataName = definition.getName(); 566 Object valueFromMap = values.get(dataName); 567 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 568 SynchronizableValue syncValue = valueFromMap instanceof SynchronizableValue ? (SynchronizableValue) valueFromMap : new SynchronizableValue(valueFromMap, valueContext.getStatus().orElse(null)); 569 Object value = syncValue.getValue(valueContext.getStatus()); 570 T result = null; 571 572 if (!(value instanceof UntouchedValue)) 573 { 574 575 Object defaultValue = definition.getDefaultValue(); 576 if (value == null && synchronizationContext.useDefaultFromModel() && defaultValue != null) 577 { 578 result = _setValueForSynchronize(dataName, new SynchronizableValue(defaultValue, valueContext.getStatus().orElse(null)), valueContext); 579 } 580 else 581 { 582 if (values.containsKey(dataName)) 583 { 584 result = _setValueForSynchronize(dataName, syncValue, valueContext); 585 } 586 else 587 { 588 result = _removeValueForSynchronize(dataName, valueContext); 589 } 590 } 591 } 592 else 593 { 594 result = _createSetValueResultInstance(); 595 } 596 597 if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), definition)) 598 { 599 boolean statusHasChanged = _updateStatusForSynchronize(dataName, syncValue, valueContext, synchronizationContext.forceStatusIfNotPresent(), values.containsKey(dataName)); 600 if (statusHasChanged) 601 { 602 result.setHasChanged(true); 603 } 604 } 605 else 606 { 607 _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataName); 608 } 609 610 List<DataComment> newComments = syncValue.getComments(); 611 List<DataComment> oldComments = hasComments(dataName) ? getComments(dataName) : List.of(); 612 if (newComments != null && !newComments.equals(oldComments)) 613 { 614 result.setHasChanged(true); 615 setComments(dataName, newComments); 616 } 617 618 return result; 619 } 620 621 /** 622 * Updates the status of the data with the given name 623 * @param dataName name of the data 624 * @param value the value 625 * @param context context of the data 626 * @param forceStatus <code>true</code> to force the status if it is not present, <code>false</code> otherwise 627 * @param doValuesContainData <code>true</code> if the values contain the data, <code>false</code> otherwise 628 * @return <code>true</code> if the status has changed, <code>false</code> otherwise 629 */ 630 protected boolean _updateStatusForSynchronize(String dataName, SynchronizableValue value, ValueContext context, boolean forceStatus, boolean doValuesContainData) 631 { 632 boolean hasChanged = false; 633 634 ExternalizableDataStatus oldStatus = null; 635 if (_repositoryData.hasValue(dataName + STATUS_SUFFIX)) 636 { 637 String status = _repositoryData.getString(dataName + STATUS_SUFFIX); 638 oldStatus = ExternalizableDataStatus.valueOf(status.toUpperCase()); 639 } 640 ExternalizableDataStatus newStatus = value.getExternalizableStatus(); 641 Optional<ExternalizableDataStatus> contextStatus = context.getStatus(); 642 if (forceStatus && oldStatus == null && newStatus == null && doValuesContainData && contextStatus.isPresent()) 643 { 644 setStatus(dataName, contextStatus.get()); 645 hasChanged = true; 646 } 647 else if (newStatus != null && !newStatus.equals(oldStatus)) 648 { 649 setStatus(dataName, newStatus); 650 hasChanged = true; 651 } 652 653 return hasChanged; 654 } 655 656 /** 657 * Sets the value of the data with the given name 658 * @param <T> the type of the {@link SynchronizationResult} 659 * @param dataName name of the data 660 * @param value the value to set. Give <code>null</code> to empty the value. 661 * @param context context of the data to set 662 * @return the {@link SynchronizationResult} 663 * @throws IllegalArgumentException if the given data path is null or empty 664 * @throws UndefinedItemPathException if the given data path is not defined by the model 665 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 666 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 667 */ 668 protected <T extends SynchronizationResult> T _setValueForSynchronize(String dataName, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 669 { 670 T result = _createSetValueResultInstance(); 671 boolean hasValue = DataHolderHelper.hasValueOrEmpty(this, dataName, context); 672 673 if (SynchronizableValue.Mode.REMOVE.equals(value.getMode())) 674 { 675 if (hasValue) 676 { 677 if (isMultiple(dataName)) 678 { 679 Object oldValues = DataHolderHelper.getValue(this, dataName, context); 680 Object valuesToRemove = _getValuesInArrayFromSynchronizableValue(value, context); 681 ElementType type = ((ElementDefinition) getDefinition(dataName)).getType(); 682 683 // Remove the given values from the existent ones 684 Object newValues = _removeArrayValues(oldValues, valuesToRemove, type); 685 686 if (Array.getLength(oldValues) > Array.getLength(newValues)) 687 { 688 result.setHasChanged(true); 689 _setValue(dataName, newValues, context); 690 } 691 } 692 else 693 { 694 result.setHasChanged(true); 695 _removeValue(dataName, context); 696 } 697 } 698 } 699 else 700 { 701 if (hasValue) 702 { 703 if (SynchronizableValue.Mode.APPEND.equals(value.getMode()) && isMultiple(dataName)) 704 { 705 Object valuesToAppend = _getValuesInArrayFromSynchronizableValue(value, context); 706 if (Array.getLength(valuesToAppend) > 0) 707 { 708 Object oldValues = DataHolderHelper.getValue(this, dataName, context); 709 ElementType type = ((ElementDefinition) getDefinition(dataName)).getType(); 710 711 // Append the given values to the existent ones 712 Object newValues = _appendArrayValues(oldValues, valuesToAppend, type); 713 714 result.setHasChanged(true); 715 _setValue(dataName, newValues, context); 716 } 717 } 718 else 719 { 720 Object oldValue = DataHolderHelper.getValue(this, dataName, context); 721 ElementType type = ((ElementDefinition) getDefinition(dataName)).getType(); 722 if (type.compareValues(value.getValue(context.getStatus()), oldValue).count() > 0) 723 { 724 // There are differences between old and new value 725 result.setHasChanged(true); 726 _setValue(dataName, value.getValue(context.getStatus()), context); 727 } 728 } 729 } 730 else 731 { 732 // There was no values, set one 733 result.setHasChanged(true); 734 _setValue(dataName, value.getValue(context.getStatus()), context); 735 } 736 } 737 738 return result; 739 } 740 741 private Object _getValuesInArrayFromSynchronizableValue(SynchronizableValue value, ValueContext context) 742 { 743 Object values = value.getValue(context.getStatus()); 744 745 if (!values.getClass().isArray()) 746 { 747 // Create an array to put the single given value 748 Object valueToRemove = values; 749 values = Array.newInstance(valueToRemove.getClass(), 1); 750 Array.set(values, 0, valueToRemove); 751 } 752 753 return values; 754 } 755 756 private Object _removeArrayValues(Object originalValues, Object valuesToRemove, ElementType type) 757 { 758 List<Object> valuesAsList = new ArrayList<>(); 759 760 for (int i = 0; i < Array.getLength(originalValues); i++) 761 { 762 // for each original value 763 Object originalValue = Array.get(originalValues, i); 764 765 boolean keepOldValue = true; 766 for (int j = 0; j < Array.getLength(valuesToRemove); j++) 767 { 768 Object valueToRemove = Array.get(valuesToRemove, j); 769 if (type.compareValues(valueToRemove, originalValue).count() == 0) 770 { 771 // If the original value is corresponding to a value to remove, do not keep it in the final values 772 keepOldValue = false; 773 break; 774 } 775 } 776 777 if (keepOldValue) 778 { 779 valuesAsList.add(originalValue); 780 } 781 } 782 783 Object[] values = (Object[]) Array.newInstance(type.getManagedClass(), valuesAsList.size()); 784 return valuesAsList.toArray(values); 785 } 786 787 private Object _appendArrayValues(Object originalValues, Object valuesToAppend, ElementType type) 788 { 789 // Create a array with the original values with the final size 790 Object[] values = Arrays.copyOf((Object[]) originalValues, Array.getLength(originalValues) + Array.getLength(valuesToAppend)); 791 792 // Append each value from the given array 793 for (int i = 0; i < Array.getLength(valuesToAppend); i++) 794 { 795 Object valueToAppend = type.castValue(Array.get(valuesToAppend, i)); 796 values[i + Array.getLength(originalValues)] = valueToAppend; 797 } 798 799 return values; 800 } 801 802 /** 803 * Removes the stored value of the data with the given name 804 * @param <T> the type of the {@link SynchronizationResult} 805 * @param dataName name of the data 806 * @param context context of the data to remove 807 * @return the {@link SynchronizationResult} 808 * @throws IllegalArgumentException if the given data path is null or empty 809 * @throws UnknownDataException if the value at the given data path does not exist 810 * @throws BadItemTypeException if the value of the parent of the given path is not an item container 811 * @throws UndefinedItemPathException if the given data path is not defined by the model 812 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 813 */ 814 protected <T extends SynchronizationResult> T _removeValueForSynchronize(String dataName, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, UnknownDataException, BadDataPathCardinalityException 815 { 816 T result = _createSetValueResultInstance(); 817 818 if (DataHolderHelper.hasValueOrEmpty(this, dataName, context)) 819 { 820 _removeValue(dataName, context); 821 result.setHasChanged(true); 822 } 823 824 return result; 825 } 826 827 public void setValue(String dataPath, Object value) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 828 { 829 _setValue(dataPath, value, ValueContext.newInstance()); 830 } 831 832 public void setLocalValue(String dataPath, Object localValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 833 { 834 _setValue(dataPath, localValue, ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL)); 835 } 836 837 public void setExternalValue(String dataPath, Object externalValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 838 { 839 _setValue(dataPath, externalValue, ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL)); 840 } 841 842 /** 843 * Sets the value of the data at the given path 844 * @param dataPath path of the data 845 * @param value the value to set. Give <code>null</code> to empty the value. 846 * @param context context of the data to set 847 * @throws IllegalArgumentException if the given data path is null or empty 848 * @throws UndefinedItemPathException if the given data path is not defined by the model 849 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 850 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 851 */ 852 protected void _setValue(String dataPath, Object value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 853 { 854 _checkModifiableDefinition(dataPath, "Unable to set the value '" + value + "' at path '" + dataPath + "'."); 855 856 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 857 858 if (pathSegments == null || pathSegments.length < 1) 859 { 860 throw new IllegalArgumentException("Unable to set the value '" + value + "' at the given path. This path is empty."); 861 } 862 else if (pathSegments.length == 1) 863 { 864 ModelItem modelItem = getDefinition(dataPath); 865 866 // Simple path => set the value 867 if (modelItem instanceof ElementDefinition) 868 { 869 String dataName = _getFinalDataName(dataPath, context.getStatus()); 870 _setElementValue((ElementDefinition) modelItem, dataName, value); 871 } 872 else 873 { 874 throw new BadItemTypeException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "' in the repository because it is a group item."); 875 } 876 } 877 else 878 { 879 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 880 881 // Multiple items are allowed only at the last segment of the data path 882 if (isMultiple(parentPath)) 883 { 884 throw new BadDataPathCardinalityException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 885 } 886 887 Object parentValue = getValue(parentPath); 888 String childName = pathSegments[pathSegments.length - 1]; 889 if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder) 890 { 891 ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue; 892 if (context.getStatus().isPresent()) 893 { 894 if (ExternalizableDataStatus.EXTERNAL.equals(context.getStatus().get())) 895 { 896 parent.setExternalValue(childName, value); 897 } 898 else 899 { 900 parent.setLocalValue(childName, value); 901 } 902 } 903 else 904 { 905 parent.setValue(childName, value); 906 } 907 } 908 else 909 { 910 throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'."); 911 } 912 } 913 } 914 915 private void _setElementValue(ElementDefinition defintion, String dataName, Object value) 916 { 917 RepositoryElementType type = (RepositoryElementType) defintion.getType(); 918 919 if (defintion.isMultiple()) 920 { 921 if (value == null) 922 { 923 type.write(_modifiableRepositoryData, dataName, Array.newInstance(type.getManagedClass(), 0)); 924 } 925 else if (!value.getClass().isArray()) 926 { 927 // The value is single but should be an array. Create the array with the single value 928 Object arrayValue = Array.newInstance(value.getClass(), 1); 929 Array.set(arrayValue, 0, value); 930 type.write(_modifiableRepositoryData, dataName, arrayValue); 931 } 932 else 933 { 934 type.write(_modifiableRepositoryData, dataName, value); 935 } 936 } 937 else 938 { 939 if (type.getManagedClassArray().isInstance(value)) 940 { 941 // The value is multiple but should be single. 942 if (Array.getLength(value) > 1) 943 { 944 throw new IllegalArgumentException("Unable to set the mutilple value '" + value + "' at path '" + dataName + "'. This item is not multiple."); 945 } 946 else 947 { 948 Object singleValue = Array.getLength(value) == 1 ? Array.get(value, 0) : null; 949 type.write(_modifiableRepositoryData, dataName, singleValue); 950 } 951 } 952 else 953 { 954 type.write(_modifiableRepositoryData, dataName, value); 955 } 956 } 957 } 958 959 public void setStatus(String dataPath, ExternalizableDataStatus status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 960 { 961 _checkModifiableDefinition(dataPath, "Unable to set the status at path '" + dataPath + "'."); 962 963 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 964 965 if (pathSegments == null || pathSegments.length < 1) 966 { 967 throw new IllegalArgumentException("Unable to set the status at the given path. This path is empty."); 968 } 969 else if (pathSegments.length == 1) 970 { 971 ExternalizableDataStatus oldStatus = getStatus(dataPath); 972 boolean hasStatus = _repositoryData.hasValue(dataPath + STATUS_SUFFIX); 973 974 if (!hasStatus || oldStatus != status) 975 { 976 // Set the status if it has not been set yet or if it is different from the new one 977 _modifiableRepositoryData.setValue(dataPath + STATUS_SUFFIX, status.name().toLowerCase()); 978 } 979 980 if (oldStatus != status) 981 { 982 // Switch value and alternative value if the old status is different from the new one 983 ModelItem modelItem = getDefinition(dataPath); 984 if (modelItem instanceof CompositeDefinition) 985 { 986 _setStatus(dataPath, (CompositeDefinition) modelItem); 987 } 988 else if (modelItem instanceof RepeaterDefinition) 989 { 990 _setStatus(dataPath, (RepeaterDefinition) modelItem); 991 } 992 else 993 { 994 _setStatus(dataPath, (ElementDefinition) modelItem); 995 } 996 } 997 } 998 else 999 { 1000 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 1001 1002 // Multiple items are allowed only at the last segment of the data path 1003 if (isMultiple(parentPath)) 1004 { 1005 throw new BadDataPathCardinalityException("Unable to set the status on the data at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 1006 } 1007 1008 Object parentValue = getValue(parentPath); 1009 String childName = pathSegments[pathSegments.length - 1]; 1010 if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder) 1011 { 1012 ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue; 1013 parent.setStatus(childName, status); 1014 } 1015 else 1016 { 1017 throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'."); 1018 } 1019 } 1020 } 1021 1022 private void _setStatus(String dataPath, CompositeDefinition compositeDefinition) 1023 { 1024 if (_repositoryData.hasValue(dataPath)) 1025 { 1026 ModifiableModelAwareComposite composite = _getComposite(dataPath, compositeDefinition); 1027 ModifiableModelAwareComposite tempComposite = _getComposite(dataPath + __TEMP_SUFFIX, compositeDefinition, true); 1028 composite.copyTo(tempComposite); 1029 _modifiableRepositoryData.removeValue(dataPath); 1030 } 1031 1032 if (_repositoryData.hasValue(dataPath + ALTERNATIVE_SUFFIX)) 1033 { 1034 ModifiableModelAwareComposite altComposite = _getComposite(dataPath + ALTERNATIVE_SUFFIX, compositeDefinition); 1035 ModifiableModelAwareComposite composite = _getComposite(dataPath, compositeDefinition, true); 1036 altComposite.copyTo(composite); 1037 _modifiableRepositoryData.removeValue(dataPath + ALTERNATIVE_SUFFIX); 1038 } 1039 1040 if (_repositoryData.hasValue(dataPath + __TEMP_SUFFIX)) 1041 { 1042 ModifiableModelAwareComposite tempComposite = _getComposite(dataPath + __TEMP_SUFFIX, compositeDefinition); 1043 ModifiableModelAwareComposite altComposite = _getComposite(dataPath + ALTERNATIVE_SUFFIX, compositeDefinition, true); 1044 tempComposite.copyTo(altComposite); 1045 _modifiableRepositoryData.removeValue(dataPath + __TEMP_SUFFIX); 1046 } 1047 } 1048 1049 private void _setStatus(String dataPath, RepeaterDefinition repeaterDefinition) 1050 { 1051 if (_repositoryData.hasValue(dataPath)) 1052 { 1053 ModifiableModelAwareRepeater repeater = _getRepeater(dataPath, repeaterDefinition); 1054 ModifiableModelAwareRepeater tempRepeater = _getRepeater(dataPath + __TEMP_SUFFIX, repeaterDefinition, true); 1055 repeater.copyTo(tempRepeater); 1056 _modifiableRepositoryData.removeValue(dataPath); 1057 } 1058 1059 if (_repositoryData.hasValue(dataPath + ALTERNATIVE_SUFFIX)) 1060 { 1061 ModifiableModelAwareRepeater altRepeater = _getRepeater(dataPath + ALTERNATIVE_SUFFIX, repeaterDefinition); 1062 ModifiableModelAwareRepeater repeater = _getRepeater(dataPath, repeaterDefinition, true); 1063 altRepeater.copyTo(repeater); 1064 _modifiableRepositoryData.removeValue(dataPath + ALTERNATIVE_SUFFIX); 1065 } 1066 1067 if (_repositoryData.hasValue(dataPath + __TEMP_SUFFIX)) 1068 { 1069 ModifiableModelAwareRepeater tempRepeater = _getRepeater(dataPath + __TEMP_SUFFIX, repeaterDefinition); 1070 ModifiableModelAwareRepeater altRepeater = _getRepeater(dataPath + ALTERNATIVE_SUFFIX, repeaterDefinition, true); 1071 tempRepeater.copyTo(altRepeater); 1072 _modifiableRepositoryData.removeValue(dataPath + __TEMP_SUFFIX); 1073 } 1074 } 1075 1076 private void _setStatus(String dataName, ElementDefinition elementDefinition) 1077 { 1078 RepositoryElementType type = (RepositoryElementType) elementDefinition.getType(); 1079 if (type.hasValue(_repositoryData, dataName)) 1080 { 1081 Object value = type.read(_repositoryData, dataName); 1082 type.write(_modifiableRepositoryData, dataName + __TEMP_SUFFIX, value); 1083 type.remove(_modifiableRepositoryData, dataName); 1084 } 1085 1086 if (type.hasValue(_repositoryData, dataName + ALTERNATIVE_SUFFIX)) 1087 { 1088 Object altValue = type.read(_repositoryData, dataName + ALTERNATIVE_SUFFIX); 1089 type.write(_modifiableRepositoryData, dataName, altValue); 1090 type.remove(_modifiableRepositoryData, dataName + ALTERNATIVE_SUFFIX); 1091 } 1092 1093 if (type.hasValue(_repositoryData, dataName + __TEMP_SUFFIX)) 1094 { 1095 Object tempValue = type.read(_repositoryData, dataName + __TEMP_SUFFIX); 1096 type.write(_modifiableRepositoryData, dataName + ALTERNATIVE_SUFFIX, tempValue); 1097 type.remove(_modifiableRepositoryData, dataName + __TEMP_SUFFIX); 1098 } 1099 } 1100 1101 public void setComments(String dataName, List<DataComment> comments) throws IllegalArgumentException, UndefinedItemPathException 1102 { 1103 _checkModifiableDefinition(dataName, "Unable to retrieve the comments of the data named '" + dataName + "'."); 1104 1105 // Remove old comments if needed 1106 if (hasComments(dataName)) 1107 { 1108 _modifiableRepositoryData.removeValue(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 1109 } 1110 1111 if (!comments.isEmpty()) 1112 { 1113 ModifiableRepositoryData commentsRepositoryData = _modifiableRepositoryData.addRepositoryData(dataName + COMMENTS_SUFFIX, RepositoryConstants.COMPOSITE_METADTA_NODETYPE, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 1114 1115 int commentId = 1; 1116 for (DataComment comment : comments) 1117 { 1118 ModifiableRepositoryData commentRepositoryData = commentsRepositoryData.addRepositoryData(String.valueOf(commentId), RepositoryConstants.COMPOSITE_METADTA_NODETYPE, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 1119 commentId++; 1120 1121 commentRepositoryData.setValue("comment", comment.getComment()); 1122 commentRepositoryData.setValue("author", comment.getAuthor()); 1123 commentRepositoryData.setValue("date", DateUtils.asCalendar(comment.getDate())); 1124 } 1125 } 1126 } 1127 1128 public void removeValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 1129 { 1130 _removeValue(dataPath, ValueContext.newInstance()); 1131 } 1132 1133 public void removeLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 1134 { 1135 _removeValue(dataPath, ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL)); 1136 } 1137 1138 public void removeExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 1139 { 1140 _removeValue(dataPath, ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL)); 1141 } 1142 1143 /** 1144 * Removes the stored value of the data at the given path 1145 * @param dataPath path of the data 1146 * @param context context of the data to remove 1147 * @throws IllegalArgumentException if the given data path is null or empty 1148 * @throws BadItemTypeException if the value of the parent of the given path is not an item container 1149 * @throws UndefinedItemPathException if the given data path is not defined by the model 1150 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 1151 */ 1152 protected void _removeValue(String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 1153 { 1154 _checkModifiableDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'."); 1155 1156 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 1157 1158 if (pathSegments == null || pathSegments.length < 1) 1159 { 1160 throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty."); 1161 } 1162 else if (pathSegments.length == 1) 1163 { 1164 _doRemoveValue(dataPath, context); 1165 } 1166 else 1167 { 1168 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 1169 1170 // Multiple items are allowed only at the last segment of the data path 1171 if (isMultiple(parentPath)) 1172 { 1173 throw new BadDataPathCardinalityException("Unable to remove the value at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 1174 } 1175 1176 Object parentValue = getValue(parentPath); 1177 String childName = pathSegments[pathSegments.length - 1]; 1178 if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder) 1179 { 1180 ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue; 1181 if (context.getStatus().isPresent()) 1182 { 1183 if (ExternalizableDataStatus.EXTERNAL.equals(context.getStatus().get())) 1184 { 1185 parent.removeExternalValue(childName); 1186 } 1187 else 1188 { 1189 parent.removeLocalValue(childName); 1190 } 1191 } 1192 else 1193 { 1194 parent.removeValue(childName); 1195 } 1196 } 1197 } 1198 } 1199 1200 private void _doRemoveValue(String dataName, ValueContext context) 1201 { 1202 ModelItem modelItem = getDefinition(dataName); 1203 String finalDataName = _getFinalDataName(dataName, context.getStatus()); 1204 RepositoryModelItemType type = getType(dataName); 1205 if (modelItem instanceof RepeaterDefinition && DataHolderHelper.isRepeaterEntryPath(finalDataName)) 1206 { 1207 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(finalDataName); 1208 String repeaterName = repeaterNameAndEntryPosition.getLeft(); 1209 int entryPosition = repeaterNameAndEntryPosition.getRight(); 1210 ModifiableModelAwareRepeater repeater = _getRepeater(repeaterName, (RepeaterDefinition) modelItem); 1211 repeater.removeEntry(entryPosition); 1212 } 1213 else 1214 { 1215 type.remove(_modifiableRepositoryData, finalDataName); 1216 } 1217 } 1218 1219 public void removeExternalizableMetadataIfExists(String dataPath) throws IllegalArgumentException, BadItemTypeException, UndefinedItemPathException, BadDataPathCardinalityException 1220 { 1221 _checkModifiableDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'."); 1222 1223 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 1224 1225 if (pathSegments == null || pathSegments.length < 1) 1226 { 1227 throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty."); 1228 } 1229 else if (pathSegments.length == 1) 1230 { 1231 _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataPath); 1232 } 1233 else 1234 { 1235 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 1236 1237 // Multiple items are allowed only at the last segment of the data path 1238 if (isMultiple(parentPath)) 1239 { 1240 throw new BadDataPathCardinalityException("Unable to remove the value at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path."); 1241 } 1242 1243 Object parentValue = getValue(parentPath); 1244 String childName = pathSegments[pathSegments.length - 1]; 1245 if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder) 1246 { 1247 ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue; 1248 parent.removeExternalizableMetadataIfExists(childName); 1249 } 1250 } 1251 } 1252 1253 private void _removeExternalizableMetadataIfExists(ModifiableRepositoryData repositoryData, String dataName) 1254 { 1255 RepositoryModelItemType type = getType(dataName); 1256 if (type.hasValue(repositoryData, dataName + ALTERNATIVE_SUFFIX)) 1257 { 1258 type.remove(repositoryData, dataName + ALTERNATIVE_SUFFIX); 1259 } 1260 1261 if (repositoryData.hasValue(dataName + STATUS_SUFFIX)) 1262 { 1263 repositoryData.removeValue(dataName + STATUS_SUFFIX); 1264 } 1265 } 1266 1267 /** 1268 * Creates an instance of {@link SynchronizationContext} 1269 * @param <T> the type of the {@link SynchronizationContext} 1270 * @return the created {@link SynchronizationContext} 1271 */ 1272 @SuppressWarnings("unchecked") 1273 protected <T extends SynchronizationContext> T _createSynchronizationContextInstance() 1274 { 1275 return (T) SynchronizationContext.newInstance(); 1276 } 1277 1278 /** 1279 * Creates an instance of {@link SynchronizationResult} 1280 * @param <T> the type of the {@link SynchronizationResult} 1281 * @return the created instance of {@link SynchronizationResult} 1282 */ 1283 @SuppressWarnings("unchecked") 1284 protected <T extends SynchronizationResult> T _createSetValueResultInstance() 1285 { 1286 return (T) new SynchronizationResult(); 1287 } 1288 1289 @Override 1290 public ModifiableRepositoryData getRepositoryData() 1291 { 1292 return _modifiableRepositoryData; 1293 } 1294 1295 @Override 1296 public Optional<? extends ModifiableIndexableDataHolder> getParentDataHolder() 1297 { 1298 return _modifiableParent; 1299 } 1300 1301 @Override 1302 public ModifiableIndexableDataHolder getRootDataHolder() 1303 { 1304 return _modifiableRoot; 1305 } 1306 1307 /** 1308 * Check definition for data path, for modification methods 1309 * @param dataPath the data path 1310 * @param errorMsg the error message to throw 1311 */ 1312 protected void _checkModifiableDefinition(String dataPath, String errorMsg) 1313 { 1314 super._checkDefinition(dataPath, errorMsg); 1315 1316 ModelItem modelItem = getDefinition(dataPath); 1317 if (modelItem instanceof ElementDefinition definition && !definition.isEditable()) 1318 { 1319 throw new UndefinedItemPathException(errorMsg + " The model item '" + modelItem.getPath() + "' can't be modified."); 1320 } 1321 } 1322}