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, repositoryDataContext); 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, RepositoryDataContext 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, repositoryDataContext); 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, RepositoryDataContext 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 436 Object localValueAsJSON = type.externalizableValueToJSON(localValue, context.withStatus(ExternalizableDataStatus.LOCAL)); 437 result.put("local", localValueAsJSON); 438 } 439 440 if (dataHolder.hasExternalValue(dataName) 441 || context.renderEmptyValues() && dataHolder.hasExternalValueOrEmpty(dataName)) 442 { 443 Object externalValue = dataHolder.getExternalValue(dataName); 444 Object externalValueAsJSON = type.externalizableValueToJSON(externalValue, context.withStatus(ExternalizableDataStatus.EXTERNAL)); 445 result.put("external", externalValueAsJSON); 446 } 447 448 if (!result.isEmpty()) 449 { 450 ExternalizableDataStatus status = dataHolder.getStatus(dataName); 451 result.put("status", status.name().toLowerCase()); 452 } 453 454 return result; 455 } 456 457 /** 458 * Retrieves the values of the disable conditions of the items in the given view for the given data holder 459 * @param dataHolder the {@link ModelAwareDataHolder} containing the data 460 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items 461 * @param dataContext The context of the data 462 * @return the disable conditions values 463 */ 464 public static Map<String, Boolean> getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext dataContext) 465 { 466 DefinitionContext definitionContext = DefinitionContext.newInstance(); 467 org.ametys.runtime.model.ViewHelper.getView(viewItemAccessor) 468 .ifPresent(definitionContext::withView); 469 470 return _getExternalDisableConditionsValues(dataHolder, viewItemAccessor, StringUtils.EMPTY, definitionContext, dataContext); 471 } 472 473 private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String prefix, DefinitionContext definitionContext, DataContext dataContext) 474 { 475 Map<String, Boolean> externalDisableConditionsValues = new HashMap<>(); 476 477 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 478 { 479 DataContext newDataContext = dataContext.cloneContext(); 480 481 if (viewItem instanceof ModelViewItem modelViewItem) 482 { 483 ModelItem modelItem = modelViewItem.getDefinition(); 484 485 newDataContext.addSegmentToDataPath(modelItem.getName()) 486 .withViewItem(viewItem) 487 .withModelItem(modelItem); 488 489 externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, modelItem, prefix, definitionContext, newDataContext)); 490 } 491 492 if (viewItem instanceof ViewItemAccessor group && !_isExternalDisableConditionsAware(group, true)) 493 { 494 // Add external disable conditions of view items in groups 495 String newPrefix = prefix; 496 if (group instanceof ModelViewItem modelViewItem) 497 { 498 newPrefix = prefix + modelViewItem.getDefinition().getName() + ModelItem.ITEM_PATH_SEPARATOR; 499 } 500 externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, group, newPrefix, definitionContext, newDataContext)); 501 } 502 } 503 504 return externalDisableConditionsValues; 505 } 506 507 private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ModelItem modelItem, String prefix, DefinitionContext definitionContext, DataContext dataContext) 508 { 509 Map<String, Boolean> externalDisableConditionsValues = new HashMap<>(); 510 511 DisableConditions disableConditions = modelItem.getDisableConditions(); 512 if (disableConditions != null) 513 { 514 definitionContext.withModelItem(modelItem); 515 Collection<DisableCondition> externalDisableConditions = disableConditions.getExternalDisableConditions(definitionContext); 516 for (DisableCondition condition : externalDisableConditions) 517 { 518 boolean conditionValue = condition.evaluate(modelItem, dataContext.getDataPath(), Optional.empty(), Map.of(), Optional.of(dataHolder.getRootDataHolder()), new HashMap<>()); 519 520 String conditionId = StringUtils.join(new String[] {DisableCondition.EXTERNAL_CONDITION_ID_PREFIX, condition.getId(), condition.getName(), prefix + modelItem.getName()}, "_"); 521 externalDisableConditionsValues.put(conditionId, conditionValue); 522 } 523 } 524 525 return externalDisableConditionsValues; 526 } 527 528 /** 529 * Returns all data of the given DataHolder as a typed-values Map. 530 * @param dataHolder the DataHolder to export 531 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to include in the resulting Map 532 * @param context The context of the data 533 * @return a Map containing all data. 534 */ 535 @SuppressWarnings("unchecked") 536 public static Map<String, Object> dataToMap(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context) 537 { 538 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 539 540 Map<String, Object> result = new HashMap<>(); 541 542 ViewHelper.visitView(mergedViewItemAccessor, 543 (element, definition) -> { 544 // simple element 545 String name = definition.getName(); 546 DataContext newContext = context.cloneContext().addSegmentToDataPath(name); 547 548 if (renderValue(dataHolder, name, newContext, false)) 549 { 550 if (definition instanceof Property property) 551 { 552 if (hasValue(dataHolder, name, newContext)) 553 { 554 ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(newContext); 555 Object value = property.getValue(ametysObject); 556 result.put(name, value); 557 } 558 } 559 else 560 { 561 if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable()) 562 { 563 if (_hasExternalizableValue(dataHolder, name, newContext)) 564 { 565 SynchronizableValue value = new SynchronizableValue(dataHolder.getLocalValue(name)); 566 value.setExternalValue(dataHolder.getExternalValue(name)); 567 value.setExternalizableStatus(dataHolder.getStatus(name)); 568 result.put(name, value); 569 } 570 } 571 else if (hasValue(dataHolder, name, newContext)) 572 { 573 Object value = dataHolder.getValue(name); 574 result.put(name, value); 575 } 576 } 577 } 578 }, 579 (group, definition) -> { 580 // composite 581 String name = definition.getName(); 582 DataContext newContext = context.cloneContext().addSegmentToDataPath(name); 583 if (renderValue(dataHolder, name, newContext, false) && hasValue(dataHolder, name, newContext)) 584 { 585 ModelAwareComposite value = dataHolder.getValue(name); 586 result.put(name, value == null ? null : value.dataToMap(group, newContext)); 587 } 588 }, 589 (group, definition) -> { 590 // repeater 591 String name = definition.getName(); 592 DataContext repeaterContext = context.cloneContext().addSegmentToDataPath(name); 593 594 if (renderValue(dataHolder, name, repeaterContext, false) && hasValue(dataHolder, name, context)) 595 { 596 ModelAwareRepeater repeater = dataHolder.getValue(name); 597 List<Map<String, Object>> entries = null; 598 if (repeater != null) 599 { 600 entries = new ArrayList<>(); 601 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 602 { 603 DataContext entryContext = repeaterContext.cloneContext().addSuffixToLastSegment("[" + entry.getPosition() + "]"); 604 entries.add(entry.dataToMap(group, entryContext)); 605 } 606 } 607 result.put(name, entries); 608 } 609 }, 610 group -> result.putAll(dataToMap(dataHolder, group, context))); 611 612 return result; 613 } 614 615 /** 616 * Retrieves the {@link ModelAwareDataAwareAmetysObject} from the given {@link DataContext} 617 * @param context the context containing the ametys object 618 * @return the ametys object, or <code>null</code> if there is no object id in the context 619 */ 620 public static ModelAwareDataAwareAmetysObject getAmetysObjectFromContext(DataContext context) 621 { 622 RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context); 623 624 return repoContext.getObject() 625 .filter(ModelAwareDataAwareAmetysObject.class::isInstance) 626 .map(ModelAwareDataAwareAmetysObject.class::cast) 627 .orElse(null); 628 } 629 630 /** 631 * Check if the value at the given path should be rendered 632 * @param dataHolder the data holder containing the data to check 633 * @param dataPath the path of the data to check 634 * @param context the data context 635 * @param isEdition <code>true</code> if values are rendered in edition mode, <code>false</code> otherwise 636 * @return <code>true</code> if the value has to be rendered, <code>false</code> otherwise 637 */ 638 public static boolean renderValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context, boolean isEdition) 639 { 640 if (!isEdition && !context.renderDisabledValues()) 641 { 642 ModelItem modelItem = dataHolder.getDefinition(dataPath); 643 String absoluteDataPath = context.getDataPath(); 644 return !_disableConditionsEvaluator.evaluateDisableConditions(modelItem, absoluteDataPath, dataHolder.getRootDataHolder()); 645 } 646 else 647 { 648 return true; 649 } 650 } 651 652 /** 653 * Check if there is a value to render at the given path 654 * @param dataHolder the data holder containing the data to check 655 * @param dataPath the path of the data to check 656 * @param context the data context 657 * @return <code>true</code> if there is a value to render, <code>false</code> otherwise 658 */ 659 public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context) 660 { 661 return dataHolder.hasValue(dataPath) 662 || context.renderEmptyValues() && dataHolder.hasValueOrEmpty(dataPath); 663 } 664 665 private static boolean _hasExternalizableValue(ModelAwareDataHolder dataHolder, String dataName, DataContext context) 666 { 667 return dataHolder.hasLocalValue(dataName) 668 || dataHolder.hasExternalValue(dataName) 669 || context.renderEmptyValues() 670 && (dataHolder.hasLocalValueOrEmpty(dataName) || dataHolder.hasExternalValueOrEmpty(dataName)); 671 } 672 673 /** 674 * Retrieves the {@link SystemPropertyExtensionPoint} for content system properties 675 * @return the {@link SystemPropertyExtensionPoint} for content system properties 676 */ 677 public static SystemPropertyExtensionPoint getContentSystemPropertyExtensionPoint() 678 { 679 return _contentSystemPropertyExtentionPoint; 680 } 681 682 private static ViewItemAccessor _mergeDuplicatedItemsInView(ViewItemAccessor viewItemAccessor) 683 { 684 return viewItemAccessor instanceof View 685 ? org.ametys.runtime.model.ViewHelper.mergeDuplicatedItems(viewItemAccessor) 686 : viewItemAccessor; 687 } 688}