001/* 002 * Copyright 2018 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.group.impl; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.Set; 029import java.util.SortedSet; 030import java.util.TreeSet; 031 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.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038import org.xml.sax.Attributes; 039import org.xml.sax.ContentHandler; 040import org.xml.sax.SAXException; 041 042import org.ametys.cms.content.indexing.solr.SolrFieldNames; 043import org.ametys.cms.data.holder.IndexableDataHolder; 044import org.ametys.cms.data.holder.group.IndexableRepeater; 045import org.ametys.cms.data.holder.group.IndexableRepeaterEntry; 046import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper; 047import org.ametys.cms.model.CMSDataContext; 048import org.ametys.plugins.repository.data.holder.DataHolder; 049import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 051import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater; 052import org.ametys.plugins.repository.data.holder.group.ModifiableRepeaterEntry; 053import org.ametys.plugins.repository.data.holder.group.Repeater; 054import org.ametys.plugins.repository.data.holder.group.RepeaterEntry; 055import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 056import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 057import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 058import org.ametys.plugins.repository.model.RepeaterDefinition; 059import org.ametys.runtime.model.ModelItem; 060import org.ametys.runtime.model.ModelViewItem; 061import org.ametys.runtime.model.ModelViewItemGroup; 062import org.ametys.runtime.model.ViewHelper; 063import org.ametys.runtime.model.ViewItemAccessor; 064import org.ametys.runtime.model.exception.BadItemTypeException; 065import org.ametys.runtime.model.exception.NotUniqueTypeException; 066import org.ametys.runtime.model.exception.UndefinedItemPathException; 067import org.ametys.runtime.model.exception.UnknownTypeException; 068import org.ametys.runtime.model.type.DataContext; 069 070/** 071 * Class for model aware repeaters 072 */ 073public class DefaultModelAwareRepeater implements IndexableRepeater 074{ 075 private static final Logger __LOGGER = LoggerFactory.getLogger(ModelAwareDataHolder.class); 076 077 /** Definition of this repeater */ 078 protected RepeaterDefinition _definition; 079 080 /** Parent of the current {@link Repeater} */ 081 protected IndexableDataHolder _parent; 082 083 /** Root {@link DataHolder} */ 084 protected IndexableDataHolder _root; 085 086 /** Repository data to use to store entries in the repository */ 087 protected RepositoryData _repositoryData; 088 089 /** 090 * Creates a model aware repeater 091 * @param repositoryData the repository data of the repeater 092 * @param definition the definition of the repeater 093 * @param parent the parent of the created {@link Repeater} 094 * @param root the root {@link DataHolder} 095 */ 096 public DefaultModelAwareRepeater(RepositoryData repositoryData, RepeaterDefinition definition, IndexableDataHolder parent, IndexableDataHolder root) 097 { 098 _repositoryData = repositoryData; 099 _definition = definition; 100 _parent = parent; 101 _root = root; 102 } 103 104 @Override 105 public List<? extends IndexableRepeaterEntry> getEntries() 106 { 107 SortedSet<IndexableRepeaterEntry> entries = new TreeSet<>(new Comparator<ModelAwareRepeaterEntry>() 108 { 109 public int compare(ModelAwareRepeaterEntry entry1, ModelAwareRepeaterEntry entry2) 110 { 111 return Integer.compare(entry1.getPosition(), entry2.getPosition()); 112 } 113 }); 114 115 for (String entryName : _repositoryData.getDataNames()) 116 { 117 IndexableRepeaterEntry entry = getEntry(Integer.parseInt(entryName)); 118 entries.add(entry); 119 } 120 121 return Collections.unmodifiableList(new ArrayList<>(entries)); 122 } 123 124 public IndexableRepeaterEntry getEntry(int position) 125 { 126 if (1 <= position && position <= getSize()) 127 { 128 RepositoryData entryRepositoryData = _repositoryData.getRepositoryData(String.valueOf(position)); 129 return new DefaultModelAwareRepeaterEntry(entryRepositoryData, _definition, this); 130 } 131 else if (-getSize() < position && position <= 0) 132 { 133 // Find the positive equivalent position and call the getEntry method with this position 134 return getEntry(getSize() + position); 135 } 136 else 137 { 138 return null; 139 } 140 } 141 142 public int getSize() 143 { 144 return _repositoryData.getDataNames().size(); 145 } 146 147 public boolean hasEntry(int position) 148 { 149 if (1 <= position) 150 { 151 return _repositoryData.hasValue(String.valueOf(position)); 152 } 153 else 154 { 155 return _repositoryData.hasValue(String.valueOf(getSize() + position)); 156 } 157 } 158 159 /** 160 * Retrieves the repeater's model 161 * @return the repeater's model 162 */ 163 public RepeaterDefinition getModel() 164 { 165 return _definition; 166 } 167 168 public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException 169 { 170 for (ModelAwareRepeaterEntry entry : getEntries()) 171 { 172 XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry)); 173 entry.dataToSAX(contentHandler, dataPath, context); 174 XMLUtils.endElement(contentHandler, "entry"); 175 } 176 } 177 178 public void dataToSAX(ContentHandler contentHandler, DataContext context) throws SAXException, BadItemTypeException 179 { 180 ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition); 181 dataToSAX(contentHandler, viewItemGroup, context); 182 } 183 184 public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 185 { 186 for (ModelAwareRepeaterEntry entry : getEntries()) 187 { 188 XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry)); 189 entry.dataToSAX(contentHandler, viewItemAccessor, context); 190 XMLUtils.endElement(contentHandler, "entry"); 191 } 192 } 193 194 public void dataToSAXForEdition(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 195 { 196 for (ModelAwareRepeaterEntry entry : getEntries()) 197 { 198 XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry)); 199 entry.dataToSAXForEdition(contentHandler, viewItemAccessor, context); 200 XMLUtils.endElement(contentHandler, "entry"); 201 } 202 } 203 204 private Attributes _getEntryAttributes(ModelAwareRepeaterEntry entry) 205 { 206 AttributesImpl entryAttrs = new AttributesImpl(); 207 String entryName = Integer.toString(entry.getPosition()); 208 entryAttrs.addCDATAAttribute("name", entryName); 209 return entryAttrs; 210 } 211 212 public Map<String, Object> dataToJSON(String dataPath, DataContext context) throws IOException 213 { 214 return _dataToJSON(Optional.of(dataPath), Optional.empty(), context, false); 215 } 216 217 public Map<String, Object> dataToJSON(DataContext context) throws BadItemTypeException 218 { 219 ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition); 220 return dataToJSON(viewItemGroup, context); 221 } 222 223 public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 224 { 225 return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context, false); 226 } 227 228 public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 229 { 230 return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context, true); 231 } 232 233 @SuppressWarnings("unchecked") 234 private Map<String, Object> _dataToJSON(Optional<String> dataPath, Optional<ViewItemAccessor> viewItemAccessor, DataContext context, boolean isEdition) throws BadItemTypeException 235 { 236 List<Map<String, Object>> entriesValues = new ArrayList<>(); 237 for (ModelAwareRepeaterEntry entry : getEntries()) 238 { 239 DataContext entryContext = context.cloneContext(); 240 if (StringUtils.isNotEmpty(context.getDataPath())) 241 { 242 entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]"); 243 } 244 245 Map<String, Object> entryValues = null; 246 if (dataPath.isPresent()) 247 { 248 entryValues = (Map<String, Object>) entry.dataToJSON(dataPath.get(), entryContext); 249 } 250 else if (viewItemAccessor.isPresent()) 251 { 252 entryValues = isEdition 253 ? entry.dataToJSONForEdition(viewItemAccessor.get(), entryContext) 254 : entry.dataToJSON(viewItemAccessor.get(), entryContext); 255 } 256 257 entriesValues.add(entryValues); 258 } 259 260 Map<String, Object> result = new HashMap<>(); 261 result.put("entryCount", getSize()); 262 result.put("entries", entriesValues); 263 result.put("label", _definition.getLabel()); 264 265 Optional.ofNullable(_definition.getHeaderLabel()) 266 .ifPresent(headerLabel -> result.put("header-label", headerLabel)); 267 268 return result; 269 } 270 271 public void copyTo(ModifiableRepeater repeater, DataContext context) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException 272 { 273 for (RepeaterEntry entry : getEntries()) 274 { 275 DataContext entryContext = context.cloneContext(); 276 if (StringUtils.isNotEmpty(context.getDataPath())) 277 { 278 entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]"); 279 } 280 281 ModifiableRepeaterEntry entryDestination = repeater.addEntry(entry.getPosition()); 282 entry.copyTo(entryDestination, entryContext); 283 } 284 } 285 286 public List<SolrInputDocument> indexData(SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException 287 { 288 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 289 String solrFieldName = solrFieldPrefix + context.getDataPathLastSegment(); 290 291 for (IndexableRepeaterEntry entry : getEntries()) 292 { 293 // Update the context with entry position 294 CMSDataContext newContext = context.cloneContext() 295 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 296 297 SolrInputDocument repeaterEntryDoc = new SolrInputDocument(); 298 299 if (!context.indexForFullTextField()) 300 { 301 // Creates a new Solr document for each entry 302 String repeaterEntryDocId = document.getField("id").getFirstValue().toString() + "/" + solrFieldName + "/" + entry.getPosition(); 303 repeaterEntryDoc.addField("id", repeaterEntryDocId); 304 repeaterEntryDoc.addField(SolrFieldNames.DOCUMENT_TYPE, SolrFieldNames.TYPE_REPEATER); 305 repeaterEntryDoc.addField(SolrFieldNames.REPEATER_ENTRY_POSITION, entry.getPosition()); 306 307 document.addField(solrFieldName + "_s_dv", repeaterEntryDocId); 308 } 309 310 // Add the created document to additional documents 311 additionalDocuments.add(repeaterEntryDoc); 312 313 ViewItemAccessor viewItemAccessor = context.getViewItem() 314 .map(ViewItemAccessor.class::cast) 315 .orElse(ViewHelper.createViewItemAccessor(entry.getModel())); 316 additionalDocuments.addAll(IndexableDataHolderHelper.indexData(entry, viewItemAccessor, repeaterEntryDoc, rootDocument, StringUtils.EMPTY, newContext)); 317 } 318 319 return additionalDocuments; 320 } 321 322 public boolean hasDifferences(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 323 { 324 return SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode()) 325 ? _hasDifferencesInAppendMode(viewItemAccessor, repeaterValues, context) 326 : SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode()) 327 ? _hasDifferencesInReplaceMode(viewItemAccessor, repeaterValues, context) 328 : _hasDifferencesInReplaceAllMode(viewItemAccessor, repeaterValues, context); 329 } 330 331 /** 332 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is APPEND 333 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 334 * @param repeaterValues the values of the repeater to check 335 * @param context the context of the synchronization 336 * @return <code>true</code> if there are differences, <code>false</code> otherwise 337 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 338 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 339 */ 340 protected boolean _hasDifferencesInAppendMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 341 { 342 boolean hasEntriesToAppend = !repeaterValues.getRemovedEntries().isEmpty() || !repeaterValues.getEntries().isEmpty(); 343 344 if (__LOGGER.isDebugEnabled()) 345 { 346 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 347 ? ViewHelper.getModelViewItemPath(modelViewItem) 348 : StringUtils.EMPTY; 349 if (hasEntriesToAppend) 350 { 351 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be appended", viewItemPath); 352 } 353 else 354 { 355 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 356 } 357 } 358 359 return hasEntriesToAppend; 360 } 361 362 /** 363 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE 364 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 365 * @param repeaterValues the values of the repeater to check 366 * @param context the context of the synchronization 367 * @return <code>true</code> if there are differences, <code>false</code> otherwise 368 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 369 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 370 */ 371 protected boolean _hasDifferencesInReplaceMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 372 { 373 List<Integer> positions = repeaterValues.getReplacePositions(); 374 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 375 376 for (int i = 0; i < positions.size(); i++) 377 { 378 int position = positions.get(i); 379 ModelAwareRepeaterEntry repeaterEntry = getEntry(position); 380 if (repeaterEntry.hasDifferences(viewItemAccessor, entriesValues.get(i), context)) 381 { 382 return true; 383 } 384 } 385 386 // No differences has been found in entries 387 if (__LOGGER.isDebugEnabled()) 388 { 389 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 390 ? ViewHelper.getModelViewItemPath(modelViewItem) 391 : StringUtils.EMPTY; 392 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 393 } 394 return false; 395 } 396 397 /** 398 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE_ALL 399 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 400 * @param repeaterValues the values of the repeater to check 401 * @param context the context of the synchronization 402 * @return <code>true</code> if there are differences, <code>false</code> otherwise 403 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 404 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 405 */ 406 protected boolean _hasDifferencesInReplaceAllMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 407 { 408 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 409 if (hasToMoveEntries(repeaterValues.getPositionsMapping(), entriesValues.size())) 410 { 411 if (__LOGGER.isDebugEnabled()) 412 { 413 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 414 ? ViewHelper.getModelViewItemPath(modelViewItem) 415 : StringUtils.EMPTY; 416 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be moved", viewItemPath); 417 } 418 return true; 419 } 420 421 for (ModelAwareRepeaterEntry repeaterEntry : getEntries()) 422 { 423 int entryIndex = repeaterEntry.getPosition() - 1; 424 Map<String, Object> entryValues = entriesValues.get(entryIndex); 425 if (repeaterEntry.hasDifferences(viewItemAccessor, entryValues, context)) 426 { 427 return true; 428 } 429 } 430 431 // No differences has been found in entries 432 if (__LOGGER.isDebugEnabled()) 433 { 434 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 435 ? ViewHelper.getModelViewItemPath(modelViewItem) 436 : StringUtils.EMPTY; 437 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 438 } 439 return false; 440 } 441 442 public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 443 { 444 return SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode()) 445 ? _getDifferencesInAppendMode(viewItemAccessor, repeaterValues, context) 446 : SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode()) 447 ? _getDifferencesInReplaceMode(viewItemAccessor, repeaterValues, context) 448 : _getDifferencesInReplaceAllMode(viewItemAccessor, repeaterValues, context); 449 } 450 451 /** 452 * Get the collection of model items where there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is APPEND 453 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 454 * @param repeaterValues the values of the repeater to check 455 * @param context the context of the synchronization 456 * @return a collection of model items with differences 457 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 458 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 459 */ 460 protected Collection<ModelItem> _getDifferencesInAppendMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 461 { 462 Set<ModelItem> modelItems = new HashSet<>(); 463 464 if (!repeaterValues.getRemovedEntries().isEmpty() || !repeaterValues.getEntries().isEmpty()) 465 { 466 modelItems.addAll(ViewHelper.getModelItems(viewItemAccessor)); 467 } 468 469 return modelItems; 470 } 471 472 /** 473 * Get the collection of model items where there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE 474 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 475 * @param repeaterValues the values of the repeater to check 476 * @param context the context of the synchronization 477 * @return a collection of model items with differences 478 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 479 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 480 */ 481 protected Collection<ModelItem> _getDifferencesInReplaceMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 482 { 483 Set<ModelItem> modelItems = new HashSet<>(); 484 485 List<Integer> positions = repeaterValues.getReplacePositions(); 486 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 487 488 for (int i = 0; i < positions.size(); i++) 489 { 490 int position = positions.get(i); 491 ModelAwareRepeaterEntry repeaterEntry = getEntry(position); 492 modelItems.addAll(repeaterEntry.getDifferences(viewItemAccessor, entriesValues.get(i), context)); 493 } 494 495 return modelItems; 496 } 497 498 /** 499 * Get the collection of model items where there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE_ALL 500 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 501 * @param repeaterValues the values of the repeater to check 502 * @param context the context of the synchronization 503 * @return a collection of model items with differences 504 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 505 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 506 */ 507 protected Collection<ModelItem> _getDifferencesInReplaceAllMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 508 { 509 Set<ModelItem> modelItems = new HashSet<>(); 510 511 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 512 if (hasToMoveEntries(repeaterValues.getPositionsMapping(), entriesValues.size())) 513 { 514 // Entries moving so everything would be updated 515 modelItems.addAll(ViewHelper.getModelItems(viewItemAccessor)); 516 } 517 else 518 { 519 for (ModelAwareRepeaterEntry repeaterEntry : getEntries()) 520 { 521 int entryIndex = repeaterEntry.getPosition() - 1; 522 Map<String, Object> entryValues = entriesValues.get(entryIndex); 523 modelItems.addAll(repeaterEntry.getDifferences(viewItemAccessor, entryValues, context)); 524 } 525 } 526 527 return modelItems; 528 } 529 530 public boolean hasToMoveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 531 { 532 int initialSize = getSize(); 533 534 if (targetSize != initialSize) 535 { 536 return true; 537 } 538 539 for (Map.Entry<Integer, Integer> mapping : positionsMapping.entrySet()) 540 { 541 if (!mapping.getKey().equals(mapping.getValue())) 542 { 543 return true; 544 } 545 } 546 547 return false; 548 } 549 550 public RepositoryData getRepositoryData() 551 { 552 return _repositoryData; 553 } 554 555 public IndexableDataHolder getParentDataHolder() 556 { 557 return _parent; 558 } 559 560 public IndexableDataHolder getRootDataHolder() 561 { 562 return _root; 563 } 564}