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}