001/*
002 * ============================================================================
003 * Copyright © 2002-2025 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.xml.parse;
019
020import static java.lang.String.format;
021import static org.apiguardian.api.API.Status.MAINTAINED;
022import static org.apiguardian.api.API.Status.STABLE;
023import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
024import static org.tquadrat.foundation.lang.Objects.isNull;
025import static org.tquadrat.foundation.lang.Objects.nonNull;
026import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
027import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
028import static org.tquadrat.foundation.util.StringUtils.isEmptyOrBlank;
029import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
030
031import java.net.URI;
032import java.net.URISyntaxException;
033import java.util.HashMap;
034import java.util.Map;
035import java.util.Optional;
036import java.util.TreeMap;
037import java.util.stream.IntStream;
038
039import org.apiguardian.api.API;
040import org.tquadrat.foundation.annotation.ClassVersion;
041import org.tquadrat.foundation.annotation.MountPoint;
042import org.tquadrat.foundation.util.Stack;
043import org.xml.sax.Attributes;
044import org.xml.sax.ContentHandler;
045import org.xml.sax.Locator;
046import org.xml.sax.SAXException;
047import org.xml.sax.SAXParseException;
048import org.xml.sax.helpers.DefaultHandler;
049import org.xml.sax.helpers.LocatorImpl;
050
051/**
052 *  <p>{@summary This class implements the interface
053 *  {@link ContentHandler ContentHandler}
054 *  as a base class for more advanced versions of the
055 *  {@link DefaultHandler DefaultHandler class}
056 *  or for stand-alone use.}</p>
057 *  <p>Instead of implementing the three methods
058 *  {@link org.xml.sax.helpers.DefaultHandler#characters(char[],int,int) characters()},
059 *  {@link org.xml.sax.helpers.DefaultHandler#endElement(String,String,String) endElement()},
060 *  and
061 *  {@link org.xml.sax.helpers.DefaultHandler#startElement(String,String,String,Attributes) startElement()}
062 *  only handlers for the elements have to be implemented; after registration
063 *  of these handlers using
064 *  {@link #registerElementHandler(String, HandlerMethod)}
065 *  these handler methods will be called automatically by the default
066 *  implementations of
067 *  {@link #processElement(Element) processElement()}
068 *  and
069 *  {@link #openElement(Element) openElement()}.</p>
070 *  <p>These method can still be overwritten if a different processing is
071 *  desired. When
072 *  {@link #processElement(Element) processElement()}
073 *  is called after the element is terminated, the attributes together with the
074 *  character data after closing the element is provided. The method
075 *  {@link #openElement(Element) openElement()}
076 *  is called each time an element will be opened, providing the attributes
077 *  only.</p>
078 *  <p>Some convenience methods have been implemented that will give access
079 *  to the parent element and to the path down to the current element.</p>
080 *
081 *  <p><b>Note</b>: Unfortunately, this class do not work for XML streams
082 *  that has elements embedded into text, as it is usual for HTML. The
083 *  snippet</p>
084 *  <pre><code>&lt;p&gt;First Text &lt;b&gt;Bold Text&lt;/b&gt; Second Text&lt;/p&gt;</code></pre>
085 *  <p>will be parsed as &quot;First Text Second Text&quot; for the {@code p}
086 *  element and &quot;Bold Text&quot; for the {@code b} element; the
087 *  information that the b element was embedded in between is lost.</p>
088 *
089 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
090 *  @version $Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $
091 *  @since 0.0.5
092 *
093 *  @UMLGraph.link
094 */
095@SuppressWarnings( "AbstractClassExtendsConcreteClass" )
096@ClassVersion( sourceVersion = "$Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $" )
097@API( status = STABLE, since = "0.0.5" )
098public abstract class AdvancedContentHandler extends DefaultHandler
099{
100        /*---------------*\
101    ====** Inner Classes **====================================================
102        \*---------------*/
103    /**
104     *  This class serves a container for the name, the data and the attributes
105     *  of an XML element.
106     *
107     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
108     *  @version $Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $
109     *  @since 0.0.5
110     *
111     *  @UMLGraph.link
112     */
113    @SuppressWarnings( {"InnerClassMayBeStatic", "ProtectedInnerClass"} )
114    @ClassVersion( sourceVersion = "$Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $" )
115    @API( status = STABLE, since = "0.1.0" )
116    protected static final class Element
117    {
118            /*------------*\
119        ====** Attributes **===================================================
120            \*------------*/
121        /**
122         *  The attributes.
123         */
124        private final Map<String,Attribute> m_Attributes;
125
126        /**
127         *  The data.
128         */
129        @SuppressWarnings( "StringBufferField" )
130        private final StringBuilder m_Data;
131
132        /**
133         *  The element's local name.
134         */
135        private final String m_LocalName;
136
137        /**
138         *  The parent for this element.
139         */
140        @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
141        private final Optional<Element> m_Parent;
142
143        /**
144         *  The path to the element.
145         */
146        private final String m_Path;
147
148        /**
149         *  The element's qualified name.
150         */
151        private final String m_QName;
152
153        /**
154         *  The namespace for this element; if {@code null}, the
155         *  {@linkplain #m_QName qualified name}
156         *  and the
157         *  {@linkplain #m_LocalName local name}
158         *  are the same.
159         */
160        private final URI m_URI;
161
162            /*--------------*\
163        ====** Constructors **=================================================
164            \*--------------*/
165        /**
166         *  Create a new object of this class from an element's name and its
167         *  attributes.
168         *
169         *  @param  qName   The element's qualified name.
170         *  @param  localName   The element's local name.
171         *  @param  uri The namespace for the element; can be {@code null}.
172         *  @param  attributes  The element's attributes.
173         *  @param  path    The path to the element; this is a string, compiled
174         *      from the element's name, separated by slashes ("/").
175         *  @param  parent  The parent element for this element; may be
176         *      {@code null}.
177         */
178        @SuppressWarnings( "ConstructorWithTooManyParameters" )
179        Element( final String qName, final String localName, final URI uri, final Map<String,Attribute>  attributes, final String path, @SuppressWarnings( "UseOfConcreteClass" ) final Element parent )
180        {
181            m_QName = requireNotEmptyArgument( qName, "qName" );
182            m_LocalName = requireNotEmptyArgument( localName, "localName" );
183            m_URI = uri;
184            m_Attributes = Map.copyOf( attributes );
185            m_Path = path;
186            m_Data = new StringBuilder();
187            m_Parent = Optional.ofNullable( parent );
188        }   //  Element
189
190            /*---------*\
191        ====** Methods **======================================================
192            \*---------*/
193        /**
194         *  Adds another data chunk to the data block for the current element.
195         *
196         *  @param  characters  The characters.
197         *  @param  start   The start position inside the characters array.
198         *  @param  end The ending position inside the array.
199         */
200        public final void appendData( final char [] characters, final int start, final int end )
201        {
202            m_Data.append( characters, start, end );
203        }   //  appendData()
204
205        /**
206         *  Returns the attributes of the element.
207         *
208         *  @return The attributes.
209         */
210        public final Map<String,Attribute> getAttributes() { return m_Attributes; }
211
212        /**
213         *  Returns the data block for this element.
214         *
215         *  @return The data block.
216         */
217        public final String getData() { return m_Data.toString().trim(); }
218
219        /**
220         *  Returns the local name of the element.
221         *
222         *  @return The local name of the element.
223         */
224        public final String getLocalName() { return m_LocalName; }
225
226        /**
227         *  Returns the parent element.
228         *
229         *  @return An instance of
230         *      {@link Optional}
231         *      that holds the parent element; will be
232         *      {@linkplain Optional#empty() empty}
233         *      if this element does not have a parent.
234         */
235        public final Optional<Element> getParent() { return m_Parent; }
236
237        /**
238         *  Returns to XML path to this element.
239         *
240         *  @return The element's path.
241         */
242        public final String getPath() { return m_Path; }
243
244        /**
245         *  Returns the prefix from the element's qualified name.
246         *
247         *  @return The prefix; if there is no prefix, the empty String will be
248         *      returned.
249         */
250        public final String getPrefix()
251        {
252            final var pos = m_QName.indexOf( ":" );
253            final var retValue = pos > 0 ? m_QName.substring( 0, pos ) : EMPTY_STRING;
254
255            //---* Done *------------------------------------------------------
256            return retValue;
257        }   //  getPrefix()
258
259        /**
260         *  Returns the qualified name of the element.
261         *
262         *  @return The qualified name of the element.
263         */
264        public final String getQName() { return m_QName; }
265
266        /**
267         *  Returns the namespace URI of the element.
268         *
269         *  @return An instance of
270         *      {@link Optional}
271         *      that holds the namespace URI of the element.
272         */
273        public final Optional<URI> getURI() { return Optional.ofNullable( m_URI ); }
274
275        /**
276         *  {@inheritDoc}
277         */
278        @Override
279        public final String toString() { return getQName(); }
280    }
281    //  class Element
282
283    /**
284     *  The functional interface describing a method that processes an XML
285     *  element.
286     *
287     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
288     *  @version $Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $
289     *  @since 0.1.0
290     *
291     *  @UMLGraph.link
292     */
293    @SuppressWarnings( {"ProtectedInnerClass"} )
294    @FunctionalInterface
295    @ClassVersion( sourceVersion = "$Id: AdvancedContentHandler.java 1157 2025-12-31 14:05:44Z tquadrat $" )
296    @API( status = MAINTAINED, since = "0.1.0" )
297    protected interface HandlerMethod
298    {
299            /*---------*\
300        ====** Methods **======================================================
301            \*---------*/
302        /**
303         *  <p>{@summary Processes an XML element.}</p>
304         *  <p>As each element should have its own handler, the tag is not
305         *  provided as an argument. If necessary, the tag can be derived from
306         *  the {@code path} argument.</p>
307         *
308         *  @param  terminateElement {@code true} if called by
309         *      {@link #processElement(Element)},
310         *      indicating that the element processing will be terminated,
311         *      {@code false} when called by
312         *      {@link #openElement(Element)}.
313         *  @param  data    The element data; will be {@code null} if called
314         *      by
315         *      {@link #openElement(Element)}.
316         *  @param  attributes  The element attributes.
317         *  @param  path    The element path.
318         *  @throws SAXException    The element cannot be handled properly.
319         *
320         *  @since  0.1.0
321         */
322        public void process( final boolean terminateElement, final String data, final Map<String,Attribute> attributes, final String path ) throws SAXException;
323    }
324    //  interface HandlerMethod
325
326        /*-----------*\
327    ====** Constants **========================================================
328        \*-----------*/
329    /**
330     *  An empty array of Element objects.
331     */
332    private static final Element [] EMPTY_Element_ARRAY = new Element [0];
333
334    /**
335     *  The message indicating an invalid URI: {@value}.
336     */
337    public static final String MSG_InvalidURI = "Invalid namespace URI: %s";
338
339    /**
340     *  The message indicating that there is no element handler for the given
341     *  element: {@value}.
342     */
343    public static final String MSG_NoHandler = "No handler for element '%1$s'";
344
345    /**
346     *  The message indicating that there is no element on the stack: {@value}.
347     */
348    public static final String MSG_NoElementOnStack = "No element on stack";
349
350        /*------------*\
351    ====** Attributes **=======================================================
352        \*------------*/
353    /**
354     *  The document type.
355     */
356    private String m_DocumentType = null;
357
358    /**
359     *  This stack contains the open elements, stored as instances of
360     *  {@link Element}.
361     */
362    @SuppressWarnings( "UseOfConcreteClass" )
363    private final Stack<Element> m_ElementStack = new Stack<>();
364
365    /**
366     *  The element handler methods. The key for this map is the qualified
367     *  name of the element.
368     */
369    private final Map<String,HandlerMethod> m_HandlerMethods = new TreeMap<>();
370
371    /**
372     *  The locator.
373     */
374    private Locator m_Locator;
375
376    /**
377     *  The name spaces. The prefix is the key to the map, while the URI is the
378     *  value.
379     */
380    private final Map<String,URI> m_Namespaces = new HashMap<>();
381
382        /*--------------*\
383    ====** Constructors **=====================================================
384        \*--------------*/
385    /**
386     *  The default constructor.
387     */
388    protected AdvancedContentHandler() { /* Does nothing! */ }
389
390        /*---------*\
391    ====** Methods **==========================================================
392        \*---------*/
393    /**
394     *  Receives notification of character data inside an element.
395     *
396     *  @param  ch  The characters.
397     *  @param  start   The start position inside the characters array.
398     *  @param  length  The length of the subset to process.
399     *  @throws SAXException    Something has gone wrong.
400     */
401    @Override
402    public final void characters( final char [] ch, final int start, final int length ) throws SAXException
403    {
404        final var element = m_ElementStack
405            .peek()
406            .orElseThrow( () -> new SAXParseException( MSG_NoElementOnStack, getLocator() ) );
407        element.appendData( ch, start, length );
408    }   //  characters()
409
410    /**
411     *  Composes an
412     *  {@link Attribute}
413     *  instance from the data of the given
414     *  {@link Attributes}
415     *  instance at the given index.
416     *
417     *  @param  attributes  The attributes.
418     *  @param  index   The index.
419     *  @return The attribute.
420     *  @throws URISyntaxException  The URI for the attribute's namespace
421     *      cannot be parsed correctly.
422     *  @throws IllegalArgumentException    The attribute type is invalid.
423     */
424    private static final Attribute composeAttribute( final Attributes attributes, final int index ) throws IllegalArgumentException, URISyntaxException
425    {
426        final var qName = attributes.getQName( index );
427        final var attributesLocalName = attributes.getLocalName( index );
428        final Optional<String> localName = isEmptyOrBlank( attributesLocalName ) ? Optional.empty() : Optional.of( attributesLocalName );
429        final var attributesURI = attributes.getURI( index );
430        final Optional<URI> uri = isEmptyOrBlank( attributesURI ) ? Optional.empty() : Optional.of( new URI( attributesURI ) );
431        final var type = Attribute.Type.valueOf( attributes.getType( index ) );
432        final var value = attributes.getValue( index );
433
434        final var retValue = new Attribute( qName, localName, uri, type, value, index );
435
436        //---* Done *----------------------------------------------------------
437        return retValue;
438    }   //  composeAttribute()
439
440    /**
441     *  Receives the notification about the end of the document.<br>
442     *  <br>This implementation does nothing by default. Application writers
443     *  may override this method in a subclass to take specific actions at the
444     *  end of a document (such as finalising a tree or closing an output
445     *  file).
446     *
447     *  @throws SAXException    Any SAX exception, possibly wrapping another
448     *      exception.
449     */
450    @SuppressWarnings( "NoopMethodInAbstractClass" )
451    @Override
452    @MountPoint
453    public void endDocument() throws SAXException { /* Does nothing! */ }
454
455    /**
456     *  {@summary Receives the notification about the end of an element.} This
457     *  method will call the
458     *  {@link #processElement(Element) processElement()}
459     *  method and afterwards it will remove the element from the stack - in
460     *  exactly that order, otherwise the
461     *  {@link #getPath() getPath()}
462     *  method would return wrong results.
463     *
464     *  @param  uri The URI for the namespace of this element; can be empty.
465     *  @param  localName   The local name of the element.
466     *  @param  qName   The element's qualified name.
467     *  @throws SAXException    The element was not correct according to the
468     *      DTD.
469     */
470    @Override
471    public final void endElement( final String uri, final String localName, final String qName ) throws SAXException
472    {
473        final var element = m_ElementStack.peek().orElseThrow( () -> new SAXParseException( "No element '%1$s' on Stack".formatted( qName ), getLocator() ) );
474        if( !element.getQName().equals( qName ) )
475        {
476            throw new SAXParseException( "Closing element '%1$s' does not match open element '%2$s'".formatted( qName, element.getQName() ), getLocator() );
477        }
478        processElement( element );
479
480        //---* Remove element from stack *-------------------------------------
481        m_ElementStack.pop();
482    }   //  endElement()
483
484    /**
485     *  Receives the notification of the end for a name space mapping.
486     *
487     *  @param  prefix  The Namespace prefix being declared.
488     *  @throws SAXException    Any SAX exception, possibly wrapping another
489     *      exception.
490     */
491    @Override
492    public final void endPrefixMapping( final String prefix ) throws SAXException
493    {
494        //---* Delete the namespace *------------------------------------------
495        m_Namespaces.remove( requireNonNullArgument( prefix, "prefix" ) );
496    }   //  endPrefixMapping()
497
498    /**
499     *  Returns the name of the document type.
500     *
501     *  @return The document type.
502     */
503    public final String getDocumentType() { return m_DocumentType; }
504
505    /**
506     *  Returns a copy of the locator.
507     *
508     *  @return A copy of the locator object or {@code null} if there was
509     *      none provided by the parser.
510     */
511    protected final Locator getLocator() { return nonNull( m_Locator ) ? new LocatorImpl( m_Locator ) : null; }
512
513    /**
514     *  Returns the path for the element as an array, with the qualified
515     *  element names as the entries in the array. The array is ordered in the
516     *  way that the current element is at position {@code [0]}, while the root
517     *  element (the document element) is at {@code [length - 1]}.
518     *
519     *  @return The list of element names that build the path to the current
520     *      element.
521     */
522    protected final String [] getPath()
523    {
524        final var elements = m_ElementStack.toArray( EMPTY_Element_ARRAY );
525        final var retValue = IntStream.range( 0, m_ElementStack.size() )
526            .mapToObj( i -> elements [i].getQName() )
527            .toArray( String[]::new );
528
529        //---* Done *----------------------------------------------------------
530        return retValue;
531    }   //  getPath()
532
533    /**
534     *  Returns the path depth for the element.
535     *
536     *  @return The number of nodes on the path to the current element. 0 means
537     *      that the current element is the document.
538     */
539    protected final int getPathDepth() { return m_ElementStack.size() - 1; }
540
541    /**
542     *  The default element handling; it does nothing.
543     *
544     *  @param  element The element.
545     *  @param  terminateElement {@code true} if called by
546     *      {@link #processElement(Element)},
547     *      indicating that the element processing will be terminated,
548     *      {@code false} when called by
549     *      {@link #openElement(Element)}.
550     *  @throws SAXException    The element cannot be handled properly.
551     *
552     *  @since 0.1.0
553     */
554    @SuppressWarnings( {"unused", "NoopMethodInAbstractClass"} )
555    @MountPoint
556    @API( status = MAINTAINED, since = "0.1.0" )
557    protected void handleElement( @SuppressWarnings( "UseOfConcreteClass" ) final Element element, final boolean terminateElement ) throws SAXException
558    {
559        //---* Does nothing *--------------------------------------------------
560    }   //  handleElement()
561
562    /**
563     *  Receives the notification of ignorable whitespace in element
564     *  content.<br>
565     *  <br>This implementation does nothing by default. Application writers
566     *  may override this method to take specific actions for each chunk of
567     *  ignorable whitespace (such as adding data to a node or buffer, or
568     *  printing it to a file).
569     *
570     *  @param  ch  The whitespace characters.
571     *  @param  start   The start position in the character array.
572     *  @param  length  The number of characters to use from the character
573     *      array.
574     *  @throws SAXException    Any SAX exception, possibly wrapping another
575     *      exception.
576     */
577    @SuppressWarnings( "NoopMethodInAbstractClass" )
578    @Override
579    @MountPoint
580    public void ignorableWhitespace( final char [] ch, final int start, final int length ) throws SAXException { /* Does nothing! */ }
581
582    /**
583     *  <p>{@summary This method is called every time a new element was
584     *  encountered by the parser.} It should be overwritten if it is necessary
585     *  to perform any activities for a specific element.</p>
586     *  <p>The default implementation looks up a method handler in the map of
587     *  element handlers and calls that, or throws an exception if no handler
588     *  was registered for that element.</p>
589     *
590     *  @param  element The element.
591     *  @throws SAXException    Something has gone wrong.
592     *
593     *  @see #registerElementHandler(String,HandlerMethod)
594     *  @see #processElement(Element)
595     *
596     *  @since 0.1.0
597     */
598    @MountPoint
599    @API( status = MAINTAINED, since = "0.1.0" )
600    protected void openElement( @SuppressWarnings( "UseOfConcreteClass" ) final Element element ) throws SAXException
601    {
602        final var method = m_HandlerMethods.get( element.getQName() );
603        if( isNull(method ) ) throw new SAXParseException( format( MSG_NoHandler, element ), getLocator() );
604
605        //---* Process the element *-------------------------------------------
606        method.process( false, null, element.getAttributes(), element.getPath() );
607    }   //  openElement()
608
609    /**
610     *  <p>{@summary Processing of an element of the XML file.} This method will be called
611     *  by
612     *  {@link #endElement(String,String,String) endElement()}
613     *  any time an element was closed.</p>
614     *  <p>The default implementation looks up a method handler in the map of
615     *  element handlers and calls that, or throws an exception if no handler
616     *  was registered for that element.</p>
617     *
618     *  @param  element The element.
619     *  @throws SAXException    Something has gone wrong.
620     *
621     *  @see #registerElementHandler(String,HandlerMethod)
622     *  @see #openElement(Element)
623     *
624     *  @since 0.1.0
625     */
626    @MountPoint
627    @API( status = MAINTAINED, since = "0.1.0" )
628    protected void processElement( @SuppressWarnings( "UseOfConcreteClass" ) final Element element ) throws SAXException
629    {
630        final var method = m_HandlerMethods.get( element.getQName() );
631        if( isNull(method ) ) throw new SAXParseException( format( MSG_NoHandler, element.getQName() ), getLocator() );
632
633        //---* Process the element *-------------------------------------------
634        method.process( true, element.getData(), element.getAttributes(), element.getPath() );
635    }   //  processElement()
636
637    /**
638     *  Receives notification of a processing instruction.<br>
639     *  <br>This implementation does nothing by default. Application writers
640     *  may override this method in a subclass to take specific actions for
641     *  each processing instruction, such as setting status variables or
642     *  invoking other methods.
643     *
644     *  @param  target  The processing instruction target.
645     *  @param  data    The processing instruction data, or {@code null}
646     *      if none is supplied.
647     *  @throws SAXException    Any SAX exception, possibly wrapping another
648     *      exception.
649     */
650    @SuppressWarnings( "NoopMethodInAbstractClass" )
651    @Override
652    @MountPoint
653    public void processingInstruction( final String target, final String data ) throws SAXException { /* Does nothing! */ }
654
655    /**
656     *  Adds an element handler to the map of handler methods.
657     *
658     *  @param  qName   The qualified name of the elements that should be
659     *      processed by the handler .
660     *  @param  method  The method reference for the handler.
661     *
662     *  @see #openElement(Element)
663     *  @see #processElement(Element)
664     */
665    protected final void registerElementHandler( final String qName, final HandlerMethod method )
666    {
667        m_HandlerMethods.put( requireNotEmptyArgument( qName, "qName" ), requireNonNullArgument( method, "method" ) );
668    }   //  addElementHandler()
669
670    /**
671     *  Returns the current column number in the XML file. A negative value
672     *  indicates that the column is unknown.
673     *
674     *  @return The current column number.
675     */
676    protected final int retrieveCurrentColumn() { return nonNull( m_Locator ) ? m_Locator.getColumnNumber() : -1; }
677
678    /**
679     *  Returns the current line number in the XML file. A negative value
680     *  indicates that the line is unknown.
681     *
682     *  @return The current line number.
683     */
684    protected final int retrieveCurrentLine() { return nonNull( m_Locator ) ? m_Locator.getLineNumber() : -1; }
685
686    /**
687     *  Returns the namespace for the current element (that one that is on top
688     *  of the element stack).
689     *
690     *  @return An instance of
691     *      {@link Optional}
692     *      that holds the namespace for the current element. Will be
693     *      {@linkplain Optional#empty() empty}
694     *      if there is no namespace for the current element.
695     *  @throws SAXException    An error occurred while retrieving the
696     *      namespace information.
697     *
698     *  @since  0.1.0
699     */
700    @API( status = MAINTAINED, since = "0.1.0" )
701    protected final Optional<URI> retrieveCurrentNamespace() throws SAXException
702    {
703        final var element = m_ElementStack.peek().orElseThrow( () -> new SAXParseException( MSG_NoElementOnStack, getLocator() ) );
704        final var retValue = element.getURI();
705
706        //---* Done *----------------------------------------------------------
707        return retValue;
708    }   //  retrieveCurrentNamespace()
709
710    /**
711     *  Returns the URI of the namespace for the given prefix.
712     *
713     *  @param  prefix  The prefix.
714     *  @return An instance of
715     *      {@link Optional}
716     *      that holds the namespace for the prefix. Will be
717     *      {@linkplain Optional#empty() empty}
718     *      if there is no namespace for the given prefix.
719     *
720     *  @since 0.1.0
721     */
722    @API( status = MAINTAINED, since = "0.1.0" )
723    protected final Optional<URI> retrieveNamespace( final String prefix )
724    {
725        final var retValue = Optional.ofNullable( m_Namespaces.get( requireNotEmptyArgument( prefix, "prefix" ) ) );
726
727        //---* Done *----------------------------------------------------------
728        return retValue;
729    }   //  retrieveNamespace()
730
731    /**
732     *  Returns the registered prefix for the given namespace. If more than one
733     *  prefix is registered for the same namespace, only that one that is
734     *  alphabetically the first one will be returned.
735     *
736     *  @param  namespace   The URI for the namespace.
737     *  @return An instance of
738     *      {@link Optional}
739     *      that holds the registered prefix.
740     *  @since 0.1.0
741     */
742    @API( status = MAINTAINED, since = "0.1.0" )
743    protected final Optional<String> retrievePrefix( final URI namespace )
744    {
745        requireNonNullArgument( namespace, "namespace" );
746        final var retValue = m_Namespaces
747            .entrySet()
748            .stream()
749            .filter( entry -> entry.getValue().equals( namespace ) )
750            .map( Map.Entry::getKey )
751            .findFirst();
752
753        //---* Done *----------------------------------------------------------
754        return retValue;
755    }   //  retrievePrefix()
756
757    /**
758     *  <p>{@summary Receives an object for locating the origin of SAX document
759     *  events.}</p>
760     *  <p>SAX parsers are strongly encouraged (though not absolutely
761     *  required) to supply a locator: if it does so, it must supply the
762     *  locator to the application by invoking this method before invoking any
763     *  of the other methods in the ContentHandler interface.</p>
764     *  <p>The locator allows the application to determine the end position
765     *  of any document-related event, even if the parser is not reporting an
766     *  error. Typically, the application will use this information for
767     *  reporting its own errors (such as character content that does not match
768     *  an application's business rules). The information returned by the
769     *  locator is probably not sufficient for use with a search engine.</p>
770     *  <p>Note that the locator will return correct information only during
771     *  the invocation of SAX event callbacks after
772     *  {@link #startDocument()}
773     *  returns and before
774     *  {@link #endDocument()}
775     *  is called. The application should not attempt to use it at any other
776     *  time.</p>
777     *
778     *  @param  locator An object that can return the location of any SAX
779     *      document event.
780     */
781    @Override
782    public final void setDocumentLocator( final Locator locator ) { m_Locator = requireNonNullArgument( locator, "locator" ); }
783
784    /**
785     *  <p>{@summary Receives notification of a skipped entity.}</p>
786     *  <p>This implementation does nothing by default. Application writers
787     *  may override this method in a subclass to take specific actions for
788     *  each processing instruction, such as setting status variables or
789     *  invoking other methods.</p>
790     *
791     *  @param  name    The name of the skipped entity.
792     *  @throws SAXException    Any SAX exception, possibly wrapping another
793     *      exception.
794     */
795    @SuppressWarnings( "NoopMethodInAbstractClass" )
796    @Override
797    @MountPoint
798    public void skippedEntity( final String name ) throws SAXException { /* Does nothing! */ }
799
800    /**
801     *  Receives the notification about the start of an element.
802     *
803     *  @param  uri The URI for the namespace of this element; can be empty.
804     *  @param  localName   The local name of the element.
805     *  @param  qName   The element's qualified name.
806     *  @param  attributes  The element's attributes.
807     *  @throws SAXException    The element was not correct according to the
808     *      DTD.
809     */
810    @SuppressWarnings( "OverlyComplexMethod" )
811    @Override
812    public final void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) throws SAXException
813    {
814        if( isNull( localName ) && isNull( qName ) )
815        {
816            throw new SAXParseException( "No name for element", getLocator() );
817        }
818
819        //---* Store the document type *---------------------------------------
820        if( isNull( m_DocumentType ) ) m_DocumentType = qName;
821
822        //---* Build the path *------------------------------------------------
823        Element parent = null;
824        final var path = new StringBuilder();
825        if( !m_ElementStack.isEmpty() )
826        {
827            parent = m_ElementStack.peek().orElseThrow( () -> new SAXParseException( MSG_NoElementOnStack, getLocator() ) );
828            path.append( parent.getPath() );
829        }
830        path.append( '/' )
831            .append( qName.trim() );
832
833        //---* Build the attributes map *--------------------------------------
834        final Map<String,Attribute> attributesMap = new HashMap<>();
835        try
836        {
837            for( var i = 0; i < attributes.getLength(); ++i )
838            {
839                attributesMap.put( attributes.getQName( i ), composeAttribute( attributes, i ) );
840            }
841        }
842        catch( final IllegalArgumentException | URISyntaxException e )
843        {
844            throw new SAXParseException( "Invalid Argument data", getLocator(), e );
845        }
846
847        //---* Build the element *---------------------------------------------
848        var effectiveQName = qName;
849        var effectiveLocalName = localName;
850        URI namespace = null;
851        if( isNotEmptyOrBlank( uri ) )
852        {
853            try
854            {
855                namespace = new URI( uri );
856            }
857            catch( final URISyntaxException e )
858            {
859                throw new SAXParseException( format( MSG_InvalidURI, uri), getLocator(), e );
860            }
861
862            if( isNull( effectiveLocalName ) )
863            {
864                final var pos = effectiveQName.indexOf( ':' );
865                effectiveLocalName = pos == -1 ? effectiveQName : effectiveQName.substring( pos + 1 );
866            }
867            if( isNull( effectiveQName ) )
868            {
869                final var prefix = retrievePrefix( namespace ).orElseThrow( () -> new SAXParseException( "Unknown Namespace: %s".formatted( uri ), getLocator() ) );
870                effectiveQName = format( "%s:%s", prefix, effectiveLocalName );
871            }
872        }
873        else
874        {
875            if( isEmptyOrBlank( effectiveLocalName ) ) effectiveLocalName = effectiveQName;
876            if( isEmptyOrBlank( effectiveQName ) ) effectiveQName = effectiveLocalName;
877        }
878
879        final var element = new Element( effectiveQName, effectiveLocalName, namespace, attributesMap, path.toString(), parent );
880        m_ElementStack.push( element );
881        openElement( element );
882    }  //  startElement()
883
884    /**
885     *  <p>{@summary Receives the notification of the beginning of the
886     *  document.}</p>
887     *  <p>This implementation does nothing by default. Application writers
888     *  may override this method in a subclass to take specific actions at the
889     *  beginning of a document (such as allocating the root node of a tree or
890     *  creating an output file).</p>
891     *
892     *  @throws SAXException    Any SAX exception, possibly wrapping another
893     *      exception.
894     */
895    @SuppressWarnings( "NoopMethodInAbstractClass" )
896    @Override
897    @MountPoint
898    public void startDocument() throws SAXException { /* Does nothing! */ }
899
900    /**
901     *  Receives the notification of the start of a Namespace mapping.
902     *
903     *  @param  prefix  The Namespace prefix being declared.
904     *  @param  uri The Namespace URI mapped to the prefix.
905     *  @throws SAXException    Any SAX exception, possibly wrapping another
906     *      exception.
907     */
908    @Override
909    public final void startPrefixMapping( final String prefix, final String uri ) throws SAXException
910    {
911        final URI namespace;
912        try
913        {
914            namespace = new URI( requireNonNullArgument( uri, "uri" ) );
915        }
916        catch( final URISyntaxException e )
917        {
918            throw new SAXParseException( format( MSG_InvalidURI, uri), getLocator(), e );
919        }
920
921        //---* Store the mapping *---------------------------------------------
922        m_Namespaces.put( requireNonNullArgument( prefix, "prefix" ), namespace );
923    }   //  startPrefixMapping()
924}
925//  class AdvancedContentHandler
926
927/*
928 *  End of File
929 */