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