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.xml.sax.ContentHandler; 030import org.xml.sax.SAXException; 031 032import org.ametys.cms.data.ContentValue; 033import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject; 034import org.ametys.cms.data.holder.IndexableDataHolder; 035import org.ametys.cms.data.holder.group.IndexableComposite; 036import org.ametys.cms.data.holder.group.IndexableRepeater; 037import org.ametys.cms.data.holder.group.impl.DefaultModelAwareComposite; 038import org.ametys.cms.data.holder.group.impl.DefaultModelAwareRepeater; 039import org.ametys.cms.model.ContentElementDefinition; 040import org.ametys.cms.model.properties.Property; 041import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 042import org.ametys.core.util.DateUtils; 043import org.ametys.plugins.repository.AmetysObject; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.plugins.repository.data.DataComment; 046import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 047import org.ametys.plugins.repository.data.holder.DataHolder; 048import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 049import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 051import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 052import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 053import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 054import org.ametys.plugins.repository.data.type.RepositoryElementType; 055import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType; 056import org.ametys.plugins.repository.data.type.RepositoryModelItemType; 057import org.ametys.plugins.repository.model.CompositeDefinition; 058import org.ametys.plugins.repository.model.RepeaterDefinition; 059import org.ametys.runtime.model.ElementDefinition; 060import org.ametys.runtime.model.ModelHelper; 061import org.ametys.runtime.model.ModelItem; 062import org.ametys.runtime.model.ModelItemContainer; 063import org.ametys.runtime.model.ViewItemAccessor; 064import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 065import org.ametys.runtime.model.exception.BadItemTypeException; 066import org.ametys.runtime.model.exception.UndefinedItemPathException; 067import org.ametys.runtime.model.type.DataContext; 068import org.ametys.runtime.model.type.ModelItemType; 069 070/** 071 * Default implementation for data holder with model 072 */ 073public class DefaultModelAwareDataHolder implements IndexableDataHolder 074{ 075 /** Repository data to use to store data in the repository */ 076 protected RepositoryData _repositoryData; 077 078 /** Parent of the current {@link DataHolder} */ 079 protected Optional<? extends IndexableDataHolder> _parent; 080 081 /** Root {@link DataHolder} */ 082 protected IndexableDataHolder _root; 083 084 /** Model containers to use to get information about definitions */ 085 protected Collection<? extends ModelItemContainer> _itemContainers; 086 087 /** 088 * Creates a default model aware data holder 089 * @param repositoryData the repository data to use 090 * @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. 091 */ 092 public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer... itemContainers) 093 { 094 this(repositoryData, Optional.empty(), Optional.empty(), Arrays.asList(itemContainers)); 095 } 096 097 /** 098 * Creates a default model aware data holder 099 * @param repositoryData the repository data to use 100 * @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. 101 */ 102 public DefaultModelAwareDataHolder(RepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers) 103 { 104 this(repositoryData, Optional.empty(), Optional.empty(), itemContainers); 105 } 106 107 /** 108 * Creates a default model aware data holder 109 * @param repositoryData the repository data to use 110 * @param parent the optional parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 111 * @param root the root {@link DataHolder} 112 * @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. 113 */ 114 public DefaultModelAwareDataHolder(RepositoryData repositoryData, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root, ModelItemContainer... itemContainers) 115 { 116 this(repositoryData, parent, root, Arrays.asList(itemContainers)); 117 } 118 119 /** 120 * Creates a default model aware data holder 121 * @param repositoryData the repository data to use 122 * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder} 123 * @param root the root {@link DataHolder} 124 * @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. 125 */ 126 public DefaultModelAwareDataHolder(RepositoryData repositoryData, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root, Collection<? extends ModelItemContainer> itemContainers) 127 { 128 _repositoryData = repositoryData; 129 _itemContainers = itemContainers; 130 _ensureNonNullItemContainers(); 131 132 _parent = parent; 133 _root = root.map(IndexableDataHolder.class::cast) 134 .or(() -> _parent.map(IndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root 135 .orElse(this); // if no root or parent is specified, the root is the current DataHolder 136 } 137 138 private void _ensureNonNullItemContainers() 139 { 140 if (_itemContainers.contains(null)) 141 { 142 throw new NullPointerException(String.format("Invalid item containers for creating DefaultModelAwareDataHolder, one of them is null: %s", _itemContainers)); 143 } 144 } 145 146 public IndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 147 { 148 Object value = getValue(compositePath); 149 return _getCompositeFromValue(value, compositePath); 150 } 151 152 public IndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 153 { 154 Object value = getLocalValue(compositePath); 155 return _getCompositeFromValue(value, compositePath); 156 } 157 158 public IndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 159 { 160 Object value = getExternalValue(compositePath); 161 return _getCompositeFromValue(value, compositePath); 162 } 163 164 private IndexableComposite _getCompositeFromValue(Object value, String compositePath) 165 { 166 if (value == null) 167 { 168 return null; 169 } 170 else if (value instanceof IndexableComposite composite) 171 { 172 return composite; 173 } 174 else 175 { 176 throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite."); 177 } 178 } 179 180 public IndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 181 { 182 Object value = getValue(repeaterPath); 183 return _getRepeaterFromValue(value, repeaterPath); 184 } 185 186 public IndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 187 { 188 Object value = getLocalValue(repeaterPath); 189 return _getRepeaterFromValue(value, repeaterPath); 190 } 191 192 public IndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 193 { 194 Object value = getExternalValue(repeaterPath); 195 return _getRepeaterFromValue(value, repeaterPath); 196 } 197 198 private IndexableRepeater _getRepeaterFromValue(Object value, String repeaterPath) 199 { 200 if (value == null) 201 { 202 return null; 203 } 204 else if (value instanceof IndexableRepeater repeater) 205 { 206 return repeater; 207 } 208 else 209 { 210 throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater."); 211 } 212 } 213 214 public <T> T getValue(String dataPath, boolean allowMultiValuedPathSegments) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 215 { 216 return _getValue(dataPath, allowMultiValuedPathSegments, Optional.empty()); 217 } 218 219 public <T> T getLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 220 { 221 return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.LOCAL)); 222 } 223 224 public <T> T getExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 225 { 226 return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.EXTERNAL)); 227 } 228 229 private <T> T _getValue(String dataPath, boolean allowMultiValuedPathSegments, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 230 { 231 _checkDefinition(dataPath, status.isPresent(), "Unable to retrieve the value at path '" + dataPath + "'."); 232 233 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 234 235 if (pathSegments == null || pathSegments.length < 1) 236 { 237 throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty."); 238 } 239 else if (pathSegments.length == 1) 240 { 241 // Simple path => get the value 242 ModelItem modelItem = getDefinition(dataPath); 243 String dataName = _getFinalDataName(dataPath, status); 244 245 if (modelItem instanceof Property property) 246 { 247 return _getPropertyValue(property); 248 } 249 else if (modelItem instanceof ElementDefinition elementDefinition) 250 { 251 return _getElementValue(elementDefinition, dataName); 252 } 253 else 254 { 255 return _getGroupValue(modelItem, dataName); 256 } 257 } 258 else 259 { 260 if (isMultiple(pathSegments[0])) 261 { 262 if (allowMultiValuedPathSegments) 263 { 264 return _getMultipleValues(dataPath); 265 } 266 else 267 { 268 // Multiple items are allowed only at the last segment of the data path 269 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."); 270 } 271 } 272 else 273 { 274 // Path where first part is a data holder 275 ModelAwareDataHolder dataHolder = getValue(pathSegments[0]); 276 if (dataHolder == null) 277 { 278 return null; 279 } 280 else 281 { 282 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 283 return status.isPresent() 284 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 285 ? dataHolder.getExternalValue(subDataPath) 286 : dataHolder.getLocalValue(subDataPath) 287 : dataHolder.getValue(subDataPath, allowMultiValuedPathSegments); 288 } 289 } 290 } 291 } 292 293 public ExternalizableDataStatus getStatus(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadDataPathCardinalityException 294 { 295 _checkDefinition(dataPath, true, "Unable to retrieve the value at path '" + dataPath + "'."); 296 297 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 298 299 if (pathSegments == null || pathSegments.length < 1) 300 { 301 throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty."); 302 } 303 else if (pathSegments.length == 1) 304 { 305 if (_repositoryData.hasValue(dataPath + STATUS_SUFFIX)) 306 { 307 String status = _repositoryData.getString(dataPath + STATUS_SUFFIX); 308 return ExternalizableDataStatus.valueOf(status.toUpperCase()); 309 } 310 else 311 { 312 return ExternalizableDataStatus.LOCAL; 313 } 314 } 315 else 316 { 317 if (isMultiple(pathSegments[0])) 318 { 319 // Multiple items are allowed only at the last segment of the data path 320 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."); 321 } 322 else 323 { 324 // Path where first part is a data holder 325 ModelAwareDataHolder dataHolder = getValue(pathSegments[0]); 326 if (dataHolder == null) 327 { 328 return null; 329 } 330 else 331 { 332 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 333 return dataHolder.getStatus(subDataPath); 334 } 335 } 336 } 337 } 338 339 @SuppressWarnings("unchecked") 340 private <T> T _getPropertyValue(Property property) 341 { 342 return getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametyObject ? (T) property.getValue(ametyObject) : null; 343 } 344 345 @SuppressWarnings("unchecked") 346 private <T> T _getElementValue(ElementDefinition definition, String dataName) 347 { 348 RepositoryElementType type = (RepositoryElementType) definition.getType(); 349 Object value = type.read(_repositoryData, dataName); 350 351 if (definition.isMultiple() && type.getManagedClass().isInstance(value)) 352 { 353 // The value is single but should be an array. Create the array with the single value 354 T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1); 355 Array.set(arrayValue, 0, value); 356 return arrayValue; 357 } 358 else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value)) 359 { 360 // The value is multiple but should be single. Retrieve the first value of the array 361 return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null; 362 } 363 else 364 { 365 return (T) value; 366 } 367 } 368 369 @SuppressWarnings("unchecked") 370 private <T> T _getGroupValue(ModelItem modelItem, String dataName) 371 { 372 if (modelItem instanceof RepeaterDefinition) 373 { 374 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName); 375 if (repeaterNameAndEntryPosition != null) 376 { 377 return (T) DataHolderHelper.getRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 378 } 379 else 380 { 381 return (T) _getRepeater(dataName, (RepeaterDefinition) modelItem); 382 } 383 } 384 else 385 { 386 return (T) _getComposite(dataName, (CompositeDefinition) modelItem); 387 } 388 } 389 390 @SuppressWarnings("unchecked") 391 private <T> T _getMultipleValues(String dataPath) 392 { 393 String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 394 String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 395 Class managedClass = _getManagedClass(this, dataPath); 396 397 Object segmentValue = getValue(pathSegments[0]); 398 if (segmentValue == null) 399 { 400 return (T) Array.newInstance(managedClass, 0); 401 } 402 403 if (segmentValue instanceof ModelAwareRepeater) 404 { 405 ModelAwareRepeater repeater = (ModelAwareRepeater) segmentValue; 406 return DataHolderHelper.aggregateMultipleValues(repeater.getEntries(), subDataPath, managedClass); 407 } 408 else 409 { 410 ModelAwareDataHolder[] dataHolders = (ModelAwareDataHolder[]) segmentValue; 411 return DataHolderHelper.aggregateMultipleValues(Arrays.asList(dataHolders), subDataPath, managedClass); 412 } 413 } 414 415 private Class _getManagedClass(ModelAwareDataHolder dataHolder, String dataPath) 416 { 417 Class managedClass; 418 ModelItem modelItem = dataHolder.getDefinition(dataPath); 419 if (modelItem instanceof ElementDefinition) 420 { 421 managedClass = ((ElementDefinition) modelItem).getType().getManagedClass(); 422 } 423 else 424 { 425 if (modelItem instanceof RepeaterDefinition) 426 { 427 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 428 managedClass = repeaterNameAndEntryPosition != null ? _getRepeaterEntryClass() : _getRepeaterClass(); 429 } 430 else 431 { 432 managedClass = _getCompositeClass(); 433 } 434 } 435 return managedClass; 436 } 437 438 /** 439 * Retrieves the class of the managed repeater entries 440 * @return the class of the managed repeater entries 441 */ 442 protected Class _getRepeaterEntryClass() 443 { 444 return ModelAwareRepeaterEntry.class; 445 } 446 447 /** 448 * Retrieves the class of the managed repeaters 449 * @return the class of the managed repeaters 450 */ 451 protected Class _getRepeaterClass() 452 { 453 return ModelAwareRepeater.class; 454 } 455 456 /** 457 * Retrieves the class of the managed composites 458 * @return the class of the managed composites 459 */ 460 protected Class _getCompositeClass() 461 { 462 return ModelAwareComposite.class; 463 } 464 465 public <T> T getValue(String dataPath, boolean useDefaultFromModel, T defaultValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 466 { 467 _checkDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'."); 468 469 if (hasValue(dataPath)) 470 { 471 return getValue(dataPath); 472 } 473 474 if (useDefaultFromModel) 475 { 476 ModelItem modelItem = getDefinition(dataPath); 477 478 if (modelItem instanceof ElementDefinition) 479 { 480 @SuppressWarnings("unchecked") 481 T defaultFromModel = (T) ((ElementDefinition) modelItem).getDefaultValue(); 482 if (defaultFromModel != null) 483 { 484 return defaultFromModel; 485 } 486 } 487 } 488 489 return defaultValue; 490 } 491 492 /** 493 * Retrieves the composite with the given name 494 * @param name name of the composite to retrieve 495 * @param compositeDefinition the definition of the composite to retrieve 496 * @return the composite 497 * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite 498 */ 499 protected ModelAwareComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException 500 { 501 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType(); 502 RepositoryData compositeRepositoryData = type.read(_repositoryData, name); 503 504 if (compositeRepositoryData != null) 505 { 506 return new DefaultModelAwareComposite(compositeRepositoryData, this, _root, compositeDefinition); 507 } 508 else 509 { 510 return null; 511 } 512 } 513 514 /** 515 * Retrieves the repeater with the given name 516 * @param name name of the repeater to retrieve 517 * @param repeaterDefinition the definition of the repeater to retrieve 518 * @return the repeater 519 * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater 520 */ 521 protected ModelAwareRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException 522 { 523 RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType(); 524 RepositoryData repeaterRepositoryData = type.read(_repositoryData, name); 525 526 if (repeaterRepositoryData != null) 527 { 528 return new DefaultModelAwareRepeater(repeaterRepositoryData, this, _root, repeaterDefinition); 529 } 530 else 531 { 532 return null; 533 } 534 } 535 536 public List<DataComment> getComments(String dataName) throws IllegalArgumentException, UndefinedItemPathException 537 { 538 _checkDefinition(dataName, "Unable to retrieve the comments of the data named '" + dataName + "'."); 539 540 List<DataComment> comments = new ArrayList<>(); 541 542 RepositoryData commentsRepositoryData = _repositoryData.getRepositoryData(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 543 544 for (String commentId : commentsRepositoryData.getDataNames(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)) 545 { 546 RepositoryData commentRepositoryData = commentsRepositoryData.getRepositoryData(commentId, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 547 548 String content = commentRepositoryData.getString("comment"); 549 String author = commentRepositoryData.getString("author"); 550 ZonedDateTime date = DateUtils.asZonedDateTime(commentRepositoryData.getDate("date")); 551 552 DataComment comment = new DataComment(content, date, author); 553 comments.add(comment); 554 } 555 556 return comments; 557 } 558 559 public boolean hasValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 560 { 561 return _hasValue(dataPath, Optional.empty()); 562 } 563 564 public boolean hasLocalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 565 { 566 return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.LOCAL)); 567 } 568 569 public boolean hasExternalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 570 { 571 return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL)); 572 } 573 574 @SuppressWarnings("unchecked") 575 private boolean _hasValue(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException 576 { 577 if (!hasDefinition(dataPath)) 578 { 579 return false; 580 } 581 582 if (StringUtils.isEmpty(dataPath)) 583 { 584 throw new IllegalArgumentException("Unable to check if there is a non empty value at the given path. This path is empty."); 585 } 586 else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 587 { 588 if (DataHolderHelper.isRepeaterEntryPath(dataPath)) 589 { 590 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 591 return DataHolderHelper.hasNonEmptyRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 592 } 593 else 594 { 595 if (getDefinition(dataPath) instanceof Property property) 596 { 597 if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 598 { 599 return property.getValue(ametysObject) != null; 600 } 601 else 602 { 603 return false; 604 } 605 } 606 else 607 { 608 RepositoryModelItemType type = getType(dataPath); 609 String dataName = _getFinalDataName(dataPath, status); 610 611 try 612 { 613 return type.hasNonEmptyValue(_repositoryData, dataName); 614 } 615 catch (BadItemTypeException e) 616 { 617 return false; 618 } 619 } 620 } 621 } 622 else 623 { 624 String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 625 626 // Multiple items are allowed only at the last segment of the data path 627 if (isMultiple(parentPath)) 628 { 629 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."); 630 } 631 632 try 633 { 634 ModelAwareDataHolder parent = getValue(parentPath); 635 if (parent == null) 636 { 637 return false; 638 } 639 else 640 { 641 String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 642 return status.isPresent() 643 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 644 ? parent.hasExternalValue(childName) 645 : parent.hasLocalValue(childName) 646 : parent.hasValue(childName); 647 } 648 } 649 catch (BadItemTypeException e) 650 { 651 return false; 652 } 653 } 654 } 655 656 public boolean hasValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 657 { 658 return _hasValueOrEmpty(dataPath, Optional.empty()); 659 } 660 661 public boolean hasLocalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 662 { 663 return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.LOCAL)); 664 } 665 666 public boolean hasExternalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException 667 { 668 return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL)); 669 } 670 671 @SuppressWarnings("unchecked") 672 private boolean _hasValueOrEmpty(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException 673 { 674 if (!hasDefinition(dataPath)) 675 { 676 return false; 677 } 678 679 if (StringUtils.isEmpty(dataPath)) 680 { 681 throw new IllegalArgumentException("Unable to check if there is a value at the given path. This path is empty."); 682 } 683 else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 684 { 685 if (DataHolderHelper.isRepeaterEntryPath(dataPath)) 686 { 687 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath); 688 return DataHolderHelper.hasRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight()); 689 } 690 else 691 { 692 if (getDefinition(dataPath) instanceof Property property) 693 { 694 if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 695 { 696 return property.getValue(ametysObject) != null; 697 } 698 else 699 { 700 return false; 701 } 702 } 703 else 704 { 705 RepositoryModelItemType type = getType(dataPath); 706 String dataName = _getFinalDataName(dataPath, status); 707 return type.hasValue(_repositoryData, dataName); 708 } 709 } 710 } 711 else 712 { 713 String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 714 715 // Multiple items are allowed only at the last segment of the data path 716 if (isMultiple(parentPath)) 717 { 718 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."); 719 } 720 721 try 722 { 723 ModelAwareDataHolder parent = getValue(parentPath); 724 if (parent == null) 725 { 726 return false; 727 } 728 else 729 { 730 String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 731 return status.isPresent() 732 ? ExternalizableDataStatus.EXTERNAL.equals(status.get()) 733 ? parent.hasExternalValueOrEmpty(childName) 734 : parent.hasLocalValueOrEmpty(childName) 735 : parent.hasValueOrEmpty(childName); 736 } 737 } 738 catch (BadItemTypeException e) 739 { 740 return false; 741 } 742 } 743 } 744 745 /** 746 * Retrieves the name of the data according to the given status 747 * @param dataName the name of the data 748 * @param status the status 749 * @return the final name of the data 750 */ 751 protected String _getFinalDataName(String dataName, Optional<ExternalizableDataStatus> status) 752 { 753 if (status.isPresent() && getStatus(dataName) != status.get()) 754 { 755 return dataName + ALTERNATIVE_SUFFIX; 756 } 757 758 return dataName; 759 } 760 761 public boolean hasComments(String dataName) throws IllegalArgumentException, UndefinedItemPathException 762 { 763 _checkDefinition(dataName, "Unable to check if there are comments on the data named '" + dataName + "'."); 764 765 return _repositoryData.hasValue(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 766 } 767 768 public Collection< ? extends ModelItemContainer> getModel() 769 { 770 return _itemContainers; 771 } 772 773 public ModelItem getDefinition(String path) throws IllegalArgumentException, UndefinedItemPathException 774 { 775 try 776 { 777 ModelItem definition = IndexableDataHolder.super.getDefinition(path); 778 779 // A definition has been found, ok 780 return definition; 781 } 782 catch (UndefinedItemPathException e) 783 { 784 // Look for system properties 785 if (StringUtils.contains(path, ModelItem.ITEM_PATH_SEPARATOR)) 786 { 787 String parentDataPath = StringUtils.substringBeforeLast(path, ModelItem.ITEM_PATH_SEPARATOR); 788 ModelItem modelItem = getDefinition(parentDataPath); 789 if (modelItem instanceof ContentElementDefinition) 790 { 791 SystemPropertyExtensionPoint contentSystemPropertyExtensionPoint = IndexableDataHolderHelper.getContentSystemPropertyExtensionPoint(); 792 String propertyName = StringUtils.substringAfterLast(path, ModelItem.ITEM_PATH_SEPARATOR); 793 if (contentSystemPropertyExtensionPoint.hasExtension(propertyName)) 794 { 795 return contentSystemPropertyExtensionPoint.getExtension(propertyName); 796 } 797 } 798 } 799 else if (getParentDataHolder().isEmpty() 800 && getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject 801 && ametysObject.getSystemPropertyExtensionPoint().isPresent()) 802 { 803 SystemPropertyExtensionPoint systemPropertyExtensionPoint = ametysObject.getSystemPropertyExtensionPoint().get(); 804 if (systemPropertyExtensionPoint.hasExtension(path)) 805 { 806 return systemPropertyExtensionPoint.getExtension(path); 807 } 808 } 809 810 // No system property has been found, throw the UndefinedItemPathException 811 throw e; 812 } 813 } 814 815 public Collection<String> getDataNames() 816 { 817 return ModelHelper.getModelItems(getModel()) 818 .stream() 819 .map(ModelItem::getName) 820 .filter(this::hasValueOrEmpty) 821 .toList(); 822 } 823 824 @SuppressWarnings("unchecked") 825 public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException 826 { 827 _checkDefinition(dataPath, "Unable to generate SAX events for the data at path '" + dataPath + "'."); 828 829 ModelAwareDataHolder root = getRootDataHolder(); 830 DataContext newContext = context.cloneContext(); 831 if (root instanceof AmetysObject ametysObject) 832 { 833 newContext.withObjectId(ametysObject.getId()); 834 } 835 836 if (hasValue(dataPath) 837 || newContext.renderEmptyValues() && hasValueOrEmpty(dataPath)) 838 { 839 ModelItem modelItem = getDefinition(dataPath); 840 if (modelItem instanceof Property property) 841 { 842 ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath); 843 property.valueToSAX(contentHandler, ametysObject, newContext); 844 } 845 else 846 { 847 ModelItemType type = modelItem.getType(); 848 Object value = getValue(dataPath); 849 850 type.valueToSAX(contentHandler, modelItem.getName(), value, newContext.cloneContext().withDataPath(dataPath)); 851 } 852 } 853 } 854 855 public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 856 { 857 ModelAwareDataHolder root = getRootDataHolder(); 858 859 DataContext newContext = context.cloneContext(); 860 if (root instanceof AmetysObject ametysObject) 861 { 862 newContext.withObjectId(ametysObject.getId()); 863 } 864 865 IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, false); 866 } 867 868 public void dataToSAXForEdition(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 869 { 870 ModelAwareDataHolder root = getRootDataHolder(); 871 872 DataContext newContext = context.cloneContext(); 873 if (root instanceof AmetysObject ametysObject) 874 { 875 newContext.withObjectId(ametysObject.getId()); 876 } 877 878 IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, true); 879 } 880 881 @SuppressWarnings("unchecked") 882 public Object dataToJSON(String dataPath, DataContext context) 883 { 884 _checkDefinition(dataPath, "Unable to convert the data at path '" + dataPath + "' to JSON."); 885 886 ModelAwareDataHolder root = getRootDataHolder(); 887 DataContext newContext = context.cloneContext(); 888 if (root instanceof AmetysObject ametysObject) 889 { 890 newContext.withObjectId(ametysObject.getId()); 891 } 892 893 if (hasValue(dataPath) 894 || newContext.renderEmptyValues() && hasValueOrEmpty(dataPath)) 895 { 896 ModelItem modelItem = getDefinition(dataPath); 897 if (modelItem instanceof Property property) 898 { 899 ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath); 900 return property.valueToJSON(ametysObject, newContext); 901 } 902 else 903 { 904 ModelItemType type = modelItem.getType(); 905 Object value = getValue(dataPath); 906 907 return type.valueToJSONForClient(value, newContext.cloneContext().withDataPath(dataPath)); 908 } 909 } 910 else 911 { 912 return null; 913 } 914 } 915 916 /** 917 * Retrieves the ametys object containing the property at the given path 918 * @param dataPath the path of the property 919 * @return the ametys object containing the property 920 * @throws UndefinedItemPathException if the given path does not represent a property 921 */ 922 protected ModelAwareDataAwareAmetysObject _getPropertysAmetysObject(String dataPath) throws UndefinedItemPathException 923 { 924 if (StringUtils.contains(dataPath, ModelItem.ITEM_PATH_SEPARATOR)) 925 { 926 String parentDataPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 927 ContentValue value = getValue(parentDataPath); 928 return value.getContent(); 929 } 930 else if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject) 931 { 932 return ametysObject; 933 } 934 else 935 { 936 throw new UndefinedItemPathException("There is no property at path '" + dataPath + "'"); 937 } 938 } 939 940 public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 941 { 942 ModelAwareDataHolder root = getRootDataHolder(); 943 944 DataContext newContext = context.cloneContext(); 945 if (root instanceof AmetysObject ametysObject) 946 { 947 newContext.withObjectId(ametysObject.getId()); 948 } 949 950 return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, false); 951 } 952 953 public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 954 { 955 ModelAwareDataHolder root = getRootDataHolder(); 956 957 DataContext newContext = context.cloneContext(); 958 if (root instanceof AmetysObject ametysObject) 959 { 960 newContext.withObjectId(ametysObject.getId()); 961 } 962 963 return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, true); 964 } 965 966 public Map<String, Object> dataToMap(ViewItemAccessor viewItemAccessor, DataContext context) 967 { 968 ModelAwareDataHolder root = getRootDataHolder(); 969 970 DataContext newContext = context.cloneContext(); 971 if (root instanceof AmetysObject) 972 { 973 newContext.withObjectId(((AmetysObject) root).getId()); 974 } 975 976 return IndexableDataHolderHelper.dataToMap(this, viewItemAccessor, newContext); 977 } 978 979 public RepositoryData getRepositoryData() 980 { 981 return _repositoryData; 982 } 983 984 public Optional<? extends IndexableDataHolder> getParentDataHolder() 985 { 986 return _parent; 987 } 988 989 public IndexableDataHolder getRootDataHolder() 990 { 991 return _root; 992 } 993 994 /** 995 * Check definition for data path 996 * @param dataPath the data path 997 * @param errorMsg the error message to throw 998 */ 999 protected void _checkDefinition(String dataPath, String errorMsg) 1000 { 1001 _checkDefinition(dataPath, false, errorMsg); 1002 } 1003 1004 /** 1005 * Check definition for data path 1006 * @param dataPath the data path 1007 * @param checkStatusAvailable <code>true</code> to check if the definition supports externalizable data status 1008 * @param errorMsg the error message to throw 1009 */ 1010 protected void _checkDefinition(String dataPath, boolean checkStatusAvailable, String errorMsg) 1011 { 1012 // Check if the model exists 1013 if (_itemContainers.isEmpty()) 1014 { 1015 throw new UndefinedItemPathException(errorMsg + " No model is defined for this object [" + getRootDataHolder() + "]"); 1016 } 1017 1018 // Check that there is an item at the given path 1019 if (!hasDefinition(dataPath)) 1020 { 1021 throw new UndefinedItemPathException(errorMsg + " There is no such item defined by the model."); 1022 } 1023 1024 // Externalizable data status is not available for properties 1025 if (checkStatusAvailable && getDefinition(dataPath) instanceof Property) 1026 { 1027 throw new UndefinedItemPathException(errorMsg + " A property can't have an externalizable status."); 1028 } 1029 } 1030}