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