001/* 002 * Copyright 2022 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.Arrays; 020import java.util.Collection; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027import org.apache.commons.lang3.StringUtils; 028import org.apache.commons.lang3.tuple.Pair; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031import org.xml.sax.ContentHandler; 032import org.xml.sax.SAXException; 033 034import org.ametys.cms.data.ContentValue; 035import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject; 036import org.ametys.cms.data.holder.IndexableDataHolder; 037import org.ametys.cms.data.holder.group.IndexableComposite; 038import org.ametys.cms.data.holder.group.IndexableRepeater; 039import org.ametys.cms.data.holder.group.impl.DefaultModelAwareComposite; 040import org.ametys.cms.data.holder.group.impl.DefaultModelAwareRepeater; 041import org.ametys.cms.model.ContentElementDefinition; 042import org.ametys.cms.model.properties.Property; 043import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 044import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject; 045import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 046import org.ametys.plugins.repository.data.holder.DataHolder; 047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 048import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 049import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 051import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 052import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 053import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 054import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 055import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 056import org.ametys.plugins.repository.data.holder.values.ValueContext; 057import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 058import org.ametys.plugins.repository.data.type.RepositoryElementType; 059import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType; 060import org.ametys.plugins.repository.data.type.RepositoryModelItemType; 061import org.ametys.plugins.repository.model.CompositeDefinition; 062import org.ametys.plugins.repository.model.RepeaterDefinition; 063import org.ametys.plugins.repository.model.RepositoryDataContext; 064import org.ametys.runtime.model.DefinitionContext; 065import org.ametys.runtime.model.ElementDefinition; 066import org.ametys.runtime.model.ModelHelper; 067import org.ametys.runtime.model.ModelItem; 068import org.ametys.runtime.model.ModelItemContainer; 069import org.ametys.runtime.model.ModelViewItem; 070import org.ametys.runtime.model.ModelViewItemGroup; 071import org.ametys.runtime.model.View; 072import org.ametys.runtime.model.ViewElement; 073import org.ametys.runtime.model.ViewHelper; 074import org.ametys.runtime.model.ViewItem; 075import org.ametys.runtime.model.ViewItemAccessor; 076import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 077import org.ametys.runtime.model.exception.BadItemTypeException; 078import org.ametys.runtime.model.exception.UndefinedItemPathException; 079import org.ametys.runtime.model.type.DataContext; 080import org.ametys.runtime.model.type.ElementType; 081import org.ametys.runtime.model.type.ModelItemType; 082 083/** 084 * Default implementation for data holder with model 085 */ 086public class DefaultModelAwareDataHolder implements IndexableDataHolder 087{ 088 private static final Logger __LOGGER = LoggerFactory.getLogger(ModelAwareDataHolder.class); 089 090 /** Repository data to use to store data in the repository */ 091 protected RepositoryData _repositoryData; 092 093 /** Parent of the current {@link DataHolder} */ 094 protected Optional<? extends IndexableDataHolder> _parent; 095 096 /** Root {@link DataHolder} */ 097 protected IndexableDataHolder _root; 098 099 /** Model containers to use to get information about definitions */ 100 protected Collection<? extends ModelItemContainer> _itemContainers; 101 102 /** 103 * Creates a default model aware data holder 104 * @param repositoryData the repository data to use 105 * @param itemContainer the model container 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. 106 */ 107 public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer itemContainer) 108 { 109 this(repositoryData, itemContainer, Optional.empty(), Optional.empty()); 110 } 111 112 /** 113 * Creates a default model aware data holder 114 * @param repositoryData the repository data to use 115 * @param itemContainer the model container 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 * @param parent the optional parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 117 * @param root the root {@link DataHolder} 118 */ 119 public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer itemContainer, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root) 120 { 121 this(repositoryData, List.of(itemContainer), parent, root); 122 } 123 124 /** 125 * Creates a default model aware data holder 126 * @param repositoryData the repository data to use 127 * @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. 128 * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 129 * @param root the root {@link DataHolder} 130 */ 131 public DefaultModelAwareDataHolder(RepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root) 132 { 133 _repositoryData = repositoryData; 134 _itemContainers = itemContainers; 135 _ensureNonNullItemContainers(); 136 137 _parent = parent; 138 _root = root.map(IndexableDataHolder.class::cast) 139 .or(() -> _parent.map(IndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root 140 .orElse(this); // if no root or parent is specified, the root is the current DataHolder 141 } 142 143 private void _ensureNonNullItemContainers() 144 { 145 for (ModelItemContainer itemContainer : _itemContainers) 146 { 147 if (itemContainer == null) 148 { 149 throw new NullPointerException(String.format("Invalid item containers for creating DefaultModelAwareDataHolder, one of them is null: %s", _itemContainers)); 150 } 151 } 152 } 153 154 public IndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 155 { 156 Object value = getValue(compositePath); 157 return _getCompositeFromValue(value, compositePath); 158 } 159 160 public IndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 161 { 162 Object value = getLocalValue(compositePath); 163 return _getCompositeFromValue(value, compositePath); 164 } 165 166 public IndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 167 { 168 Object value = getExternalValue(compositePath); 169 return _getCompositeFromValue(value, compositePath); 170 } 171 172 private IndexableComposite _getCompositeFromValue(Object value, String compositePath) 173 { 174 if (value == null) 175 { 176 return null; 177 } 178 else if (value instanceof IndexableComposite composite) 179 { 180 return composite; 181 } 182 else 183 { 184 throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite."); 185 } 186 } 187 188 public IndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 189 { 190 Object value = getValue(repeaterPath); 191 return _getRepeaterFromValue(value, repeaterPath); 192 } 193 194 public IndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 195 { 196 Object value = getLocalValue(repeaterPath); 197 return _getRepeaterFromValue(value, repeaterPath); 198 } 199 200 public IndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 201 { 202 Object value = getExternalValue(repeaterPath); 203 return _getRepeaterFromValue(value, repeaterPath); 204 } 205 206 private IndexableRepeater _getRepeaterFromValue(Object value, String repeaterPath) 207 { 208 if (value == null) 209 { 210 return null; 211 } 212 else if (value instanceof IndexableRepeater repeater) 213 { 214 return repeater; 215 } 216 else 217 { 218 throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater."); 219 } 220 } 221 222 public <T> T getValue(String dataPath, boolean allowMultiValuedPathSegments) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 223 { 224 return _getValue(dataPath, allowMultiValuedPathSegments, Optional.empty()); 225 } 226 227 public <T> T getLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 228 { 229 return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.LOCAL)); 230 } 231 232 public <T> T getExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 233 { 234 return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.EXTERNAL)); 235 } 236 237 private <T> T _getValue(String dataPath, boolean allowMultiValuedPathSegments, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 238 { 239 _checkDefinition(dataPath, status.isPresent(), "Unable to retrieve the value at path '" + dataPath + "'."); 240 241 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 242 243 if (pathSegments == null || pathSegments.length < 1) 244 { 245 throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty."); 246 } 247 else if (pathSegments.length == 1) 248 { 249 // Simple path => get the value 250 ModelItem modelItem = getDefinition(dataPath); 251 String dataName = _getFinalDataName(dataPath, status); 252 253 if (modelItem instanceof Property property) 254 { 255 return _getPropertyValue(property); 256 } 257 else if (modelItem instanceof ElementDefinition elementDefinition) 258 { 259 return _getElementValue(elementDefinition, dataName); 260 } 261 else 262 { 263 return _getGroupValue(modelItem, dataName); 264 } 265 } 266 else 267 { 268 if (isMultiple(pathSegments[0])) 269 { 270 if (allowMultiValuedPathSegments) 271 { 272 return _getMultipleValues(dataPath); 273 } 274 else 275 { 276 // Multiple items are allowed only at the last segment of the data path 277 throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path."); 278 } 279 } 280 else 281 { 282 // Path where first part is a data holder 283 ModelAwareDataHolder dataHolder = getValue(pathSegments[0]); 284 if (dataHolder == null) 285 { 286 return null; 287 } 288 else 289 { 290 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 291 return status.isPresent() 292 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 293 ? dataHolder.getExternalValue(subDataPath) 294 : dataHolder.getLocalValue(subDataPath) 295 : dataHolder.getValue(subDataPath, allowMultiValuedPathSegments); 296 } 297 } 298 } 299 } 300 301 public ExternalizableDataStatus getStatus(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadDataPathCardinalityException 302 { 303 _checkDefinition(dataPath, true, "Unable to retrieve the value at path '" + dataPath + "'."); 304 305 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 306 307 if (pathSegments == null || pathSegments.length < 1) 308 { 309 throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty."); 310 } 311 else if (pathSegments.length == 1) 312 { 313 if (_repositoryData.hasValue(dataPath + STATUS_SUFFIX)) 314 { 315 String status = _repositoryData.getString(dataPath + STATUS_SUFFIX); 316 return ExternalizableDataStatus.valueOf(status.toUpperCase()); 317 } 318 else 319 { 320 return ExternalizableDataStatus.LOCAL; 321 } 322 } 323 else 324 { 325 if (isMultiple(pathSegments[0])) 326 { 327 // Multiple items are allowed only at the last segment of the data path 328 throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path."); 329 } 330 else 331 { 332 // Path where first part is a data holder 333 ModelAwareDataHolder dataHolder = getValue(pathSegments[0]); 334 if (dataHolder == null) 335 { 336 return null; 337 } 338 else 339 { 340 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 341 return dataHolder.getStatus(subDataPath); 342 } 343 } 344 } 345 } 346 347 @SuppressWarnings("unchecked") 348 private <T> T _getPropertyValue(Property property) 349 { 350 return getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametyObject ? (T) property.getValue(ametyObject) : null; 351 } 352 353 @SuppressWarnings("unchecked") 354 private <T> T _getElementValue(ElementDefinition definition, String dataName) 355 { 356 RepositoryElementType type = (RepositoryElementType) definition.getType(); 357 Object value = type.read(_repositoryData, dataName); 358 359 if (definition.isMultiple() && type.getManagedClass().isInstance(value)) 360 { 361 // The value is single but should be an array. Create the array with the single value 362 T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1); 363 Array.set(arrayValue, 0, value); 364 return arrayValue; 365 } 366 else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value)) 367 { 368 // The value is multiple but should be single. Retrieve the first value of the array 369 return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null; 370 } 371 else 372 { 373 return (T) value; 374 } 375 } 376 377 @SuppressWarnings("unchecked") 378 private <T> T _getGroupValue(ModelItem modelItem, String dataName) 379 { 380 if (modelItem instanceof RepeaterDefinition) 381 { 382 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName); 383 if (repeaterNameAndEntryPosition != null) 384 { 385 return (T) DataHolderHelper.getRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 386 } 387 else 388 { 389 return (T) _getRepeater(dataName, (RepeaterDefinition) modelItem); 390 } 391 } 392 else 393 { 394 return (T) _getComposite(dataName, (CompositeDefinition) modelItem); 395 } 396 } 397 398 @SuppressWarnings("unchecked") 399 private <T> T _getMultipleValues(String dataPath) 400 { 401 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 402 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 403 Class managedClass = _getManagedClass(this, dataPath); 404 405 Object segmentValue = getValue(pathSegments[0]); 406 if (segmentValue == null) 407 { 408 return (T) Array.newInstance(managedClass, 0); 409 } 410 411 if (segmentValue instanceof ModelAwareRepeater) 412 { 413 ModelAwareRepeater repeater = (ModelAwareRepeater) segmentValue; 414 return DataHolderHelper.aggregateMultipleValues(repeater.getEntries(), subDataPath, managedClass); 415 } 416 else 417 { 418 ModelAwareDataHolder[] dataHolders = (ModelAwareDataHolder[]) segmentValue; 419 return DataHolderHelper.aggregateMultipleValues(Arrays.asList(dataHolders), subDataPath, managedClass); 420 } 421 } 422 423 private Class _getManagedClass(ModelAwareDataHolder dataHolder, String dataPath) 424 { 425 Class managedClass; 426 ModelItem modelItem = dataHolder.getDefinition(dataPath); 427 if (modelItem instanceof ElementDefinition) 428 { 429 managedClass = ((ElementDefinition) modelItem).getType().getManagedClass(); 430 } 431 else 432 { 433 if (modelItem instanceof RepeaterDefinition) 434 { 435 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 436 managedClass = repeaterNameAndEntryPosition != null ? _getRepeaterEntryClass() : _getRepeaterClass(); 437 } 438 else 439 { 440 managedClass = _getCompositeClass(); 441 } 442 } 443 return managedClass; 444 } 445 446 /** 447 * Retrieves the class of the managed repeater entries 448 * @return the class of the managed repeater entries 449 */ 450 protected Class _getRepeaterEntryClass() 451 { 452 return ModelAwareRepeaterEntry.class; 453 } 454 455 /** 456 * Retrieves the class of the managed repeaters 457 * @return the class of the managed repeaters 458 */ 459 protected Class _getRepeaterClass() 460 { 461 return ModelAwareRepeater.class; 462 } 463 464 /** 465 * Retrieves the class of the managed composites 466 * @return the class of the managed composites 467 */ 468 protected Class _getCompositeClass() 469 { 470 return ModelAwareComposite.class; 471 } 472 473 public <T> T getValue(String dataPath, boolean useDefaultFromModel, T defaultValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 474 { 475 _checkDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'."); 476 477 if (hasValue(dataPath)) 478 { 479 return getValue(dataPath); 480 } 481 482 if (useDefaultFromModel) 483 { 484 ModelItem modelItem = getDefinition(dataPath); 485 486 if (modelItem instanceof ElementDefinition) 487 { 488 @SuppressWarnings("unchecked") 489 T defaultFromModel = (T) ((ElementDefinition) modelItem).getDefaultValue(); 490 if (defaultFromModel != null) 491 { 492 return defaultFromModel; 493 } 494 } 495 } 496 497 return defaultValue; 498 } 499 500 /** 501 * Retrieves the composite with the given name 502 * @param name name of the composite to retrieve 503 * @param compositeDefinition the definition of the composite to retrieve 504 * @return the composite 505 * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite 506 */ 507 protected ModelAwareComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException 508 { 509 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType(); 510 RepositoryData compositeRepositoryData = type.read(_repositoryData, name); 511 512 if (compositeRepositoryData != null) 513 { 514 return new DefaultModelAwareComposite(compositeRepositoryData, compositeDefinition, this, _root); 515 } 516 else 517 { 518 return null; 519 } 520 } 521 522 /** 523 * Retrieves the repeater with the given name 524 * @param name name of the repeater to retrieve 525 * @param repeaterDefinition the definition of the repeater to retrieve 526 * @return the repeater 527 * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater 528 */ 529 protected ModelAwareRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException 530 { 531 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType(); 532 RepositoryData repeaterRepositoryData = type.read(_repositoryData, name); 533 534 if (repeaterRepositoryData != null) 535 { 536 return new DefaultModelAwareRepeater(repeaterRepositoryData, repeaterDefinition, this, _root); 537 } 538 else 539 { 540 return null; 541 } 542 } 543 544 public boolean hasValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 545 { 546 return _hasValue(dataPath, Optional.empty()); 547 } 548 549 public boolean hasLocalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 550 { 551 return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.LOCAL)); 552 } 553 554 public boolean hasExternalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 555 { 556 return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL)); 557 } 558 559 @SuppressWarnings("unchecked") 560 private boolean _hasValue(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException 561 { 562 if (!hasDefinition(dataPath)) 563 { 564 return false; 565 } 566 567 if (StringUtils.isEmpty(dataPath)) 568 { 569 throw new IllegalArgumentException("Unable to check if there is a non empty value at the given path. This path is empty."); 570 } 571 else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 572 { 573 if (DataHolderHelper.isRepeaterEntryPath(dataPath)) 574 { 575 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 576 return DataHolderHelper.hasNonEmptyRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 577 } 578 else 579 { 580 if (getDefinition(dataPath) instanceof Property property) 581 { 582 if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 583 { 584 return property.getValue(ametysObject) != null; 585 } 586 else 587 { 588 return false; 589 } 590 } 591 else 592 { 593 RepositoryModelItemType type = getType(dataPath); 594 String dataName = _getFinalDataName(dataPath, status); 595 596 try 597 { 598 return type.hasNonEmptyValue(_repositoryData, dataName); 599 } 600 catch (BadItemTypeException e) 601 { 602 return false; 603 } 604 } 605 } 606 } 607 else 608 { 609 String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 610 611 // Multiple items are allowed only at the last segment of the data path 612 if (isMultiple(parentPath)) 613 { 614 throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path."); 615 } 616 617 try 618 { 619 ModelAwareDataHolder parent = getValue(parentPath); 620 if (parent == null) 621 { 622 return false; 623 } 624 else 625 { 626 String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 627 return status.isPresent() 628 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 629 ? parent.hasExternalValue(childName) 630 : parent.hasLocalValue(childName) 631 : parent.hasValue(childName); 632 } 633 } 634 catch (BadItemTypeException e) 635 { 636 return false; 637 } 638 } 639 } 640 641 public boolean hasValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 642 { 643 return _hasValueOrEmpty(dataPath, Optional.empty()); 644 } 645 646 public boolean hasLocalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 647 { 648 return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.LOCAL)); 649 } 650 651 public boolean hasExternalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 652 { 653 return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL)); 654 } 655 656 @SuppressWarnings("unchecked") 657 private boolean _hasValueOrEmpty(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException 658 { 659 if (!hasDefinition(dataPath)) 660 { 661 return false; 662 } 663 664 if (StringUtils.isEmpty(dataPath)) 665 { 666 throw new IllegalArgumentException("Unable to check if there is a value at the given path. This path is empty."); 667 } 668 else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 669 { 670 if (DataHolderHelper.isRepeaterEntryPath(dataPath)) 671 { 672 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 673 return DataHolderHelper.hasRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 674 } 675 else 676 { 677 if (getDefinition(dataPath) instanceof Property property) 678 { 679 if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 680 { 681 return property.getValue(ametysObject) != null; 682 } 683 else 684 { 685 return false; 686 } 687 } 688 else 689 { 690 RepositoryModelItemType type = getType(dataPath); 691 String dataName = _getFinalDataName(dataPath, status); 692 return type.hasValue(_repositoryData, dataName); 693 } 694 } 695 } 696 else 697 { 698 String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 699 700 // Multiple items are allowed only at the last segment of the data path 701 if (isMultiple(parentPath)) 702 { 703 throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path."); 704 } 705 706 try 707 { 708 ModelAwareDataHolder parent = getValue(parentPath); 709 if (parent == null) 710 { 711 return false; 712 } 713 else 714 { 715 String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 716 return status.isPresent() 717 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 718 ? parent.hasExternalValueOrEmpty(childName) 719 : parent.hasLocalValueOrEmpty(childName) 720 : parent.hasValueOrEmpty(childName); 721 } 722 } 723 catch (BadItemTypeException e) 724 { 725 return false; 726 } 727 } 728 } 729 730 /** 731 * Retrieves the name of the data according to the given status 732 * @param dataName the name of the data 733 * @param status the status 734 * @return the final name of the data 735 */ 736 protected String _getFinalDataName(String dataName, Optional<ExternalizableDataStatus> status) 737 { 738 if (status.isPresent() && getStatus(dataName) != status.get()) 739 { 740 return dataName + ALTERNATIVE_SUFFIX; 741 } 742 743 return dataName; 744 } 745 746 public Collection< ? extends ModelItemContainer> getModel() 747 { 748 return _itemContainers; 749 } 750 751 public ModelItem getDefinition(String path) throws IllegalArgumentException, UndefinedItemPathException 752 { 753 try 754 { 755 ModelItem definition = IndexableDataHolder.super.getDefinition(path); 756 757 // A definition has been found, ok 758 return definition; 759 } 760 catch (UndefinedItemPathException e) 761 { 762 // Look for system properties 763 if (StringUtils.contains(path, ModelItem.ITEM_PATH_SEPARATOR)) 764 { 765 String parentDataPath = StringUtils.substringBeforeLast(path, ModelItem.ITEM_PATH_SEPARATOR); 766 ModelItem modelItem = getDefinition(parentDataPath); 767 if (modelItem instanceof ContentElementDefinition) 768 { 769 SystemPropertyExtensionPoint contentSystemPropertyExtensionPoint = IndexableDataHolderHelper.getContentSystemPropertyExtensionPoint(); 770 String propertyName = StringUtils.substringAfterLast(path, ModelItem.ITEM_PATH_SEPARATOR); 771 if (contentSystemPropertyExtensionPoint.hasExtension(propertyName)) 772 { 773 return contentSystemPropertyExtensionPoint.getExtension(propertyName); 774 } 775 } 776 } 777 else if (getParentDataHolder().isEmpty() 778 && getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject 779 && ametysObject.getSystemPropertyExtensionPoint().isPresent()) 780 { 781 SystemPropertyExtensionPoint systemPropertyExtensionPoint = ametysObject.getSystemPropertyExtensionPoint().get(); 782 if (systemPropertyExtensionPoint.hasExtension(path)) 783 { 784 return systemPropertyExtensionPoint.getExtension(path); 785 } 786 } 787 788 // No system property has been found, throw the UndefinedItemPathException 789 throw e; 790 } 791 } 792 793 public Collection<String> getDataNames() 794 { 795 return ModelHelper.getModelItems(getModel()) 796 .stream() 797 .map(ModelItem::getName) 798 .filter(this::hasValueOrEmpty) 799 .toList(); 800 } 801 802 @SuppressWarnings("unchecked") 803 public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException 804 { 805 _checkDefinition(dataPath, "Unable to generate SAX events for the data at path '" + dataPath + "'."); 806 807 DataContext newContext = _addObjectInfoToContext(context); 808 if (StringUtils.isBlank(newContext.getDataPath())) 809 { 810 newContext.withDataPath(dataPath); 811 } 812 813 if (IndexableDataHolderHelper.renderValue(this, dataPath, newContext, false) && IndexableDataHolderHelper.hasValue(this, dataPath, newContext)) 814 { 815 ModelItem modelItem = getDefinition(dataPath); 816 if (modelItem instanceof Property property) 817 { 818 ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath); 819 property.valueToSAX(contentHandler, ametysObject, newContext); 820 } 821 else 822 { 823 ModelItemType type = modelItem.getType(); 824 Object value = getValue(dataPath); 825 826 type.valueToSAX(contentHandler, modelItem.getName(), value, newContext); 827 } 828 } 829 } 830 831 public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 832 { 833 DataContext newContext = _addObjectInfoToContext(context); 834 IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, false); 835 } 836 837 public void dataToSAXForEdition(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 838 { 839 DataContext newContext = _addObjectInfoToContext(context); 840 IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, true); 841 842 if (viewItemAccessor instanceof View view) 843 { 844 DefinitionContext definitionContext = DefinitionContext.newInstance() 845 .withView(view); 846 IndexableDataHolderHelper.externalDisableConditionsToSAX(this, contentHandler, viewItemAccessor, definitionContext, newContext); 847 } 848 } 849 850 @SuppressWarnings("unchecked") 851 public Object dataToJSON(String dataPath, DataContext context) 852 { 853 _checkDefinition(dataPath, "Unable to convert the data at path '" + dataPath + "' to JSON."); 854 855 DataContext newContext = _addObjectInfoToContext(context) 856 .withDataPath(dataPath); 857 858 if (IndexableDataHolderHelper.renderValue(this, dataPath, newContext, false) && IndexableDataHolderHelper.hasValue(this, dataPath, newContext)) 859 { 860 ModelItem modelItem = getDefinition(dataPath); 861 if (modelItem instanceof Property property) 862 { 863 ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath); 864 return property.valueToJSON(ametysObject, newContext); 865 } 866 else 867 { 868 ModelItemType type = modelItem.getType(); 869 Object value = getValue(dataPath); 870 871 return type.valueToJSONForClient(value, newContext); 872 } 873 } 874 else 875 { 876 return null; 877 } 878 } 879 880 /** 881 * Retrieves the ametys object containing the property at the given path 882 * @param dataPath the path of the property 883 * @return the ametys object containing the property 884 * @throws UndefinedItemPathException if the given path does not represent a property 885 */ 886 protected ModelAwareDataAwareAmetysObject _getPropertysAmetysObject(String dataPath) throws UndefinedItemPathException 887 { 888 if (StringUtils.contains(dataPath, ModelItem.ITEM_PATH_SEPARATOR)) 889 { 890 String parentDataPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 891 ContentValue value = getValue(parentDataPath); 892 return value.getContent(); 893 } 894 else if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 895 { 896 return ametysObject; 897 } 898 else 899 { 900 throw new UndefinedItemPathException("There is no property at path '" + dataPath + "'"); 901 } 902 } 903 904 public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 905 { 906 DataContext newContext = _addObjectInfoToContext(context); 907 return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, false); 908 } 909 910 public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 911 { 912 DataContext newContext = _addObjectInfoToContext(context); 913 Map<String, Object> json = IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, true); 914 915 if (viewItemAccessor instanceof View view) 916 { 917 DefinitionContext definitionContext = DefinitionContext.newInstance() 918 .withView(view); 919 920 Map<String, Boolean> conditionsValues = IndexableDataHolderHelper.getExternalDisableConditionsValues(this, viewItemAccessor, definitionContext, newContext); 921 if (!conditionsValues.isEmpty()) 922 { 923 json.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 924 } 925 } 926 927 return json; 928 } 929 930 public Map<String, Object> dataToMap(ViewItemAccessor viewItemAccessor, DataContext context) 931 { 932 DataContext newContext = _addObjectInfoToContext(context); 933 return IndexableDataHolderHelper.dataToMap(this, viewItemAccessor, newContext); 934 } 935 936 public boolean hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException 937 { 938 return hasDifferences(viewItemAccessor, values, _createSynchronizationContextInstance()); 939 } 940 941 public boolean hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 942 { 943 return _hasDifferences(viewItemAccessor, values, context); 944 } 945 946 /** 947 * Check if there are differences between the given values and the current ones 948 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 949 * @param values the values to check 950 * @param context the context of the synchronization 951 * @return <code>true</code> if there are differences, <code>false</code> otherwise 952 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 953 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 954 */ 955 protected boolean _hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 956 { 957 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 958 { 959 if (viewItem instanceof ModelViewItem) 960 { 961 if (viewItem instanceof ModelViewItemGroup group && _hasDifferencesInGroup(group, values, context) 962 || viewItem instanceof ViewElement element && _hasDifferencesInElement(element, values, context)) 963 { 964 return true; 965 } 966 } 967 else if (viewItem instanceof ViewItemAccessor accessor && _hasDifferences(accessor, values, context)) 968 { 969 return true; 970 } 971 } 972 973 // No difference has been found 974 return false; 975 } 976 977 public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException 978 { 979 return getDifferences(viewItemAccessor, values, _createSynchronizationContextInstance()); 980 } 981 982 public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 983 { 984 return _getDifferences(viewItemAccessor, values, context); 985 } 986 987 /** 988 * Get the collection of model items where there are differences between the given values and the current ones 989 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 990 * @param values the values to check 991 * @param context the context of the synchronization 992 * @return a collection of model items with differences 993 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 994 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 995 */ 996 protected Collection<ModelItem> _getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 997 { 998 Set<ModelItem> modelItems = new HashSet<>(); 999 1000 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 1001 { 1002 if (viewItem instanceof ModelViewItem) 1003 { 1004 if (viewItem instanceof ModelViewItemGroup group) 1005 { 1006 modelItems.addAll(_getDifferencesInGroup(group, values, context)); 1007 } 1008 else if (viewItem instanceof ViewElement element) 1009 { 1010 if (_hasDifferencesInElement(element, values, context)) 1011 { 1012 modelItems.add(element.getDefinition()); 1013 } 1014 } 1015 } 1016 else if (viewItem instanceof ViewItemAccessor accessor) 1017 { 1018 modelItems.addAll(_getDifferences(accessor, values, context)); 1019 } 1020 } 1021 1022 return modelItems; 1023 } 1024 1025 /** 1026 * Check if there are differences between the given values and the given group's ones 1027 * @param modelViewItemGroup the group 1028 * @param values the values to check 1029 * @param synchronizationContext the context of the synchronization 1030 * @return <code>true</code> if there are differences, <code>false</code> otherwise 1031 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 1032 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 1033 */ 1034 @SuppressWarnings("unchecked") 1035 protected boolean _hasDifferencesInGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException 1036 { 1037 ModelItem modelItem = modelViewItemGroup.getDefinition(); 1038 String dataName = modelItem.getName(); 1039 Object value = values.get(dataName); 1040 if (value instanceof UntouchedValue) 1041 { 1042 if (__LOGGER.isDebugEnabled()) 1043 { 1044 String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup); 1045 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1046 } 1047 return false; 1048 } 1049 else if (value == null) 1050 { 1051 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 1052 return DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext); 1053 } 1054 else if (modelItem instanceof RepeaterDefinition) 1055 { 1056 if (value instanceof SynchronizableRepeater || value instanceof List) 1057 { 1058 ModelAwareRepeater repeater = getRepeater(dataName); 1059 SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null); 1060 1061 if (repeater == null) 1062 { 1063 if (__LOGGER.isDebugEnabled()) 1064 { 1065 String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup); 1066 __LOGGER.debug("#hasDifferences[{}] differences detected: repeater will be created", viewItemPath); 1067 } 1068 return true; 1069 } 1070 else 1071 { 1072 return repeater.hasDifferences(modelViewItemGroup, repeaterValues, synchronizationContext); 1073 } 1074 } 1075 else 1076 { 1077 throw new BadItemTypeException("Unable to check differences for the repeater named '" + dataName + "': the given value should be a list containing its entries"); 1078 } 1079 } 1080 else 1081 { 1082 if (value instanceof Map) 1083 { 1084 ModelAwareComposite composite = getComposite(dataName); 1085 if (composite == null) 1086 { 1087 if (__LOGGER.isDebugEnabled()) 1088 { 1089 String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup); 1090 __LOGGER.debug("#hasDifferences[{}] differences detected: composite will be created", viewItemPath); 1091 } 1092 return true; 1093 } 1094 else 1095 { 1096 return composite.hasDifferences(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext); 1097 } 1098 } 1099 else 1100 { 1101 throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items"); 1102 } 1103 } 1104 } 1105 1106 /** 1107 * Get the collection of model items where there are differences between the given values and the given group's ones 1108 * @param modelViewItemGroup the group 1109 * @param values the values to check 1110 * @param synchronizationContext the context of the synchronization 1111 * @return a collection of model items with differences 1112 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 1113 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 1114 */ 1115 @SuppressWarnings("unchecked") 1116 protected Collection<ModelItem> _getDifferencesInGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException 1117 { 1118 Set<ModelItem> modelItems = new HashSet<>(); 1119 1120 ModelItem modelItem = modelViewItemGroup.getDefinition(); 1121 String dataName = modelItem.getName(); 1122 Object value = values.get(dataName); 1123 if (value instanceof UntouchedValue) 1124 { 1125 return modelItems; 1126 } 1127 else if (value == null) 1128 { 1129 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 1130 if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext)) 1131 { 1132 modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup)); 1133 } 1134 } 1135 else if (modelItem instanceof RepeaterDefinition) 1136 { 1137 if (value instanceof SynchronizableRepeater || value instanceof List) 1138 { 1139 ModelAwareRepeater repeater = getRepeater(dataName); 1140 SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null); 1141 1142 if (repeater == null) 1143 { 1144 modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup)); 1145 } 1146 else 1147 { 1148 modelItems.addAll(repeater.getDifferences(modelViewItemGroup, repeaterValues, synchronizationContext)); 1149 } 1150 } 1151 else 1152 { 1153 throw new BadItemTypeException("Unable to check differences for the repeater named '" + dataName + "': the given value should be a list containing its entries"); 1154 } 1155 } 1156 else 1157 { 1158 if (value instanceof Map) 1159 { 1160 ModelAwareComposite composite = getComposite(dataName); 1161 if (composite == null) 1162 { 1163 modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup)); 1164 } 1165 else 1166 { 1167 modelItems.addAll(composite.getDifferences(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext)); 1168 } 1169 } 1170 else 1171 { 1172 throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items"); 1173 } 1174 } 1175 1176 return modelItems; 1177 } 1178 1179 /** 1180 * Check if there are differences between the given values and the given element's ones 1181 * @param viewElement the element 1182 * @param values the values to check 1183 * @param synchronizationContext the context of the synchronization 1184 * @return <code>true</code> if there are differences, <code>false</code> otherwise 1185 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 1186 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 1187 */ 1188 protected boolean _hasDifferencesInElement(ViewElement viewElement, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException 1189 { 1190 ElementDefinition definition = viewElement.getDefinition(); 1191 String dataName = definition.getName(); 1192 1193 Object valueFromMap = values.get(dataName); 1194 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 1195 1196 SynchronizableValue syncValue = valueFromMap instanceof SynchronizableValue ? (SynchronizableValue) valueFromMap : new SynchronizableValue(valueFromMap, valueContext.getStatus().orElse(null)); 1197 Object value = syncValue.getValue(valueContext.getStatus()); 1198 1199 if (!(value instanceof UntouchedValue)) 1200 { 1201 Object defaultValue = definition.getDefaultValue(); 1202 if (value == null && synchronizationContext.useDefaultFromModel() && defaultValue != null) 1203 { 1204 if (_checkElementDifferences(viewElement, new SynchronizableValue(defaultValue, valueContext.getStatus().orElse(null)), valueContext)) 1205 { 1206 return true; 1207 } 1208 } 1209 else 1210 { 1211 if (values.containsKey(dataName)) 1212 { 1213 if (_checkElementDifferences(viewElement, syncValue, valueContext)) 1214 { 1215 return true; 1216 } 1217 } 1218 else if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext)) 1219 { 1220 if (__LOGGER.isDebugEnabled()) 1221 { 1222 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1223 __LOGGER.debug("#hasDifferences[{}] differences detected: value will be removed", viewItemPath); 1224 } 1225 return true; 1226 } 1227 } 1228 } 1229 1230 if (_checkStatusDifferences(definition, syncValue, synchronizationContext, values.containsKey(dataName))) 1231 { 1232 if (__LOGGER.isDebugEnabled()) 1233 { 1234 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1235 __LOGGER.debug("#hasDifferences[{}] differences detected: status will change", viewItemPath); 1236 } 1237 return true; 1238 } 1239 1240 // No difference has been found 1241 if (__LOGGER.isDebugEnabled()) 1242 { 1243 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1244 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1245 } 1246 return false; 1247 } 1248 1249 /** 1250 * Check if there are differences between the given value and the given view element's value 1251 * @param viewElement the element 1252 * @param value the value to check 1253 * @param context context of the data to check 1254 * @return <code>true</code> if there are differences, <code>false</code> otherwise 1255 * @throws IllegalArgumentException if the given data name is null or empty 1256 * @throws UndefinedItemPathException if the given data name is not defined by the model 1257 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 1258 */ 1259 protected boolean _checkElementDifferences(ViewElement viewElement, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException 1260 { 1261 ElementDefinition definition = viewElement.getDefinition(); 1262 String dataName = definition.getName(); 1263 1264 if (SynchronizableValue.Mode.REMOVE.equals(value.getMode())) 1265 { 1266 return _checkElementDifferencesInRemoveMode(viewElement, value, context); 1267 } 1268 1269 if (SynchronizableValue.Mode.APPEND.equals(value.getMode()) && isMultiple(dataName)) 1270 { 1271 Object valuesToAppend = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context); 1272 boolean hasValuesToAppend = Array.getLength(valuesToAppend) > 0; 1273 1274 if (__LOGGER.isDebugEnabled()) 1275 { 1276 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1277 if (hasValuesToAppend) 1278 { 1279 __LOGGER.debug("#hasDifferences[{}] differences detected: values {} will be appended", viewItemPath, valuesToAppend); 1280 } 1281 else 1282 { 1283 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1284 } 1285 } 1286 1287 return hasValuesToAppend; 1288 } 1289 1290 boolean hasValueOrEmpty = DataHolderHelper.hasValueOrEmpty(this, dataName, context); 1291 boolean hasEmptyValue = hasValueOrEmpty && !DataHolderHelper.hasValue(this, dataName, context); 1292 Object newValue = value.getValue(context.getStatus()); 1293 1294 if (newValue == null && hasEmptyValue) 1295 { 1296 if (__LOGGER.isDebugEnabled()) 1297 { 1298 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1299 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1300 } 1301 1302 return false; 1303 } 1304 else if (hasValueOrEmpty) 1305 { 1306 Object oldValue = DataHolderHelper.getValue(this, dataName, context); 1307 ElementType type = ((ElementDefinition) getDefinition(dataName)).getType(); 1308 1309 // Check if there are differences between old and new value 1310 boolean hasDiff = type.compareValues(newValue, oldValue).count() > 0; 1311 1312 if (__LOGGER.isDebugEnabled()) 1313 { 1314 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1315 if (hasDiff) 1316 { 1317 __LOGGER.debug("#hasDifferences[{}] differences detected: {} will replace {}", viewItemPath, newValue, oldValue); 1318 } 1319 else 1320 { 1321 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1322 } 1323 } 1324 1325 return hasDiff; 1326 } 1327 else 1328 { 1329 // There was no values at all, one should be set (even empty) 1330 1331 if (__LOGGER.isDebugEnabled()) 1332 { 1333 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1334 __LOGGER.debug("#hasDifferences[{}] differences detected: {} will replace emty value", viewItemPath, newValue); 1335 } 1336 1337 return true; 1338 } 1339 } 1340 1341 /** 1342 * Check if there are differences between the given value and the given view element's value 1343 * @param viewElement the element 1344 * @param value the value to check 1345 * @param context context of the data to check 1346 * @return <code>true</code> if there are differences, <code>false</code> otherwise 1347 * @throws IllegalArgumentException if the given data name is null or empty 1348 * @throws UndefinedItemPathException if the given data name is not defined by the model 1349 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 1350 */ 1351 protected boolean _checkElementDifferencesInRemoveMode(ViewElement viewElement, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException 1352 { 1353 ElementDefinition definition = viewElement.getDefinition(); 1354 String dataName = definition.getName(); 1355 1356 boolean hasValue = DataHolderHelper.hasValueOrEmpty(this, dataName, context); 1357 1358 if (hasValue && isMultiple(dataName)) 1359 { 1360 Object oldValues = DataHolderHelper.getValue(this, dataName, context); 1361 Object valuesToRemove = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context); 1362 ElementType type = ((ElementDefinition) getDefinition(dataName)).getType(); 1363 1364 // Remove the given values from the existent ones 1365 Object newValues = DataHolderHelper.removeValuesInArray(oldValues, valuesToRemove, type); 1366 1367 boolean hasDiff = Array.getLength(oldValues) > Array.getLength(newValues); 1368 1369 if (__LOGGER.isDebugEnabled()) 1370 { 1371 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1372 if (hasDiff) 1373 { 1374 __LOGGER.debug("#hasDifferences[{}] differences detected: some values of {} will be removed from {}", viewItemPath, valuesToRemove, oldValues); 1375 } 1376 else 1377 { 1378 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1379 } 1380 } 1381 1382 return hasDiff; 1383 } 1384 else 1385 { 1386 if (__LOGGER.isDebugEnabled()) 1387 { 1388 String viewItemPath = ViewHelper.getModelViewItemPath(viewElement); 1389 if (hasValue) 1390 { 1391 Object oldValue = DataHolderHelper.getValue(this, dataName, context); 1392 __LOGGER.debug("#hasDifferences[{}] differences detected: value {} will be removed", viewItemPath, oldValue); 1393 } 1394 else 1395 { 1396 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 1397 } 1398 } 1399 1400 return hasValue; 1401 } 1402 } 1403 1404 /** 1405 * Check if the data's status will have to be changed 1406 * @param definition definition of the data 1407 * @param value the value 1408 * @param synchronizationContext the context of the synchronization 1409 * @param doValuesContainData <code>true</code> if the values contain the data, <code>false</code> otherwise 1410 * @return <code>true</code> if the status has changed, <code>false</code> otherwise 1411 */ 1412 protected boolean _checkStatusDifferences(ElementDefinition definition, SynchronizableValue value, SynchronizationContext synchronizationContext, boolean doValuesContainData) 1413 { 1414 if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), definition)) 1415 { 1416 String dataName = definition.getName(); 1417 ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext); 1418 1419 ExternalizableDataStatus oldStatus = null; 1420 if (_repositoryData.hasValue(dataName + STATUS_SUFFIX)) 1421 { 1422 String status = _repositoryData.getString(dataName + STATUS_SUFFIX); 1423 oldStatus = ExternalizableDataStatus.valueOf(status.toUpperCase()); 1424 } 1425 1426 ExternalizableDataStatus newStatus = value.getExternalizableStatus(); 1427 1428 return synchronizationContext.forceStatusIfNotPresent() && oldStatus == null && newStatus == null && doValuesContainData && valueContext.getStatus().isPresent() 1429 || newStatus != null && !newStatus.equals(oldStatus); 1430 } 1431 else 1432 { 1433 return false; 1434 } 1435 } 1436 1437 /** 1438 * Creates an instance of {@link SynchronizationContext} 1439 * @param <T> the type of the {@link SynchronizationContext} 1440 * @return the created {@link SynchronizationContext} 1441 */ 1442 @SuppressWarnings("unchecked") 1443 protected <T extends SynchronizationContext> T _createSynchronizationContextInstance() 1444 { 1445 return (T) SynchronizationContext.newInstance(); 1446 } 1447 1448 private DataContext _addObjectInfoToContext(DataContext context) 1449 { 1450 ModelAwareDataHolder root = getRootDataHolder(); 1451 1452 RepositoryDataContext newContext = RepositoryDataContext.newInstance(context); 1453 if (root instanceof DataAwareAmetysObject ametysObject) 1454 { 1455 newContext.withObject(ametysObject); 1456 } 1457 1458 return newContext; 1459 } 1460 1461 public RepositoryData getRepositoryData() 1462 { 1463 return _repositoryData; 1464 } 1465 1466 public Optional<? extends IndexableDataHolder> getParentDataHolder() 1467 { 1468 return _parent; 1469 } 1470 1471 public IndexableDataHolder getRootDataHolder() 1472 { 1473 return _root; 1474 } 1475 1476 /** 1477 * Check definition for data path 1478 * @param dataPath the data path 1479 * @param errorMsg the error message to throw 1480 */ 1481 protected void _checkDefinition(String dataPath, String errorMsg) 1482 { 1483 _checkDefinition(dataPath, false, errorMsg); 1484 } 1485 1486 /** 1487 * Check definition for data path 1488 * @param dataPath the data path 1489 * @param checkStatusAvailable <code>true</code> to check if the definition supports externalizable data status 1490 * @param errorMsg the error message to throw 1491 */ 1492 protected void _checkDefinition(String dataPath, boolean checkStatusAvailable, String errorMsg) 1493 { 1494 // Check if the model exists 1495 if (_itemContainers.isEmpty()) 1496 { 1497 throw new UndefinedItemPathException(errorMsg + " No model is defined for this object [" + getRootDataHolder() + "]"); 1498 } 1499 1500 // Check that there is an item at the given path 1501 if (!hasDefinition(dataPath)) 1502 { 1503 throw new UndefinedItemPathException(errorMsg + " There is no such item defined by the model."); 1504 } 1505 1506 // Externalizable data status is not available for properties 1507 if (checkStatusAvailable && getDefinition(dataPath) instanceof Property) 1508 { 1509 throw new UndefinedItemPathException(errorMsg + " A property can't have an externalizable status."); 1510 } 1511 } 1512}