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