001/*
002 *  Copyright 2016 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.search.cocoon;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.components.ContextHelper;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.excalibur.xml.sax.ContentHandlerProxy;
034import org.xml.sax.Attributes;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.contenttype.ContentConstants;
039import org.ametys.cms.contenttype.MetadataDefinition;
040import org.ametys.cms.contenttype.MetadataManager;
041import org.ametys.cms.contenttype.MetadataType;
042import org.ametys.cms.contenttype.RepeaterDefinition;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.search.model.MetadataResultField;
045import org.ametys.cms.search.model.ResultField;
046import org.ametys.cms.search.model.SystemProperty;
047import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.UnknownAmetysObjectException;
050import org.ametys.plugins.repository.metadata.CompositeMetadata;
051import org.ametys.runtime.parameter.ParameterHelper;
052
053/**
054 * Helper to SAX result of a search based on the requested result fields.
055 *
056 */
057public class ContentResultSetHelper extends MetadataManager
058{
059    /** Avalon Role. */
060    @SuppressWarnings("hiding")
061    public static final String ROLE = ContentResultSetHelper.class.getName();
062
063    private static final String __RESULT_FIELD_PATH_ATTRIBUTE = "fieldPath";
064    
065    private SystemPropertyExtensionPoint _systemPropEP;
066
067    @Override
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        super.service(smanager);
071        _systemPropEP = (SystemPropertyExtensionPoint) smanager.lookup(SystemPropertyExtensionPoint.ROLE);
072    }
073    
074    /**
075     * SAXes the result fields of a content. This method does not check rights.
076     * @param contentHandler the content handler where to SAX into.
077     * @param content the content.
078     * @param fields the result fields.
079     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
080     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
081     * @throws AmetysRepositoryException if an error occurs.
082     * @throws SAXException if an error occurs.
083     * @throws IOException if an error occurs.
084     */
085    public void saxResultFields(ContentHandler contentHandler, Content content, Collection<? extends ResultField> fields, Locale defaultLocale) throws AmetysRepositoryException, SAXException, IOException
086    {
087        SearchResultFieldSet resultSet = buildResultSet(fields, content);
088        _saxResultSet(contentHandler, content, content.getMetadataHolder(), resultSet, null, defaultLocale, "", false);
089    }
090    
091    /**
092     * SAXes the result fields of a content. This method returns only readable fields (user rights are checked)
093     * @param contentHandler the content handler where to SAX into.
094     * @param content the content.
095     * @param fields the result fields.
096     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
097     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
098     * @throws AmetysRepositoryException if an error occurs.
099     * @throws SAXException if an error occurs.
100     * @throws IOException if an error occurs.
101     */
102    public void saxReadableResultFields(ContentHandler contentHandler, Content content, Collection<? extends ResultField> fields, Locale defaultLocale) throws AmetysRepositoryException, SAXException, IOException
103    {
104        SearchResultFieldSet resultSet = buildResultSet(fields, content);
105        _saxResultSet(contentHandler, content, content.getMetadataHolder(), resultSet, null, defaultLocale, "", true);
106    }
107    
108    /**
109     * SAXes a content result set. This method does not check rights.
110     * @param contentHandler the content handler where to SAX into.
111     * @param content the content.
112     * @param resultSet the set of result fields to SAX
113     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. Only use if initial content's language is not null. Can be null.
114     * @throws AmetysRepositoryException if an error occurs.
115     * @throws SAXException if an error occurs.
116     * @throws IOException if an error occurs.
117     */
118    public void saxResultSet(ContentHandler contentHandler, Content content, SearchResultFieldSet resultSet, Locale defaultLocale) throws AmetysRepositoryException, SAXException, IOException
119    {
120        _saxResultSet(contentHandler, content, content.getMetadataHolder(), resultSet, null, defaultLocale, "", false);
121    }
122    
123    /**
124     * SAX a content metadata set that are readable.
125     * @param contentHandler the content handler where to SAX into.
126     * @param content the content.
127     * @param resultSet the set of result fields to SAX
128     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
129     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
130     * @throws AmetysRepositoryException if an error occurs.
131     * @throws SAXException if an error occurs.
132     * @throws IOException if an error occurs.
133     */
134    public void saxReadableResultSet (ContentHandler contentHandler, Content content, SearchResultFieldSet resultSet, Locale defaultLocale) throws AmetysRepositoryException, SAXException, IOException
135    {
136        _saxResultSet(contentHandler, content, content.getMetadataHolder(), resultSet, null, defaultLocale, "", true);
137    }
138    
139    private void _saxResultSet(ContentHandler contentHandler, Content content, CompositeMetadata metadata, SearchResultFieldSet resultSetElement, MetadataDefinition parentMetadataDef, Locale defaultLocale, String prefix, boolean checkRead) throws AmetysRepositoryException, SAXException, IOException
140    {
141        if (resultSetElement instanceof SearchResultFieldSetElement)
142        {
143            String elmtId = ((SearchResultFieldSetElement) resultSetElement).getName();
144            
145            MetadataDefinition metadataDef = _getMetadataDefinition(elmtId, parentMetadataDef, content);
146            
147            if (metadataDef != null)
148            {
149                _saxMetadata(contentHandler, content, metadata, resultSetElement, metadataDef, defaultLocale, prefix, checkRead);
150            }
151            else if (_systemPropEP.hasExtension(elmtId))
152            {
153                ResultFieldContentHandler wrappedHandler = new ResultFieldContentHandler(contentHandler, ((SearchResultFieldSetElement) resultSetElement).getPath());
154                saxSystemProperty(wrappedHandler, elmtId, content);
155            }
156            else
157            {
158                // TODO Custom field
159            }
160        }
161        else
162        {
163            for (SearchResultFieldSetElement subElement : resultSetElement.getElements())
164            {
165                _saxResultSet(contentHandler, content, metadata, subElement, parentMetadataDef, defaultLocale, prefix, checkRead);
166            }
167        }
168    }
169    
170    /**
171     * SAX a metadata
172     * @param contentHandler The content handler where to SAX into.
173     * @param content The content
174     * @param metadata The parent composite metadata.
175     * @param resultSet the set of remaining result fields to SAX
176     * @param metadataDefinition The metadata definition
177     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. Only use if initial content's language is not null. Can be null.
178     * @param prefix the metadata path prefix.
179     * @param checkRead true if need to check read right
180     * @throws AmetysRepositoryException if an error occurred
181     * @throws SAXException if an error occurred while SAXing
182     * @throws IOException if an error occurred
183     */
184    protected void _saxMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadata, SearchResultFieldSet resultSet, MetadataDefinition metadataDefinition, Locale defaultLocale, String prefix, boolean checkRead) throws AmetysRepositoryException, SAXException, IOException
185    {
186        String metadataName = metadataDefinition.getName();
187        
188        ContentHandler wrappedHandler = resultSet instanceof SearchResultFieldSetElement ? new ResultFieldContentHandler(contentHandler, ((SearchResultFieldSetElement) resultSet).getPath()) : contentHandler;
189        
190        if (metadata.hasMetadata(metadataName))
191        {
192            MetadataType type = metadataDefinition.getType();
193            switch (type)
194            {
195                case COMPOSITE:
196                    _saxCompositeMetadata(contentHandler, content, metadata, metadataDefinition, metadataName, resultSet, defaultLocale, prefix, checkRead);
197                    break;
198                case BINARY:
199                    _saxBinaryMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
200                    break;
201                    
202                case FILE:
203                    _saxFileMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
204                    break;
205                    
206                case RICH_TEXT:
207                    _saxRichTextMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
208                    break;
209                    
210                case DATE:
211                case DATETIME:
212                    _saxSingleDateMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead);
213                    break;
214                    
215                case USER:
216                    _saxUserMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
217                    break;
218                    
219                case CONTENT:
220                    _saxContentReferenceMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, resultSet, defaultLocale, prefix, checkRead);
221                    break;
222                    
223                case SUB_CONTENT:
224                    // TODO To remove ?
225                    break;
226                    
227                case GEOCODE:
228                    _saxGeocodeMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
229                    break;
230                    
231                case REFERENCE:
232                    _saxReferenceMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, false);
233                    break;
234                    
235                case MULTILINGUAL_STRING:
236                    _saxMultilingualStringMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, defaultLocale, prefix, checkRead, false);
237                    break;
238                    
239                default:
240                    _saxStringMetadata(wrappedHandler, content, metadata, metadataDefinition, metadataName, prefix, checkRead, metadataDefinition.getEnumerator());
241                    break;
242            }
243        }
244    }
245
246    /**
247     * SAX a composite metadata.
248     * @param contentHandler the content handler to SAX into.
249     * @param content the content.
250     * @param metadata the parent metadata holder.
251     * @param metadataDefinition the metadata definition.
252     * @param metadataName The name of the metadata to sax
253     * @param resultSet the set of result fields to SAX for this composite
254     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. Only use if initial content's language is not null. Can be null.
255     * @param prefix the metadata path prefix.
256     * @param checkRead true if need to check read right.
257     * @throws SAXException if an error occurs.
258     * @throws IOException if an error occurs.
259     */
260    protected void _saxCompositeMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadata, MetadataDefinition metadataDefinition, String metadataName, SearchResultFieldSet resultSet, Locale defaultLocale, String prefix, boolean checkRead) throws SAXException, IOException
261    {
262        CompositeMetadata subMetadata = metadata.getCompositeMetadata(metadataName);
263        
264        AttributesImpl attrs = new AttributesImpl();
265        if (metadataDefinition instanceof RepeaterDefinition)
266        {
267            attrs.addCDATAAttribute("entryCount", Integer.toString(subMetadata.getMetadataNames().length));
268        }
269        
270        XMLUtils.startElement(contentHandler, metadataName, attrs);
271        
272        if (metadataDefinition instanceof RepeaterDefinition)
273        {
274            String[] subMetadataNames = subMetadata.getMetadataNames();
275            Arrays.sort(subMetadataNames, MetadataManager.REPEATER_ENTRY_COMPARATOR);
276            
277            for (String entryName : subMetadataNames)
278            {
279                org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType entryType = subMetadata.getType(entryName);
280                
281                if (entryType != org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.COMPOSITE)
282                {
283                    throw new AmetysRepositoryException("Invalid type: " + entryType + " for metadata: " + metadataName + " and entry of name: " + entryName);
284                }
285                
286                CompositeMetadata entry = subMetadata.getCompositeMetadata(entryName);
287                
288                AttributesImpl entryAttrs = new AttributesImpl();
289                entryAttrs.addCDATAAttribute("name", entryName);
290                XMLUtils.startElement(contentHandler, "entry", entryAttrs);
291
292                for (SearchResultFieldSetElement subElement : resultSet.getElements())
293                {
294                    // SAX all result fields contains in the current SearchResultFieldSet
295                    _saxResultSet(contentHandler, content, entry, subElement, metadataDefinition, defaultLocale, prefix + metadataName + "/" + entryName + "/", checkRead);
296                }
297                
298                XMLUtils.endElement(contentHandler, "entry");
299            }
300        }
301        else
302        {
303            // SAX all result fields contains in the current SearchResultFieldSet
304            for (SearchResultFieldSetElement subElement : resultSet.getElements())
305            {
306                _saxResultSet(contentHandler, content, subMetadata, subElement, metadataDefinition, defaultLocale, prefix + metadataName + "/", checkRead);
307            }
308        }
309
310        XMLUtils.endElement(contentHandler, metadataName);
311    }
312    
313    /**
314     * SAX a "content" metadata.
315     * @param contentHandler the content handler to SAX into.
316     * @param content The currently saxed content.
317     * @param metadatas the parent composite metadata.
318     * @param metadataDefinition the metadata definition.
319     * @param metadataName the metadata name.
320     * @param resultSet the set of result fields to SAX for the referenced contents
321     * @param defaultLocale The locale to use to resolve localized values of referenced content. Only use if initial content's language is not null.
322     * @param prefix the metadata path prefix.
323     * @param checkRead true if need to check read right.
324     * @throws SAXException if an error occurs.
325     * @throws IOException if an error occurs.
326     */
327    protected void _saxContentReferenceMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadatas, MetadataDefinition metadataDefinition, String metadataName, SearchResultFieldSet resultSet, Locale defaultLocale, String prefix, boolean checkRead) throws SAXException, IOException
328    {
329        if (!checkRead || _canRead(content, metadataDefinition))
330        {
331            Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale;
332            String[] values = metadatas.getStringArray(metadataName);
333            
334            for (String value : values)
335            {
336                Request request = ContextHelper.getRequest(_context);
337
338                Object oldContentAttribute = request.getAttribute(Content.class.getName());
339
340                try
341                {
342                    request.setAttribute(Content.class.getName(), content);
343                    
344                    Content refContent = _resolver.resolveById(value);
345                    _saxContent(contentHandler, refContent, locale, metadataName, resultSet, prefix, checkRead);
346                }
347                catch (UnknownAmetysObjectException e)
348                {
349                    if (getLogger().isInfoEnabled())
350                    {
351                        getLogger().info("The content of ID '" + content.getId() + "' references a non-existing content in the metadata '" + metadataDefinition.getId() + "'", e);
352                    }
353                }
354                finally
355                {
356                    request.setAttribute(Content.class.getName(), oldContentAttribute);
357                }
358            }
359        }        
360    }
361    
362    /**
363     * SAX a content (referenced or sub-content) in view mode.
364     * @param contentHandler the content handler to SAX into.
365     * @param content The referenced or sub-content to SAX.
366     * @param defaultLocale The locale to use to sax the referenced content's localized values. Only use if initial content 's language is null.
367     * @param metadataName the metadata name.
368     * @param resultSet the set of result fields to SAX for this content
369     * @param prefix the metadata path prefix.
370     * @param checkRead true if need to check read right.
371     * @throws SAXException if an error occurs.
372     * @throws IOException if an error occurs.
373     */
374    protected void _saxContent(ContentHandler contentHandler, Content content, Locale defaultLocale, String metadataName, SearchResultFieldSet resultSet, String prefix, boolean checkRead) throws SAXException, IOException
375    {
376        AttributesImpl attrs = new AttributesImpl();
377        attrs.addCDATAAttribute("id", content.getId());
378        attrs.addCDATAAttribute("name", content.getName());
379        attrs.addCDATAAttribute("title", content.getTitle(defaultLocale));
380        if (content.getLanguage() != null)
381        {
382            attrs.addCDATAAttribute("language", content.getLanguage());
383        }
384        attrs.addCDATAAttribute("createdAt", ParameterHelper.valueToString(content.getCreationDate()));
385        attrs.addCDATAAttribute("creator", content.getCreator().getLogin());
386        attrs.addCDATAAttribute("lastModifiedAt", ParameterHelper.valueToString(content.getLastModified()));
387        
388        if (resultSet instanceof SearchResultFieldSetElement)
389        {
390            attrs.addCDATAAttribute(__RESULT_FIELD_PATH_ATTRIBUTE, ((SearchResultFieldSetElement) resultSet).getPath());
391        }
392        XMLUtils.startElement(contentHandler, metadataName, attrs);
393        
394        if (resultSet != null && resultSet.getElements().size() > 0)
395        {
396            SearchResultFieldSet refResultSet = new SearchResultFieldSet();
397            refResultSet.addElements(resultSet.getElements());
398            
399            Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 
400            _saxResultSet(contentHandler, content, content.getMetadataHolder(), refResultSet, null, locale, prefix, checkRead);
401        }
402        
403        XMLUtils.endElement(contentHandler, metadataName);
404    }
405    
406    /**
407     * SAX a system property
408     * @param contentHandler the content handler to SAX into.
409     * @param systemPropertyId The id of system property
410     * @param content The content
411     * @throws SAXException if an error occurs.
412     */
413    public void saxSystemProperty(ContentHandler contentHandler, String systemPropertyId, Content content) throws SAXException
414    {
415        SystemProperty systemProp = _systemPropEP.getExtension(systemPropertyId);
416        if (systemProp != null)
417        {
418            systemProp.saxValue(contentHandler, content);
419        }
420    }
421    
422    /**
423     * Build a result set from result fields
424     * @param fields The result fields
425     * @param content The result content
426     * @return The result set
427     */
428    public SearchResultFieldSet buildResultSet (Collection<? extends ResultField> fields, Content content)
429    {
430        SearchResultFieldSet resultSet = new SearchResultFieldSet();
431        
432        SearchResultFieldSet resultSetElmt = resultSet;
433        for (ResultField field : fields)
434        {
435            String fieldPath = field instanceof MetadataResultField ? ((MetadataResultField) field).getFieldPath() : field.getId();
436            String[] pathSegments = fieldPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
437            
438            MetadataDefinition metadataDef = null;
439            
440            String currentFieldPath = null;
441            for (String pathSegment : pathSegments)
442            {
443                currentFieldPath = currentFieldPath == null ? pathSegment : currentFieldPath + ContentConstants.METADATA_PATH_SEPARATOR + pathSegment;
444                metadataDef = _getMetadataDefinition(pathSegment, metadataDef, content);
445                
446                if (metadataDef != null)
447                {
448                    SearchResultFieldSetElement element = resultSetElmt.getElement(pathSegment);
449                    if (element == null)
450                    {
451                        element = new SearchResultFieldSetElement(pathSegment, currentFieldPath);
452                    }
453                    
454                    // This part of the path represents a metadata field, continue
455                    resultSetElmt.addElement(element);
456                    resultSetElmt = element;
457                }
458                else
459                {
460                    resultSetElmt.addElement(new SearchResultFieldSetElement(pathSegment, currentFieldPath));
461                    // This part of the path represents a system property field or a custom field
462                    // Stop iteration : a system property can be only at end of the path
463                    break;
464                }
465            }
466            
467            resultSetElmt = resultSet;
468        }
469        
470        return resultSet;
471    }
472    
473    private MetadataDefinition _getMetadataDefinition(String metadataName, MetadataDefinition parentMetadataDef, Content content)
474    {
475        if (parentMetadataDef == null)
476        {
477            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
478        }
479        
480        if (parentMetadataDef.getType().equals(MetadataType.CONTENT))
481        {
482            return _contentTypesHelper.getMetadataDefinition(metadataName, new String[] {parentMetadataDef.getContentType()}, org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY);
483        }
484        
485        return parentMetadataDef.getMetadataDefinition(metadataName);
486    }
487    
488       
489    /**
490     * Inner class representing a element of a {@link SearchResultFieldSet}
491     * This can contains sub elements.
492     *
493     */
494    public class SearchResultFieldSetElement extends SearchResultFieldSet
495    {
496        private String _name;
497        private String _fieldPath;
498        
499        /**
500         * Creates a new element representing a result field
501         * @param name The unique name of result field into its parent set
502         * @param fieldPath The path of result field into its parent set
503         */
504        public SearchResultFieldSetElement(String name, String fieldPath)
505        {
506            super();
507            _name = name;
508            _fieldPath = fieldPath;
509        }
510        
511        /**
512         * Get the name of this element
513         * @return The name
514         */
515        public String getName()
516        {
517            return _name;
518        }
519        
520        /**
521         * Get the path of this field  into its parent set
522         * @return the path
523         */
524        public String getPath()
525        {
526            return _fieldPath;
527        }
528    }
529    
530    /**
531     * Inner class representing a set of result fields.
532     *
533     */
534    public class SearchResultFieldSet
535    {
536        private Map<String, SearchResultFieldSetElement> _elements = new LinkedHashMap<>();
537        
538        /**
539         * Creates a set of a result field
540         */
541        public SearchResultFieldSet()
542        {
543            // Nothing
544        }
545                
546        /**
547         * Add a new element to this set
548         * @param element The element to add
549         * @return true if the element was added, false otherwise
550         */
551        public boolean addElement(SearchResultFieldSetElement element)
552        {
553            String id = element.getName();
554            
555            if (_elements.containsKey(id))
556            {
557                // Already existing metadata definition reference
558                return false;
559            }
560            
561            _elements.put(id, element);
562            
563            return true;
564        }
565        
566        /**
567         * Add a list of elements to this set
568         * @param elements The elements to add
569         */
570        public void addElements(List<SearchResultFieldSetElement> elements)
571        {
572            for (SearchResultFieldSetElement elmt : elements)
573            {
574                addElement(elmt);
575            }
576        }
577        
578        /**
579         * Get a element contains into this set by its name
580         * @param name The name of the element
581         * @return the element or <code>null</code> if not found
582         */
583        public SearchResultFieldSetElement getElement (String name)
584        {
585            return _elements.get(name);
586        }
587        
588        /**
589         * Determines if this set contains the given element
590         * @param name The name of the element
591         * @return <code>true</code> if exists
592         */
593        public boolean hasElement (String name)
594        {
595            return _elements.containsKey(name);
596        }
597        
598        /**
599         * Returns the elements of this set
600         * @return the element
601         */
602        public List<SearchResultFieldSetElement> getElements()
603        {
604            return new ArrayList<>(_elements.values());
605        }
606    }
607    
608    class ResultFieldContentHandler extends ContentHandlerProxy
609    {
610        /** Used to add fieldPath only on top level element */
611        private int _depth;
612        private String _resultFieldPath;
613        
614        ResultFieldContentHandler(ContentHandler wrappedHandler, String fieldPath)
615        {
616            super(wrappedHandler);
617            _resultFieldPath = fieldPath;
618            _depth = 0;
619        }
620        
621        @Override
622        public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException
623        {
624            if (_depth == 0)
625            {
626                AttributesImpl atts = new AttributesImpl(a);
627                atts.addCDATAAttribute(__RESULT_FIELD_PATH_ATTRIBUTE, _resultFieldPath);
628                super.startElement(uri, loc, raw, atts);
629            }
630            else
631            {
632                super.startElement(uri, loc, raw, a);
633            }
634            _depth++;
635        }
636
637        @Override
638        public void endElement(String uri, String loc, String raw) throws SAXException
639        {
640            super.endElement(uri, loc, raw);
641            _depth--;
642        }
643    }
644}