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 IndexableDataHolderHelper.externalDisableConditionsToSAX(getParentDataHolder(), contentHandler, viewItemAccessor, context); 204 } 205 206 private Attributes _getEntryAttributes(ModelAwareRepeaterEntry entry) 207 { 208 AttributesImpl entryAttrs = new AttributesImpl(); 209 String entryName = Integer.toString(entry.getPosition()); 210 entryAttrs.addCDATAAttribute("name", entryName); 211 return entryAttrs; 212 } 213 214 public Map<String, Object> dataToJSON(String dataPath, DataContext context) throws IOException 215 { 216 return _dataToJSON(Optional.of(dataPath), Optional.empty(), context, false); 217 } 218 219 public Map<String, Object> dataToJSON(DataContext context) throws BadItemTypeException 220 { 221 ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition); 222 return dataToJSON(viewItemGroup, context); 223 } 224 225 public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 226 { 227 return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context, false); 228 } 229 230 public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 231 { 232 return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context, true); 233 } 234 235 @SuppressWarnings("unchecked") 236 private Map<String, Object> _dataToJSON(Optional<String> dataPath, Optional<ViewItemAccessor> viewItemAccessor, DataContext context, boolean isEdition) throws BadItemTypeException 237 { 238 List<Map<String, Object>> entriesValues = new ArrayList<>(); 239 for (ModelAwareRepeaterEntry entry : getEntries()) 240 { 241 DataContext entryContext = context.cloneContext(); 242 if (StringUtils.isNotEmpty(context.getDataPath())) 243 { 244 entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]"); 245 } 246 247 Map<String, Object> entryValues = null; 248 if (dataPath.isPresent()) 249 { 250 entryValues = (Map<String, Object>) entry.dataToJSON(dataPath.get(), entryContext); 251 } 252 else if (viewItemAccessor.isPresent()) 253 { 254 entryValues = isEdition 255 ? entry.dataToJSONForEdition(viewItemAccessor.get(), entryContext) 256 : entry.dataToJSON(viewItemAccessor.get(), entryContext); 257 } 258 259 entriesValues.add(entryValues); 260 } 261 262 Map<String, Object> result = new HashMap<>(); 263 result.put("entryCount", getSize()); 264 result.put("entries", entriesValues); 265 result.put("label", _definition.getLabel()); 266 267 Optional.ofNullable(_definition.getHeaderLabel()) 268 .ifPresent(headerLabel -> result.put("header-label", headerLabel)); 269 270 if (isEdition && viewItemAccessor.isPresent()) 271 { 272 Map<String, Boolean> conditionsValues = IndexableDataHolderHelper.getExternalDisableConditionsValues(getParentDataHolder(), viewItemAccessor.get(), context); 273 if (!conditionsValues.isEmpty()) 274 { 275 result.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 276 } 277 } 278 279 return result; 280 } 281 282 public void copyTo(ModifiableRepeater repeater, DataContext context) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException 283 { 284 for (RepeaterEntry entry : getEntries()) 285 { 286 DataContext entryContext = context.cloneContext(); 287 if (StringUtils.isNotEmpty(context.getDataPath())) 288 { 289 entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]"); 290 } 291 292 ModifiableRepeaterEntry entryDestination = repeater.addEntry(entry.getPosition()); 293 entry.copyTo(entryDestination, entryContext); 294 } 295 } 296 297 public List<SolrInputDocument> indexData(SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException 298 { 299 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 300 String solrFieldName = solrFieldPrefix + context.getDataPathLastSegment(); 301 302 for (IndexableRepeaterEntry entry : getEntries()) 303 { 304 // Update the context with entry position 305 CMSDataContext newContext = context.cloneContext() 306 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 307 308 SolrInputDocument repeaterEntryDoc = new SolrInputDocument(); 309 310 if (!context.indexForFullTextField()) 311 { 312 // Creates a new Solr document for each entry 313 String repeaterEntryDocId = document.getField("id").getFirstValue().toString() + "/" + solrFieldName + "/" + entry.getPosition(); 314 repeaterEntryDoc.addField("id", repeaterEntryDocId); 315 repeaterEntryDoc.addField(SolrFieldNames.DOCUMENT_TYPE, SolrFieldNames.TYPE_REPEATER); 316 repeaterEntryDoc.addField(SolrFieldNames.REPEATER_ENTRY_POSITION, entry.getPosition()); 317 318 document.addField(solrFieldName + "_s_dv", repeaterEntryDocId); 319 } 320 321 // Add the created document to additional documents 322 additionalDocuments.add(repeaterEntryDoc); 323 324 ViewItemAccessor viewItemAccessor = context.getViewItem() 325 .map(ViewItemAccessor.class::cast) 326 .orElse(ViewHelper.createViewItemAccessor(entry.getModel())); 327 additionalDocuments.addAll(IndexableDataHolderHelper.indexData(entry, viewItemAccessor, repeaterEntryDoc, rootDocument, StringUtils.EMPTY, newContext)); 328 } 329 330 return additionalDocuments; 331 } 332 333 public boolean hasDifferences(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 334 { 335 return SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode()) 336 ? _hasDifferencesInAppendMode(viewItemAccessor, repeaterValues, context) 337 : SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode()) 338 ? _hasDifferencesInReplaceMode(viewItemAccessor, repeaterValues, context) 339 : _hasDifferencesInReplaceAllMode(viewItemAccessor, repeaterValues, context); 340 } 341 342 /** 343 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is APPEND 344 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 345 * @param repeaterValues the values of the repeater to check 346 * @param context the context of the synchronization 347 * @return <code>true</code> if there are differences, <code>false</code> otherwise 348 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 349 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 350 */ 351 protected boolean _hasDifferencesInAppendMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 352 { 353 boolean hasEntriesToAppend = !repeaterValues.getRemovedEntries().isEmpty() || !repeaterValues.getEntries().isEmpty(); 354 355 if (__LOGGER.isDebugEnabled()) 356 { 357 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 358 ? ViewHelper.getModelViewItemPath(modelViewItem) 359 : StringUtils.EMPTY; 360 if (hasEntriesToAppend) 361 { 362 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be appended", viewItemPath); 363 } 364 else 365 { 366 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 367 } 368 } 369 370 return hasEntriesToAppend; 371 } 372 373 /** 374 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE 375 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 376 * @param repeaterValues the values of the repeater to check 377 * @param context the context of the synchronization 378 * @return <code>true</code> if there are differences, <code>false</code> otherwise 379 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 380 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 381 */ 382 protected boolean _hasDifferencesInReplaceMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 383 { 384 List<Integer> positions = repeaterValues.getReplacePositions(); 385 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 386 387 for (int i = 0; i < positions.size(); i++) 388 { 389 int position = positions.get(i); 390 ModelAwareRepeaterEntry repeaterEntry = getEntry(position); 391 if (repeaterEntry.hasDifferences(viewItemAccessor, entriesValues.get(i), context)) 392 { 393 return true; 394 } 395 } 396 397 // No differences has been found in entries 398 if (__LOGGER.isDebugEnabled()) 399 { 400 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 401 ? ViewHelper.getModelViewItemPath(modelViewItem) 402 : StringUtils.EMPTY; 403 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 404 } 405 return false; 406 } 407 408 /** 409 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE_ALL 410 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 411 * @param repeaterValues the values of the repeater to check 412 * @param context the context of the synchronization 413 * @return <code>true</code> if there are differences, <code>false</code> otherwise 414 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 415 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 416 */ 417 protected boolean _hasDifferencesInReplaceAllMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 418 { 419 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 420 if (hasToMoveEntries(repeaterValues.getPositionsMapping(), entriesValues.size())) 421 { 422 if (__LOGGER.isDebugEnabled()) 423 { 424 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 425 ? ViewHelper.getModelViewItemPath(modelViewItem) 426 : StringUtils.EMPTY; 427 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be moved", viewItemPath); 428 } 429 return true; 430 } 431 432 for (ModelAwareRepeaterEntry repeaterEntry : getEntries()) 433 { 434 int entryIndex = repeaterEntry.getPosition() - 1; 435 Map<String, Object> entryValues = entriesValues.get(entryIndex); 436 if (repeaterEntry.hasDifferences(viewItemAccessor, entryValues, context)) 437 { 438 return true; 439 } 440 } 441 442 // No differences has been found in entries 443 if (__LOGGER.isDebugEnabled()) 444 { 445 String viewItemPath = viewItemAccessor instanceof ModelViewItem modelViewItem 446 ? ViewHelper.getModelViewItemPath(modelViewItem) 447 : StringUtils.EMPTY; 448 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 449 } 450 return false; 451 } 452 453 public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 454 { 455 return SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode()) 456 ? _getDifferencesInAppendMode(viewItemAccessor, repeaterValues, context) 457 : SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode()) 458 ? _getDifferencesInReplaceMode(viewItemAccessor, repeaterValues, context) 459 : _getDifferencesInReplaceAllMode(viewItemAccessor, repeaterValues, context); 460 } 461 462 /** 463 * 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 464 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 465 * @param repeaterValues the values of the repeater to check 466 * @param context the context of the synchronization 467 * @return a collection of model items with differences 468 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 469 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 470 */ 471 protected Collection<ModelItem> _getDifferencesInAppendMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 472 { 473 Set<ModelItem> modelItems = new HashSet<>(); 474 475 if (!repeaterValues.getRemovedEntries().isEmpty() || !repeaterValues.getEntries().isEmpty()) 476 { 477 modelItems.addAll(ViewHelper.getModelItems(viewItemAccessor)); 478 } 479 480 return modelItems; 481 } 482 483 /** 484 * 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 485 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 486 * @param repeaterValues the values of the repeater to check 487 * @param context the context of the synchronization 488 * @return a collection of model items with differences 489 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 490 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 491 */ 492 protected Collection<ModelItem> _getDifferencesInReplaceMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 493 { 494 Set<ModelItem> modelItems = new HashSet<>(); 495 496 List<Integer> positions = repeaterValues.getReplacePositions(); 497 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 498 499 for (int i = 0; i < positions.size(); i++) 500 { 501 int position = positions.get(i); 502 ModelAwareRepeaterEntry repeaterEntry = getEntry(position); 503 modelItems.addAll(repeaterEntry.getDifferences(viewItemAccessor, entriesValues.get(i), context)); 504 } 505 506 return modelItems; 507 } 508 509 /** 510 * 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 511 * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check 512 * @param repeaterValues the values of the repeater to check 513 * @param context the context of the synchronization 514 * @return a collection of model items with differences 515 * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model 516 * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value 517 */ 518 protected Collection<ModelItem> _getDifferencesInReplaceAllMode(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 519 { 520 Set<ModelItem> modelItems = new HashSet<>(); 521 522 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 523 if (hasToMoveEntries(repeaterValues.getPositionsMapping(), entriesValues.size())) 524 { 525 // Entries moving so everything would be updated 526 modelItems.addAll(ViewHelper.getModelItems(viewItemAccessor)); 527 } 528 else 529 { 530 for (ModelAwareRepeaterEntry repeaterEntry : getEntries()) 531 { 532 int entryIndex = repeaterEntry.getPosition() - 1; 533 Map<String, Object> entryValues = entriesValues.get(entryIndex); 534 modelItems.addAll(repeaterEntry.getDifferences(viewItemAccessor, entryValues, context)); 535 } 536 } 537 538 return modelItems; 539 } 540 541 public boolean hasToMoveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 542 { 543 int initialSize = getSize(); 544 545 if (targetSize != initialSize) 546 { 547 return true; 548 } 549 550 for (Map.Entry<Integer, Integer> mapping : positionsMapping.entrySet()) 551 { 552 if (!mapping.getKey().equals(mapping.getValue())) 553 { 554 return true; 555 } 556 } 557 558 return false; 559 } 560 561 public RepositoryData getRepositoryData() 562 { 563 return _repositoryData; 564 } 565 566 public IndexableDataHolder getParentDataHolder() 567 { 568 return _parent; 569 } 570 571 public IndexableDataHolder getRootDataHolder() 572 { 573 return _root; 574 } 575}