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