001/* 002 * Copyright 2016 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.search.content; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.data.ContentValue; 034import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject; 035import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper; 036import org.ametys.cms.data.type.ModelItemTypeConstants; 037import org.ametys.cms.model.CMSDataContext; 038import org.ametys.cms.model.properties.Property; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.search.model.SearchModel; 041import org.ametys.cms.search.model.SystemProperty; 042import org.ametys.cms.search.ui.model.SearchUIColumn; 043import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 044import org.ametys.cms.search.ui.model.impl.RepeaterSearchUIColumn; 045import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 046import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 048import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 049import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 051import org.ametys.plugins.repository.model.CompositeDefinition; 052import org.ametys.plugins.repository.model.RepeaterDefinition; 053import org.ametys.plugins.repository.model.ViewHelper; 054import org.ametys.runtime.model.DefinitionContext; 055import org.ametys.runtime.model.ElementDefinition; 056import org.ametys.runtime.model.Model; 057import org.ametys.runtime.model.ModelHelper; 058import org.ametys.runtime.model.ModelItem; 059import org.ametys.runtime.model.ModelItemGroup; 060import org.ametys.runtime.model.ModelViewItem; 061import org.ametys.runtime.model.ModelViewItemGroup; 062import org.ametys.runtime.model.View; 063import org.ametys.runtime.model.ViewElement; 064import org.ametys.runtime.model.ViewElementAccessor; 065import org.ametys.runtime.model.ViewItemAccessor; 066import org.ametys.runtime.model.ViewItemContainer; 067import org.ametys.runtime.model.exception.BadItemTypeException; 068import org.ametys.runtime.model.exception.UndefinedItemPathException; 069import org.ametys.runtime.model.type.DataContext; 070import org.ametys.runtime.model.type.ModelItemType; 071import org.ametys.runtime.plugin.component.AbstractLogEnabled; 072 073/** 074 * Component creating content values extractors from {@link SearchModel}s or {@link View}s. 075 */ 076public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable 077{ 078 /** The component role. */ 079 public static final String ROLE = ContentValuesExtractorFactory.class.getName(); 080 081 /** The content search helper. */ 082 protected ContentSearchHelper _searchHelper; 083 084 /** To determine the externalizable status */ 085 protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 086 087 @Override 088 public void service(ServiceManager manager) throws ServiceException 089 { 090 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 091 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 092 } 093 094 /** 095 * Create a ContentValuesExtractor from a search model. 096 * @param searchModel The reference search model. 097 * @return a ContentValuesExtractor backed by the given search model. 098 */ 099 public SearchModelContentValuesExtractor create(SearchModel searchModel) 100 { 101 return new SearchModelContentValuesExtractor(searchModel); 102 } 103 104 /** 105 * Create a simple {@link ContentValuesExtractor} from a view 106 * @param view The view. 107 * @return The created {@link ContentValuesExtractor} 108 */ 109 public SimpleContentValuesExtractor create(View view) 110 { 111 return new SimpleContentValuesExtractor(view); 112 } 113 114 /** 115 * A ContentValuesExtractor 116 */ 117 public interface ContentValuesExtractor 118 { 119 /** 120 * Get the values from the given content. 121 * @param content The content. 122 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 123 * @param contextualParameters The search contextual parameters. 124 * @return the extracted values. 125 */ 126 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters); 127 } 128 129 /** 130 * An abstract implementation of ContentValuesExtractor 131 */ 132 public abstract class AbstractContentValuesExtractor implements ContentValuesExtractor 133 { 134 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 135 { 136 CMSDataContext context = CMSDataContext.newInstance() 137 .withRichTextMaxLength(100) 138 .withObject(content) 139 .withLocale(defaultLocale) 140 .withEmptyValues(false); 141 142 boolean handleExternalizable = (boolean) contextualParameters.getOrDefault("externalizable", true); 143 if (handleExternalizable) 144 { 145 Set<String> externalizableData = _externalizableDataProviderEP.getExternalizableDataPaths(content); 146 context.withExternalizableData(externalizableData); 147 } 148 149 ViewItemContainer viewItemContainer = _getResultItems(content.getModel(), contextualParameters); 150 Map<String, Object> json = _dataToJSON(content, viewItemContainer, context, StringUtils.EMPTY, new HashMap<>()); 151 152 if (viewItemContainer instanceof View view) 153 { 154 DefinitionContext definitionContext = DefinitionContext.newInstance() 155 .withView(view); 156 157 Map<String, Boolean> conditionsValues = IndexableDataHolderHelper.getExternalDisableConditionsValues(content, viewItemContainer, definitionContext, context); 158 if (!conditionsValues.isEmpty()) 159 { 160 json.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 161 } 162 } 163 164 return json; 165 } 166 167 /** 168 * Retrieves the view to use to extract values 169 * @param model the model 170 * @param contextualParameters The search contextual parameters. 171 * @return the {@link View} to use to extract values 172 */ 173 protected abstract ViewItemContainer _getResultItems(Collection<? extends Model> model, Map<String, Object> contextualParameters); 174 } 175 176 /** 177 * A ContentValuesExtractor backed by a {@link SearchModel}. 178 */ 179 public class SearchModelContentValuesExtractor extends AbstractContentValuesExtractor 180 { 181 private SearchModel _searchModel; 182 183 /** 184 * Build a ContentValuesExtractor referencing a {@link SearchModel}. 185 * @param searchModel the {@link SearchModel}. 186 */ 187 public SearchModelContentValuesExtractor(SearchModel searchModel) 188 { 189 _searchModel = searchModel; 190 } 191 192 @Override 193 protected ViewItemContainer _getResultItems(Collection< ? extends Model> model, Map<String, Object> contextualParameters) 194 { 195 return _searchModel.getResultItems(contextualParameters); 196 } 197 } 198 199 /** 200 * A simple ContentValuesExtractor on a list of content types. 201 */ 202 public class SimpleContentValuesExtractor extends AbstractContentValuesExtractor 203 { 204 private ViewItemContainer _resultItems; 205 206 /** 207 * Build a simple {@link ContentValuesExtractor} from the given result items 208 * @param resultItems The result items 209 * @throws IllegalArgumentException if the given result items contain a composite as leaf, or a {@link SystemProperty} that is not displayable 210 */ 211 public SimpleContentValuesExtractor(ViewItemContainer resultItems) throws IllegalArgumentException 212 { 213 _resultItems = resultItems; 214 } 215 216 @Override 217 protected ViewItemContainer _getResultItems(Collection< ? extends Model> model, Map<String, Object> contextualParameters) 218 { 219 return _resultItems; 220 } 221 } 222 223 @SuppressWarnings("unchecked") 224 private Map<String, Object> _dataToJSON(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context, String prefix, Map<String, Content> resolvedContents) throws BadItemTypeException, UndefinedItemPathException 225 { 226 Map<String, Object> result = new HashMap<>(); 227 228 ViewHelper.visitView(viewItemAccessor, 229 (element, definition) -> { 230 // simple element 231 String name = definition.getName(); 232 String newDataPath = prefix + name; 233 234 DataContext newContext = CMSDataContext.newInstance(context) 235 .addSegmentToDataPath(name) 236 .withViewItem(element) 237 .withModelItem(definition); 238 239 result.putAll(_elementToJSON(dataHolder, element, definition, newDataPath, newContext, resolvedContents)); 240 }, 241 (group, definition) -> { 242 // composite 243 String name = definition.getName(); 244 String newDataPath = prefix + name; 245 246 DataContext newContext = CMSDataContext.newInstance(context) 247 .addSegmentToDataPath(name) 248 .withViewItem(group) 249 .withModelItem(definition); 250 251 result.putAll(_compositeToJSON(dataHolder, group, definition, newDataPath, newContext, resolvedContents)); 252 }, 253 (group, definition) -> { 254 // repeater 255 String name = definition.getName(); 256 String newDataPath = prefix + name; 257 258 DataContext repeaterContext = CMSDataContext.newInstance(context) 259 .addSegmentToDataPath(name) 260 .withViewItem(group) 261 .withModelItem(definition); 262 263 result.putAll(_repeaterToJSON(dataHolder, group, definition, newDataPath, repeaterContext, resolvedContents)); 264 }, 265 group -> result.putAll(_dataToJSON(dataHolder, group, context, prefix, resolvedContents))); 266 267 return result; 268 } 269 270 private Map<String, Object> _elementToJSON(ModelAwareDataHolder dataHolder, ViewElement viewElement, ElementDefinition definition, String dataPath, DataContext context, Map<String, Content> resolvedContents) 271 { 272 Map<String, Object> result = new HashMap<>(); 273 String name = definition.getName(); 274 275 if (viewElement instanceof ViewElementAccessor viewElementAccessor 276 && !viewElementAccessor.getViewItems().isEmpty()) 277 { 278 // The view item is an accessor -> convert all its children to JSON 279 if (dataHolder.hasValue(name) && ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 280 { 281 Map<String, Object> contentJSON = definition.isMultiple() 282 ? _multipleContentToJSON(dataHolder, viewElementAccessor, name, dataPath, context, resolvedContents) 283 : _singleContentToJSON(dataHolder, viewElementAccessor, name, dataPath, context, resolvedContents); 284 285 result.putAll(contentJSON); 286 } 287 } 288 else if (definition instanceof Property property) 289 { 290 ModelAwareDataAwareAmetysObject ametysObject = IndexableDataHolderHelper.getAmetysObjectFromContext(context); 291 @SuppressWarnings("unchecked") 292 Object json = property.valueToJSON(ametysObject, context); 293 result.put(dataPath, json); 294 } 295 else 296 { 297 if (((CMSDataContext) context).isDataExternalizable()) 298 { 299 Map<String, Object> json = IndexableDataHolderHelper.externalizableValuesAsJson(dataHolder, name, context); 300 if (json.isEmpty()) 301 { 302 // Always send externalizable data status for grids 303 ExternalizableDataStatus status = dataHolder.getStatus(name); 304 json.put("status", status.name().toLowerCase()); 305 } 306 307 result.put(dataPath, json); 308 } 309 else if (dataHolder.hasValue(name)) 310 { 311 Object value = dataHolder.getValue(name); 312 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 313 { 314 // Remove view item from context because children must be processed by the current algorithm, not by the type's one 315 context.withViewItem(null); 316 317 // Resolve contents here to use resolved contents cache 318 value = _resolveContentValues(definition, value, resolvedContents); 319 } 320 321 ModelItemType type = definition.getType(); 322 Object json = type.valueToJSONForClient(value, context); 323 result.put(dataPath, json); 324 } 325 } 326 327 return result; 328 } 329 330 @SuppressWarnings("unchecked") 331 private Map<String, Object> _multipleContentToJSON(ModelAwareDataHolder dataHolder, ViewElementAccessor viewElementAccessor, String modelItemName, String dataPath, DataContext context, Map<String, Content> resolvedContents) 332 { 333 Map<String, List<Object>> allDataJSON = new HashMap<>(); // JSON for all data except repeaters 334 Map<String, Object> repeatersJSON = new HashMap<>(); // JSON for repeaters that are leaves 335 336 ContentValue[] conntentValues = dataHolder.getValue(modelItemName); 337 for (ContentValue contentValue : conntentValues) 338 { 339 Content linkedContent = _getResolvedContent(contentValue, resolvedContents); 340 if (linkedContent != null) 341 { 342 DataContext contentContext = CMSDataContext.newInstance(context) 343 .withObject(linkedContent) 344 .withDataPath(StringUtils.EMPTY); 345 346 Map<String, Object> contentJSON = _dataToJSON(linkedContent, viewElementAccessor, contentContext, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 347 for (String childDataPath : contentJSON.keySet()) 348 { 349 String definitionPath = ModelHelper.getDefinitionPathFromDataPath(childDataPath); 350 Object childJSON = contentJSON.get(childDataPath); 351 if (_isRepeaterJSON(childJSON)) 352 { 353 // The child is a repeater -> merge the entries 354 _mergeRepeaterEntriesJSON(repeatersJSON, (Map<String, Object>) childJSON, definitionPath); 355 } 356 else 357 { 358 // Merge data of all contents 359 List<Object> dataJSON = allDataJSON.computeIfAbsent(definitionPath, __ -> new ArrayList<>()); 360 if (childJSON instanceof List jsonList) 361 { 362 dataJSON.addAll(jsonList); 363 } 364 else 365 { 366 dataJSON.add(childJSON); 367 } 368 } 369 } 370 } 371 } 372 373 Map<String, Object> result = new HashMap<>(); 374 result.putAll(allDataJSON); 375 result.putAll(repeatersJSON); 376 377 return result; 378 } 379 380 private Map<String, Object> _singleContentToJSON(ModelAwareDataHolder dataHolder, ViewElementAccessor viewElementAccessor, String modelItemName, String dataPath, DataContext context, Map<String, Content> resolvedContents) 381 { 382 Map<String, Object> result = new HashMap<>(); 383 384 ContentValue contentValue = dataHolder.getValue(modelItemName); 385 Content linkedContent = _getResolvedContent(contentValue, resolvedContents); 386 387 if (linkedContent != null) 388 { 389 DataContext contentContext = CMSDataContext.newInstance(context) 390 .withObject(linkedContent) 391 .withDataPath(StringUtils.EMPTY); 392 393 Map<String, Object> contentJSON = _dataToJSON(linkedContent, viewElementAccessor, contentContext, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 394 result.putAll(contentJSON); 395 } 396 397 return result; 398 } 399 400 private Object _resolveContentValues(ElementDefinition definition, Object value, Map<String, Content> resolvedContents) 401 { 402 Object result = value; 403 404 if (definition.isMultiple()) 405 { 406 ContentValue[] contentValues = (ContentValue[]) value; 407 List<Content> contents = new ArrayList<>(); 408 for (ContentValue contentValue : contentValues) 409 { 410 Content content = _getResolvedContent(contentValue, resolvedContents); 411 if (content != null) 412 { 413 contents.add(content); 414 } 415 } 416 417 result = contents.toArray(new Content[contents.size()]); 418 } 419 else 420 { 421 ContentValue contentValue = (ContentValue) value; 422 Content content = _getResolvedContent(contentValue, resolvedContents); 423 result = content != null ? content : result; 424 } 425 426 return result; 427 } 428 429 private Content _getResolvedContent(ContentValue contentValue, Map<String, Content> resolvedContents) 430 { 431 return resolvedContents.computeIfAbsent(contentValue.getContentId(), 432 id -> contentValue.getContentIfExists().orElse(null)); 433 } 434 435 private Map<String, Object> _compositeToJSON(ModelAwareDataHolder dataHolder, ModelViewItemGroup<CompositeDefinition> compositeViewItem, CompositeDefinition compositeDefinition, String dataPath, DataContext context, Map<String, Content> resolvedContents) 436 { 437 Map<String, Object> result = new HashMap<>(); 438 String name = compositeDefinition.getName(); 439 440 if (compositeViewItem.getViewItems().isEmpty()) 441 { 442 throw new IllegalArgumentException("Attribute at path '" + dataPath + "' is a composite: can not invoke #getAttributeValue"); 443 } 444 445 if (dataHolder.hasValue(name)) 446 { 447 ModelAwareComposite value = dataHolder.getValue(name); 448 Map<String, Object> json = _dataToJSON(value, compositeViewItem, context, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 449 result.putAll(json); 450 } 451 452 return result; 453 } 454 455 private Map<String, Object> _repeaterToJSON(ModelAwareDataHolder dataHolder, ModelViewItemGroup<RepeaterDefinition> repeaterViewItem, RepeaterDefinition repeaterDefinition, String repeaterPath, DataContext context, Map<String, Content> resolvedContents) 456 { 457 Map<String, Object> result = new HashMap<>(); 458 String name = repeaterDefinition.getName(); 459 460 if (dataHolder.hasValue(name)) 461 { 462 ModelAwareRepeater repeater = dataHolder.getValue(name); 463 464 if (repeaterViewItem instanceof SearchUIColumn || repeaterViewItem.getViewItems().isEmpty()) 465 { 466 ModelViewItemGroup<RepeaterDefinition> repeaterLeaf = _getRepeaterLeafViewItem(repeaterViewItem, repeaterDefinition); 467 Map<String, Object> repeaterLeafJSON = _repeaterLeafToJSON(repeater, repeaterLeaf, context, resolvedContents); 468 result.put(repeaterPath, repeaterLeafJSON); 469 } 470 else 471 { 472 Map<String, Object> repeaterJSON = _repeaterToJSON(repeater, repeaterViewItem, repeaterPath, context, resolvedContents); 473 result.putAll(repeaterJSON); 474 } 475 } 476 477 return result; 478 } 479 480 private Map<String, Object> _repeaterLeafToJSON(ModelAwareRepeater repeater, ModelViewItemGroup<RepeaterDefinition> repeaterViewItem, DataContext context, Map<String, Content> resolvedContents) 481 { 482 List<Map<String, Object>> entriesJSON = new ArrayList<>(); 483 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 484 { 485 DataContext entryContext = CMSDataContext.newInstance(context) 486 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 487 488 Map<String, Object> entryValuesJson = _dataToJSON(entry, repeaterViewItem, entryContext, StringUtils.EMPTY, resolvedContents); 489 490 Map<String, Object> entryJSON = new HashMap<>(); 491 entryJSON.put("values", entryValuesJson); 492 entryJSON.put("position", entry.getPosition()); 493 494 entriesJSON.add(entryJSON); 495 } 496 497 Map<String, Object> json = new HashMap<>(); 498 json.put("type", org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID); 499 json.put("entries", entriesJSON); 500 json.put("label", repeaterViewItem.getDefinition().getLabel()); 501 502 Optional.ofNullable(repeaterViewItem.getDefinition().getHeaderLabel()) 503 .ifPresent(headerLabel -> json.put("header-label", headerLabel)); 504 505 return json; 506 } 507 508 private ModelViewItemGroup<RepeaterDefinition> _getRepeaterLeafViewItem(ModelViewItemGroup<RepeaterDefinition> group, RepeaterDefinition definition) 509 { 510 boolean useColumns = group instanceof SearchUIColumn; 511 ModelViewItemGroup<RepeaterDefinition> viewItemGroup = useColumns ? new RepeaterSearchUIColumn() : new ModelViewItemGroup<>(); 512 viewItemGroup.setDefinition(definition); 513 514 for (ModelItem child : definition.getChildren()) 515 { 516 _addViewItemForRepeaterLeaf(child, viewItemGroup, useColumns); 517 } 518 519 return viewItemGroup; 520 } 521 522 @SuppressWarnings("unchecked") 523 private void _addViewItemForRepeaterLeaf(ModelItem modelItem, ModelViewItemGroup group, boolean isColumns) 524 { 525 ModelViewItem viewItem; 526 if (modelItem instanceof CompositeDefinition compositeDefinition) 527 { 528 viewItem = new ModelViewItemGroup<>(); 529 530 // Add children only for composites, children of type repeater do not need to have children 531 for (ModelItem child : compositeDefinition.getChildren()) 532 { 533 _addViewItemForRepeaterLeaf(child, (ModelViewItemGroup) viewItem, isColumns); 534 } 535 } 536 else 537 { 538 viewItem = isColumns 539 ? SearchUIColumnHelper.createModelItemColumn(modelItem) 540 : modelItem instanceof ModelItemGroup 541 ? new ModelViewItemGroup<>() 542 : new ViewElement(); 543 } 544 545 viewItem.setDefinition(modelItem); 546 group.addViewItem(viewItem); 547 } 548 549 @SuppressWarnings("unchecked") 550 private Map<String, Object> _repeaterToJSON(ModelAwareRepeater repeater, ModelViewItemGroup<RepeaterDefinition> group, String repeaterPath, DataContext repeaterContext, Map<String, Content> resolvedContents) 551 { 552 Map<String, List<Object>> singleDataJSON = new HashMap<>(); 553 Map<String, Object> repeatersJSON = new HashMap<>(); 554 555 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 556 { 557 DataContext entryContext = CMSDataContext.newInstance(repeaterContext) 558 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 559 560 Map<String, Object> entryJson = _dataToJSON(entry, group, entryContext, repeaterPath + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 561 562 for (String entryPath : entryJson.keySet()) 563 { 564 String definitionPath = ModelHelper.getDefinitionPathFromDataPath(entryPath); 565 Object dataJSON = entryJson.get(entryPath); 566 if (_isRepeaterJSON(dataJSON)) 567 { 568 // The child is a repeater -> merge the entries 569 _mergeRepeaterEntriesJSON(repeatersJSON, (Map<String, Object>) dataJSON, definitionPath); 570 } 571 else 572 { 573 // Merge data of all data 574 List<Object> dataJsonList = singleDataJSON.computeIfAbsent(definitionPath, __ -> new ArrayList<>()); 575 if (dataJSON instanceof List jsonList) 576 { 577 dataJsonList.addAll(jsonList); 578 } 579 else 580 { 581 dataJsonList.add(dataJSON); 582 } 583 } 584 } 585 } 586 587 Map<String, Object> result = new HashMap<>(); 588 result.putAll(singleDataJSON); 589 result.putAll(repeatersJSON); 590 591 return result; 592 } 593 594 private boolean _isRepeaterJSON(Object json) 595 { 596 return json instanceof Map map && org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID.equals(map.get("type")); 597 } 598 599 @SuppressWarnings("unchecked") 600 private void _mergeRepeaterEntriesJSON(Map<String, Object> repeatersJSON, Map<String, Object> json, String definitionPath) 601 { 602 if (repeatersJSON.containsKey(definitionPath)) 603 { 604 Map<String, Object> repeaterJSON = (Map<String, Object>) repeatersJSON.get(definitionPath); 605 List<Object> allEntries = (List<Object>) repeaterJSON.get("entries"); 606 List<Object> entries = (List<Object>) json.get("entries"); 607 allEntries.addAll(entries); 608 } 609 else 610 { 611 repeatersJSON.put(definitionPath, json); 612 } 613 } 614}