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.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026 027import org.apache.avalon.framework.activity.Disposable; 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.xml.AttributesImpl; 033import org.apache.cocoon.xml.XMLUtils; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.solr.common.SolrInputDocument; 036import org.xml.sax.ContentHandler; 037import org.xml.sax.SAXException; 038 039import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject; 040import org.ametys.cms.data.holder.IndexableDataHolder; 041import org.ametys.cms.data.holder.group.IndexableComposite; 042import org.ametys.cms.data.holder.group.IndexableRepeater; 043import org.ametys.cms.data.type.indexing.IndexableElementType; 044import org.ametys.cms.model.CMSDataContext; 045import org.ametys.cms.model.properties.Property; 046import org.ametys.cms.search.model.IndexationAwareElementDefinition; 047import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 048import org.ametys.core.util.JSONUtils; 049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 050import org.ametys.plugins.repository.data.holder.DataHolder; 051import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 052import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 053import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 054import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 055import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 056import org.ametys.plugins.repository.data.type.RepositoryElementType; 057import org.ametys.plugins.repository.model.CompositeDefinition; 058import org.ametys.plugins.repository.model.RepeaterDefinition; 059import org.ametys.plugins.repository.model.RepositoryDataContext; 060import org.ametys.plugins.repository.model.ViewHelper; 061import org.ametys.runtime.model.DefinitionContext; 062import org.ametys.runtime.model.ElementDefinition; 063import org.ametys.runtime.model.ModelItem; 064import org.ametys.runtime.model.ModelViewItem; 065import org.ametys.runtime.model.View; 066import org.ametys.runtime.model.ViewItem; 067import org.ametys.runtime.model.ViewItemAccessor; 068import org.ametys.runtime.model.disableconditions.DefaultDisableConditionsEvaluator; 069import org.ametys.runtime.model.disableconditions.DisableCondition; 070import org.ametys.runtime.model.disableconditions.DisableConditions; 071import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator; 072import org.ametys.runtime.model.exception.BadItemTypeException; 073import org.ametys.runtime.model.exception.UndefinedItemPathException; 074import org.ametys.runtime.model.type.DataContext; 075import org.ametys.runtime.model.type.ElementType; 076import org.ametys.runtime.model.type.ModelItemType; 077 078/** 079 * Helper for implementations of indexable data holder 080 */ 081public final class IndexableDataHolderHelper implements Component, Serviceable, Disposable 082{ 083 /** The constant to use to send values of external disable conditions */ 084 public static final String EXTERNAL_DISABLE_CONDITIONS_VALUES = "__externalDisableConditionsValues"; 085 086 private static JSONUtils _jsonUtils; 087 private static SystemPropertyExtensionPoint _contentSystemPropertyExtentionPoint; 088 private static DisableConditionsEvaluator _disableConditionsEvaluator; 089 090 @Override 091 public void service(ServiceManager manager) throws ServiceException 092 { 093 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 094 _contentSystemPropertyExtentionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 095 _disableConditionsEvaluator = (DisableConditionsEvaluator) manager.lookup(DefaultDisableConditionsEvaluator.ROLE); 096 } 097 098 public void dispose() 099 { 100 _jsonUtils = null; 101 _contentSystemPropertyExtentionPoint = null; 102 } 103 104 /** 105 * Indexes data of the given {@link IndexableDataHolder} 106 * @param dataHolder the {@link IndexableDataHolder} to index 107 * @param viewItemAccessor the view item accessor to explore 108 * @param document the solr document representing this {@link IndexableDataHolder} 109 * @param rootDocument the solr document of the root object. 110 * @param solrFieldPrefix the prefix of the solr field 111 * @param context The context of the data to index 112 * @return additional solr documents that may have been created (ex: repeater entries) 113 * @throws BadItemTypeException if the saxed value's type does not matches the stored data 114 */ 115 public static List<SolrInputDocument> indexData(IndexableDataHolder dataHolder, ViewItemAccessor viewItemAccessor, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException 116 { 117 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 118 119 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 120 121 for (ViewItem viewItem : mergedViewItemAccessor.getViewItems()) 122 { 123 additionalDocuments.addAll(_indexDataFromViewItem(dataHolder, viewItem, document, rootDocument, solrFieldPrefix, context)); 124 } 125 126 return additionalDocuments; 127 } 128 129 @SuppressWarnings("unchecked") 130 private static List<SolrInputDocument> _indexDataFromViewItem(IndexableDataHolder dataHolder, ViewItem viewItem, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) 131 { 132 if (viewItem instanceof ModelViewItem modelViewItem) 133 { 134 ModelItem modelItem = modelViewItem.getDefinition(); 135 String dataName = modelItem.getName(); 136 CMSDataContext newContext = context.cloneContext() 137 .addSegmentToDataPath(dataName) 138 .withModelItem(modelItem) 139 .withViewItem(viewItem); 140 141 if (modelItem instanceof IndexationAwareElementDefinition indexationAwareElementDefinition) 142 { 143 indexationAwareElementDefinition.indexValue(document, getAmetysObjectFromContext(newContext), newContext); 144 return Collections.EMPTY_LIST; 145 } 146 else if (!(modelItem instanceof Property) && dataHolder.hasValue(dataName)) 147 { 148 if (modelItem instanceof ElementDefinition definition) 149 { 150 ElementType type = definition.getType(); 151 152 if (type instanceof IndexableElementType indexingType) 153 { 154 Object value = dataHolder.getValue(dataName); 155 156 String solrFieldName = solrFieldPrefix + dataName; 157 indexingType.indexValue(document, rootDocument, solrFieldName, value, newContext); 158 } 159 160 return Collections.EMPTY_LIST; 161 } 162 else if (modelItem instanceof CompositeDefinition) 163 { 164 IndexableComposite composite = dataHolder.getValue(dataName); 165 String newSolrFieldPrefix = solrFieldPrefix + dataName + ModelItem.ITEM_PATH_SEPARATOR; 166 167 return indexData(composite, (ViewItemAccessor) viewItem, document, rootDocument, newSolrFieldPrefix, newContext); 168 } 169 else if (modelItem instanceof RepeaterDefinition) 170 { 171 IndexableRepeater repeater = dataHolder.getValue(dataName); 172 return repeater.indexData(document, rootDocument, solrFieldPrefix, newContext); 173 } 174 } 175 } 176 else if (viewItem instanceof ViewItemAccessor accessor) 177 { 178 return indexData(dataHolder, accessor, document, rootDocument, solrFieldPrefix, context); 179 } 180 return Collections.EMPTY_LIST; 181 } 182 183 /** 184 * Generates SAX events for the data in the given view in the given {@link DataHolder} 185 * @param dataHolder the {@link ModelAwareDataHolder} to SAX 186 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 187 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events 188 * @param context The context of the data to SAX 189 * @param isEdition <code>true</code> if SAX events are generated in edition mode, <code>false</code> otherwise 190 * @throws SAXException if an error occurs during the SAX events generation 191 * @throws BadItemTypeException if the saxed value's type does not matches the stored data 192 * @throws UndefinedItemPathException if an item in the view is not part of the model 193 */ 194 public static void dataToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context, boolean isEdition) throws SAXException, BadItemTypeException, UndefinedItemPathException 195 { 196 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 197 198 for (ViewItem viewItem : mergedViewItemAccessor.getViewItems()) 199 { 200 if (viewItem instanceof ModelViewItem modelViewItem) 201 { 202 ModelItem modelItem = modelViewItem.getDefinition(); 203 String dataName = modelItem.getName(); 204 DataContext newContext = context.cloneContext() 205 .addSegmentToDataPath(dataName) 206 .withViewItem(viewItem) 207 .withModelItem(modelItem); 208 209 if (renderValue(dataHolder, dataName, newContext, isEdition)) 210 { 211 if (modelItem instanceof Property property) 212 { 213 if (hasValue(dataHolder, dataName, newContext)) 214 { 215 _propertyToSAX(property, contentHandler, newContext, isEdition); 216 } 217 } 218 else 219 { 220 ModelItemType type = modelItem.getType(); 221 222 if (isEdition) 223 { 224 if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable()) 225 { 226 _saxExternalizableValuesAsJson(dataHolder, contentHandler, dataName, newContext); 227 } 228 else if (hasValue(dataHolder, dataName, newContext)) 229 { 230 Object value = dataHolder.getValue(dataName); 231 type.valueToSAXForEdition(contentHandler, modelItem.getName(), value, newContext); 232 } 233 } 234 else if (hasValue(dataHolder, dataName, newContext)) 235 { 236 Object value = dataHolder.getValue(dataName); 237 type.valueToSAX(contentHandler, modelItem.getName(), value, newContext); 238 } 239 } 240 } 241 } 242 else if (viewItem instanceof ViewItemAccessor accessor) 243 { 244 dataToSAX(dataHolder, contentHandler, accessor, context, isEdition); 245 } 246 } 247 248 if (_isExternalDisableConditionsAware(mergedViewItemAccessor, isEdition)) 249 { 250 externalDisableConditionsToSAX(dataHolder, contentHandler, mergedViewItemAccessor, context); 251 } 252 } 253 254 @SuppressWarnings("unchecked") 255 private static void _propertyToSAX(Property property, ContentHandler contentHandler, DataContext context, boolean isEdition) throws SAXException 256 { 257 ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(context); 258 if (isEdition) 259 { 260 throw new SAXException("Unable to generate SAX events for property '" + context.getDataPath() + "', for object '" + ametysObject + "' in edition mode: properties are not modifiables."); 261 } 262 else 263 { 264 property.valueToSAX(contentHandler, ametysObject, context); 265 } 266 } 267 268 private static void _saxExternalizableValuesAsJson (ModelAwareDataHolder dataHolder, ContentHandler contentHandler, String dataName, DataContext context) throws SAXException 269 { 270 Map<String, Object> values = externalizableValuesAsJson(dataHolder, dataName, context); 271 272 if (!values.isEmpty()) 273 { 274 String jsonString = _jsonUtils.convertObjectToJson(values); 275 276 AttributesImpl attrs = new AttributesImpl(); 277 attrs.addCDATAAttribute("json", "true"); 278 XMLUtils.createElement(contentHandler, dataName, attrs, jsonString); 279 } 280 } 281 282 /** 283 * Generates SAX events for external disable conditions of the items in the given view 284 * @param dataHolder the {@link ModelAwareDataHolder} to SAX 285 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 286 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events 287 * @param dataContext The context of the data to SAX 288 * @throws SAXException if an error occurs during the SAX events generation 289 * @throws BadItemTypeException if the saxed value's type does not matches the stored data 290 * @throws UndefinedItemPathException if an item in the view is not part of the model 291 */ 292 public static void externalDisableConditionsToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext dataContext) throws SAXException, BadItemTypeException, UndefinedItemPathException 293 { 294 Map<String, Boolean> externalDisableConditionsValues = getExternalDisableConditionsValues(dataHolder, viewItemAccessor, dataContext); 295 296 if (!externalDisableConditionsValues.isEmpty()) 297 { 298 XMLUtils.startElement(contentHandler, EXTERNAL_DISABLE_CONDITIONS_VALUES); 299 300 for (Map.Entry<String, Boolean> externalDisableConditionValue : externalDisableConditionsValues.entrySet()) 301 { 302 String conditionId = externalDisableConditionValue.getKey(); 303 String conditionValue = externalDisableConditionValue.getValue().toString(); 304 305 AttributesImpl attributes = new AttributesImpl(); 306 attributes.addCDATAAttribute("id", conditionId); 307 XMLUtils.createElement(contentHandler, "condition", attributes, conditionValue); 308 } 309 310 XMLUtils.endElement(contentHandler, EXTERNAL_DISABLE_CONDITIONS_VALUES); 311 } 312 } 313 314 /** 315 * Convert the data in the given view of the given {@link DataHolder} 316 * @param dataHolder the {@link ModelAwareDataHolder} containing the data to convert 317 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to convert 318 * @param context The context of the data to convert 319 * @param isEdition <code>true</code> to convert in edition mode, <code>false</code> otherwise 320 * @return The data of the given view as JSON 321 * @throws BadItemTypeException if the value's type does not matches the stored data 322 * @throws UndefinedItemPathException if an item in the view is not part of the model 323 */ 324 public static Map<String, Object> dataToJSON(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context, boolean isEdition) throws BadItemTypeException, UndefinedItemPathException 325 { 326 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 327 328 Map<String, Object> result = new HashMap<>(); 329 for (ViewItem viewItem : mergedViewItemAccessor.getViewItems()) 330 { 331 if (viewItem instanceof ModelViewItem modelViewItem) 332 { 333 ModelItem modelItem = modelViewItem.getDefinition(); 334 String dataName = modelItem.getName(); 335 DataContext newContext = context.cloneContext() 336 .addSegmentToDataPath(dataName) 337 .withViewItem(viewItem) 338 .withModelItem(modelItem); 339 340 if (renderValue(dataHolder, dataName, newContext, isEdition)) 341 { 342 if (modelItem instanceof Property property) 343 { 344 if (hasValue(dataHolder, dataName, newContext)) 345 { 346 Object json = _propertyToJSON(property, newContext, isEdition); 347 result.put(dataName, json); 348 } 349 } 350 else 351 { 352 ModelItemType type = modelItem.getType(); 353 354 if (isEdition) 355 { 356 if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable()) 357 { 358 Map<String, Object> json = externalizableValuesAsJson(dataHolder, dataName, newContext); 359 if (!json.isEmpty()) 360 { 361 result.put(dataName, json); 362 } 363 } 364 else if (hasValue(dataHolder, dataName, newContext)) 365 { 366 Object value = dataHolder.getValue(dataName); 367 Object json = type.valueToJSONForEdition(value, newContext); 368 result.put(dataName, json); 369 } 370 } 371 else if (hasValue(dataHolder, dataName, newContext)) 372 { 373 Object value = dataHolder.getValue(dataName); 374 Object json = type.valueToJSONForClient(value, newContext); 375 result.put(dataName, json); 376 } 377 } 378 } 379 } 380 else if (viewItem instanceof ViewItemAccessor accessor) 381 { 382 result.putAll(dataToJSON(dataHolder, accessor, context, isEdition)); 383 } 384 } 385 386 if (_isExternalDisableConditionsAware(mergedViewItemAccessor, isEdition)) 387 { 388 Map<String, Boolean> conditionsValues = getExternalDisableConditionsValues(dataHolder, mergedViewItemAccessor, context); 389 if (!conditionsValues.isEmpty()) 390 { 391 result.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 392 } 393 } 394 395 return result; 396 } 397 398 @SuppressWarnings("unchecked") 399 private static Object _propertyToJSON(Property property, DataContext context, boolean isEdition) throws UndefinedItemPathException 400 { 401 ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(context); 402 if (isEdition) 403 { 404 throw new UndefinedItemPathException("Unable to convert property '" + context.getDataPath() + "', for object '" + ametysObject + "' in edition mode: properties are not modifiables."); 405 } 406 else 407 { 408 return property.valueToJSON(ametysObject, context); 409 } 410 } 411 412 private static boolean _isExternalDisableConditionsAware(ViewItemAccessor viewItemAccessor, boolean isEdition) 413 { 414 return isEdition 415 && (viewItemAccessor instanceof View 416 || viewItemAccessor instanceof ModelViewItem modelViewItem && modelViewItem.getDefinition() instanceof RepeaterDefinition); 417 } 418 419 /** 420 * Convert the externalizable data with the given name 421 * @param dataHolder the {@link ModelAwareDataHolder} containing the data to convert 422 * @param dataName the name of the data to convert 423 * @param context The context of the data to convert 424 * @return The data with the given name as JSON 425 */ 426 public static Map<String, Object> externalizableValuesAsJson(ModelAwareDataHolder dataHolder, String dataName, DataContext context) 427 { 428 Map<String, Object> result = new LinkedHashMap<>(); 429 430 RepositoryElementType type = dataHolder.getType(dataName); 431 if (dataHolder.hasLocalValue(dataName) 432 || context.renderEmptyValues() && dataHolder.hasLocalValueOrEmpty(dataName)) 433 { 434 Object localValue = dataHolder.getLocalValue(dataName); 435 Object localValueAsJSON = type.externalizableValueToJSON(localValue, context); 436 result.put("local", localValueAsJSON); 437 } 438 439 if (dataHolder.hasExternalValue(dataName) 440 || context.renderEmptyValues() && dataHolder.hasExternalValueOrEmpty(dataName)) 441 { 442 Object externalValue = dataHolder.getExternalValue(dataName); 443 Object externalValueAsJSON = type.externalizableValueToJSON(externalValue, context); 444 result.put("external", externalValueAsJSON); 445 } 446 447 if (!result.isEmpty()) 448 { 449 ExternalizableDataStatus status = dataHolder.getStatus(dataName); 450 result.put("status", status.name().toLowerCase()); 451 } 452 453 return result; 454 } 455 456 /** 457 * Retrieves the values of the disable conditions of the items in the given view for the given data holder 458 * @param dataHolder the {@link ModelAwareDataHolder} containing the data 459 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items 460 * @param dataContext The context of the data 461 * @return the disable conditions values 462 */ 463 public static Map<String, Boolean> getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext dataContext) 464 { 465 DefinitionContext definitionContext = DefinitionContext.newInstance(); 466 org.ametys.runtime.model.ViewHelper.getView(viewItemAccessor) 467 .ifPresent(definitionContext::withView); 468 469 return _getExternalDisableConditionsValues(dataHolder, viewItemAccessor, StringUtils.EMPTY, definitionContext, dataContext); 470 } 471 472 private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String prefix, DefinitionContext definitionContext, DataContext dataContext) 473 { 474 Map<String, Boolean> externalDisableConditionsValues = new HashMap<>(); 475 476 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 477 { 478 DataContext newDataContext = dataContext.cloneContext(); 479 480 if (viewItem instanceof ModelViewItem modelViewItem) 481 { 482 ModelItem modelItem = modelViewItem.getDefinition(); 483 484 newDataContext.addSegmentToDataPath(modelItem.getName()) 485 .withViewItem(viewItem) 486 .withModelItem(modelItem); 487 488 externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, modelItem, prefix, definitionContext, newDataContext)); 489 } 490 491 if (viewItem instanceof ViewItemAccessor group && !_isExternalDisableConditionsAware(group, true)) 492 { 493 // Add external disable conditions of view items in groups 494 String newPrefix = prefix; 495 if (group instanceof ModelViewItem modelViewItem) 496 { 497 newPrefix = prefix + modelViewItem.getDefinition().getName() + ModelItem.ITEM_PATH_SEPARATOR; 498 } 499 externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, group, newPrefix, definitionContext, newDataContext)); 500 } 501 } 502 503 return externalDisableConditionsValues; 504 } 505 506 private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ModelItem modelItem, String prefix, DefinitionContext definitionContext, DataContext dataContext) 507 { 508 Map<String, Boolean> externalDisableConditionsValues = new HashMap<>(); 509 510 DisableConditions disableConditions = modelItem.getDisableConditions(); 511 if (disableConditions != null) 512 { 513 definitionContext.withModelItem(modelItem); 514 Collection<DisableCondition> externalDisableConditions = disableConditions.getExternalDisableConditions(definitionContext); 515 for (DisableCondition condition : externalDisableConditions) 516 { 517 boolean conditionValue = condition.evaluate(modelItem, dataContext.getDataPath(), Optional.empty(), Map.of(), Optional.of(dataHolder.getRootDataHolder()), new HashMap<>()); 518 519 String conditionId = StringUtils.join(new String[] {DisableCondition.EXTERNAL_CONDITION_ID_PREFIX, condition.getId(), condition.getName(), prefix + modelItem.getName()}, "_"); 520 externalDisableConditionsValues.put(conditionId, conditionValue); 521 } 522 } 523 524 return externalDisableConditionsValues; 525 } 526 527 /** 528 * Returns all data of the given DataHolder as a typed-values Map. 529 * @param dataHolder the DataHolder to export 530 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to include in the resulting Map 531 * @param context The context of the data 532 * @return a Map containing all data. 533 */ 534 @SuppressWarnings("unchecked") 535 public static Map<String, Object> dataToMap(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context) 536 { 537 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 538 539 Map<String, Object> result = new HashMap<>(); 540 541 ViewHelper.visitView(mergedViewItemAccessor, 542 (element, definition) -> { 543 // simple element 544 String name = definition.getName(); 545 DataContext newContext = context.cloneContext().addSegmentToDataPath(name); 546 547 if (renderValue(dataHolder, name, newContext, false)) 548 { 549 if (definition instanceof Property property) 550 { 551 if (hasValue(dataHolder, name, newContext)) 552 { 553 ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(newContext); 554 Object value = property.getValue(ametysObject); 555 result.put(name, value); 556 } 557 } 558 else 559 { 560 if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable()) 561 { 562 if (_hasExternalizableValue(dataHolder, name, newContext)) 563 { 564 SynchronizableValue value = new SynchronizableValue(dataHolder.getLocalValue(name)); 565 value.setExternalValue(dataHolder.getExternalValue(name)); 566 value.setExternalizableStatus(dataHolder.getStatus(name)); 567 result.put(name, value); 568 } 569 } 570 else if (hasValue(dataHolder, name, newContext)) 571 { 572 Object value = dataHolder.getValue(name); 573 result.put(name, value); 574 } 575 } 576 } 577 }, 578 (group, definition) -> { 579 // composite 580 String name = definition.getName(); 581 DataContext newContext = context.cloneContext().addSegmentToDataPath(name); 582 if (renderValue(dataHolder, name, newContext, false) && hasValue(dataHolder, name, newContext)) 583 { 584 ModelAwareComposite value = dataHolder.getValue(name); 585 result.put(name, value == null ? null : value.dataToMap(group, newContext)); 586 } 587 }, 588 (group, definition) -> { 589 // repeater 590 String name = definition.getName(); 591 DataContext repeaterContext = context.cloneContext().addSegmentToDataPath(name); 592 593 if (renderValue(dataHolder, name, repeaterContext, false) && hasValue(dataHolder, name, context)) 594 { 595 ModelAwareRepeater repeater = dataHolder.getValue(name); 596 List<Map<String, Object>> entries = null; 597 if (repeater != null) 598 { 599 entries = new ArrayList<>(); 600 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 601 { 602 DataContext entryContext = repeaterContext.cloneContext().addSuffixToLastSegment("[" + entry.getPosition() + "]"); 603 entries.add(entry.dataToMap(group, entryContext)); 604 } 605 } 606 result.put(name, entries); 607 } 608 }, 609 group -> result.putAll(dataToMap(dataHolder, group, context))); 610 611 return result; 612 } 613 614 /** 615 * Retrieves the {@link ModelAwareDataAwareAmetysObject} from the given {@link DataContext} 616 * @param context the context containing the ametys object 617 * @return the ametys object, or <code>null</code> if there is no object id in the context 618 */ 619 public static ModelAwareDataAwareAmetysObject getAmetysObjectFromContext(DataContext context) 620 { 621 RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context); 622 623 return repoContext.getObject() 624 .filter(ModelAwareDataAwareAmetysObject.class::isInstance) 625 .map(ModelAwareDataAwareAmetysObject.class::cast) 626 .orElse(null); 627 } 628 629 /** 630 * Check if the value at the given path should be rendered 631 * @param dataHolder the data holder containing the data to check 632 * @param dataPath the path of the data to check 633 * @param context the data context 634 * @param isEdition <code>true</code> if values are rendered in edition mode, <code>false</code> otherwise 635 * @return <code>true</code> if the value has to be rendered, <code>false</code> otherwise 636 */ 637 public static boolean renderValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context, boolean isEdition) 638 { 639 if (!isEdition && !context.renderDisabledValues()) 640 { 641 ModelItem modelItem = dataHolder.getDefinition(dataPath); 642 String absoluteDataPath = context.getDataPath(); 643 return !_disableConditionsEvaluator.evaluateDisableConditions(modelItem, absoluteDataPath, dataHolder.getRootDataHolder()); 644 } 645 else 646 { 647 return true; 648 } 649 } 650 651 /** 652 * Check if there is a value to render at the given path 653 * @param dataHolder the data holder containing the data to check 654 * @param dataPath the path of the data to check 655 * @param context the data context 656 * @return <code>true</code> if there is a value to render, <code>false</code> otherwise 657 */ 658 public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context) 659 { 660 return dataHolder.hasValue(dataPath) 661 || context.renderEmptyValues() && dataHolder.hasValueOrEmpty(dataPath); 662 } 663 664 private static boolean _hasExternalizableValue(ModelAwareDataHolder dataHolder, String dataName, DataContext context) 665 { 666 return dataHolder.hasLocalValue(dataName) 667 || dataHolder.hasExternalValue(dataName) 668 || context.renderEmptyValues() 669 && (dataHolder.hasLocalValueOrEmpty(dataName) || dataHolder.hasExternalValueOrEmpty(dataName)); 670 } 671 672 /** 673 * Retrieves the {@link SystemPropertyExtensionPoint} for content system properties 674 * @return the {@link SystemPropertyExtensionPoint} for content system properties 675 */ 676 public static SystemPropertyExtensionPoint getContentSystemPropertyExtensionPoint() 677 { 678 return _contentSystemPropertyExtentionPoint; 679 } 680 681 private static ViewItemAccessor _mergeDuplicatedItemsInView(ViewItemAccessor viewItemAccessor) 682 { 683 return viewItemAccessor instanceof View 684 ? org.ametys.runtime.model.ViewHelper.mergeDuplicatedItems(viewItemAccessor) 685 : viewItemAccessor; 686 } 687}