001/*
002 * ============================================================================
003 * Copyright © 2002-2025 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 *
007 * Licensed to the public under the agreements of the GNU Lesser General Public
008 * License, version 3.0 (the "License"). You may obtain a copy of the License at
009 *
010 *      http://www.gnu.org/licenses/lgpl.html
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015 * License for the specific language governing permissions and limitations
016 * under the License.
017 */
018
019package org.tquadrat.foundation.xml.parse.spi;
020
021import static java.lang.String.format;
022import static org.apiguardian.api.API.Status.EXPERIMENTAL;
023import static org.tquadrat.foundation.lang.Objects.isNull;
024import static org.tquadrat.foundation.lang.Objects.nonNull;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
027import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
028
029import javax.xml.stream.Location;
030import javax.xml.stream.XMLEventReader;
031import javax.xml.stream.XMLStreamException;
032import java.util.HashMap;
033import java.util.Map;
034import java.util.Optional;
035
036import org.apiguardian.api.API;
037import org.tquadrat.foundation.annotation.ClassVersion;
038import org.tquadrat.foundation.xml.parse.LocationLocator;
039import org.tquadrat.foundation.xml.parse.XMLParseEventHandler;
040import org.xml.sax.SAXException;
041import org.xml.sax.SAXParseException;
042
043/**
044 *  <p>{@summary The abstract base class for StAX based XML parsers.}</p>
045 *  <p>An implementation of this class will parse an XML stream to an object
046 *  of type {@code T} that is either provided with the constructor
047 *  {@link #StAXParserBase(Object)}
048 *  or will be created by an instance of
049 *  {@link XMLParseEventHandler}.</p>
050 *  <p>The parse event handler can be provided either programmatically, as
051 *  shown in
052 *  {@link org.tquadrat.foundation.xml.parse.StAXParser},
053 *  or as methods in an implementation of this class.</p>
054 *
055 *  @param  <T> The type of the target data structure.
056 *
057 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
058 *  @version $Id: StAXParserBase.java 1152 2025-12-25 09:51:42Z tquadrat $
059 *  @since 0.0.5
060 *
061 *  @UMLGraph.link
062 */
063@SuppressWarnings( "AbstractClassWithoutAbstractMethods" )
064@ClassVersion( sourceVersion = "$Id: StAXParserBase.java 1152 2025-12-25 09:51:42Z tquadrat $" )
065@API( status = EXPERIMENTAL, since = "0.0.5" )
066public abstract class StAXParserBase<T>
067{
068        /*-----------*\
069    ====** Constants **========================================================
070        \*-----------*/
071    /**
072     *  The message for missing handlers: {@value}.
073     */
074    public static final String MSG_NoHandler = "No handler was registered for element '%s'";
075
076    /**
077     *  The message for an unexpected tag: {@value}.
078     */
079    public static final String MSG_UnexpectedTag = "The element tag '%s' is unexpected here";
080
081        /*------------*\
082    ====** Attributes **=======================================================
083        \*------------*/
084    /**
085     *  The document element tag.
086     */
087    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
088    private Optional<String> m_DocumentTag = Optional.empty();
089
090    /**
091     *  The event handlers.
092     */
093    private final Map<String,XMLParseEventHandler<?>> m_Handlers = new HashMap<>();
094
095    /**
096     *  The target data structure.
097     */
098    private T m_Target;
099
100        /*--------------*\
101    ====** Constructors **=====================================================
102        \*--------------*/
103    /**
104     *  Creates a new {@code StAXParserBase} instance.
105     */
106    protected StAXParserBase()
107    {
108        m_Target = null;
109    }   //  StAXParserBase()
110
111    /**
112     *  Creates a new {@code StAXParser} instance.
113     *
114     *  @param  target  The target data structure.
115     */
116    protected StAXParserBase( final T target )
117    {
118        setTarget( target );
119    }   //  StAXParserBase()
120
121        /*---------*\
122    ====** Methods **==========================================================
123        \*---------*/
124    /**
125     *  Processes the given
126     *  {@linkplain XMLEventReader event reader}.
127     *
128     *  @param  eventReader The XML stream.
129     *  @return The target data structure.
130     *  @throws SAXException    Something went wrong.
131     */
132    @SuppressWarnings( "unchecked" )
133    @API( status = EXPERIMENTAL, since = "0.0.7" )
134    protected T parse( final XMLEventReader eventReader ) throws SAXException
135    {
136        final var documentTag = m_DocumentTag.orElseThrow( () -> new SAXException( "Undefined document", new IllegalStateException( "No document tag provided" ) ) );
137        try
138        {
139            ScanLoop: while( eventReader.hasNext() )
140            {
141                String elementName = null;
142                var xmlEvent = eventReader.nextEvent();
143                if( xmlEvent.isStartDocument() )
144                {
145                    elementName = documentTag;
146                    xmlEvent = eventReader.nextTag();
147                }
148                else if( xmlEvent.isEndDocument() )
149                {
150                    elementName = documentTag;
151                    if( eventReader.hasNext() ) xmlEvent = eventReader.nextTag();
152                }
153                else continue ScanLoop;
154
155                final var handler = isNotEmptyOrBlank( elementName ) ? (XMLParseEventHandler<T>) retrieveHandler( elementName ) : null;
156                setTarget( handler.process( eventReader, xmlEvent, m_Target, this::retrieveHandler ) );
157            }   //  ScanLoop:
158        }
159        catch( final XMLStreamException e )
160        {
161            final var message = "XML parse failed";
162            if( nonNull( e.getLocation() ) )
163            {
164                throw new SAXParseException( message, new LocationLocator( e.getLocation() ), e );
165            }
166            throw new SAXException( message, e );
167        }
168
169        final var retValue = m_Target;
170
171        //---* Done *----------------------------------------------------------
172        return retValue;
173    }   //  parse()
174
175    /**
176     *  Registers an element handler.
177     *
178     *  @param  elementName The element name.
179     *  @param  isDocument  {@code true} if the element name is the document
180     *      name.
181     *  @param  handler The parse event handler.
182     */
183    protected final void registerElementHandler( final String elementName, final boolean isDocument, final XMLParseEventHandler<?> handler )
184    {
185        if( isDocument && m_DocumentTag.isPresent() ) throw new IllegalStateException( "Document Tag was already set: %s".formatted( m_DocumentTag.get() ) );
186        m_Handlers.put( requireNotEmptyArgument( elementName, "elementName" ), requireNonNullArgument( handler, "handler" ) );
187        if( isDocument ) m_DocumentTag = Optional.of( elementName );
188    }   //  registerElementHandler()
189
190    /**
191     *  Throws an
192     *  {@link XMLStreamException}
193     *  that indicates an unexpected tag at the given location.
194     *
195     *  @param  elementName The encountered tag.
196     *  @param  location    The location.
197     *  @throws XMLStreamException  Always.
198     */
199    @API( status = EXPERIMENTAL, since = "0.0.7" )
200    protected static final void reportUnexpectedTag( final String elementName, final Location location ) throws XMLStreamException
201    {
202        throw new XMLStreamException( format( MSG_UnexpectedTag, requireNotEmptyArgument( elementName, "elementName" ) ), requireNonNullArgument( location, "location" ) );
203    }   //  reportUnexpectedTag()
204
205    /**
206     *  Retrieves the XML parse event handler for the given element name.
207     *
208     *  @param  elementName The name of the element to handle.
209     *  @return The requested instance of
210     *      {@link XMLParseEventHandler}.
211     *  @throws XMLStreamException  There is no registered handler for the
212     *      given element name.
213     */
214    private final XMLParseEventHandler<?> retrieveHandler( final String elementName ) throws XMLStreamException
215    {
216        final var retValue = m_Handlers.get( requireNotEmptyArgument( elementName, "elementName" ) );
217        if( isNull( retValue ) )
218        {
219            throw new XMLStreamException( format( MSG_NoHandler, elementName ) );
220        }
221
222        //---* Done *----------------------------------------------------------
223        return retValue;
224    }   //  retrieveHandler()
225
226    /**
227     *  Sets the target data structure.
228     *
229     *  @param  target  The target data structure.
230     */
231    private final void setTarget( final T target )
232    {
233        m_Target = requireNonNullArgument( target, "target" );
234    }   //  setTarget()
235}
236//  class StAXParserBase
237
238/*
239 *  End of File
240 */