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