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