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