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}