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