001/*
002 * ============================================================================
003 * Copyright © 2002-2023 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 1071 2023-09-30 01:49:32Z tquadrat $
059 *  @since 0.0.5
060 *
061 *  @UMLGraph.link
062 */
063@SuppressWarnings( "AbstractClassWithoutAbstractMethods" )
064@ClassVersion( sourceVersion = "$Id: StAXParserBase.java 1071 2023-09-30 01:49:32Z tquadrat $" )
065@API( status = EXPERIMENTAL, since = "0.0.5" )
066public abstract class StAXParserBase<T>
067{
068        /*-----------*\
069    ====** Constants **========================================================
070        \*-----------*/
071    /**
072     *  An empty array of {@code StAXParserBase} objects.
073     */
074    @SuppressWarnings( "rawtypes" )
075    public static final StAXParserBase [] EMPTY_StAXParserBase_ARRAY = new StAXParserBase [0];
076
077    /**
078     *  The message for missing handlers: {@value}.
079     */
080    public static final String MSG_NoHandler = "No handler was registered for element '%s'";
081
082    /**
083     *  The message for an unexpected tag: {@value}.
084     */
085    public static final String MSG_UnexpectedTag = "The element tag '%s' is unexpected here";
086
087        /*------------*\
088    ====** Attributes **=======================================================
089        \*------------*/
090    /**
091     *  The document element tag.
092     */
093    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
094    private Optional<String> m_DocumentTag = Optional.empty();
095
096    /**
097     *  The event handlers.
098     */
099    private final Map<String,XMLParseEventHandler<?>> m_Handlers = new HashMap<>();
100
101    /**
102     *  The target data structure.
103     */
104    private T m_Target;
105
106        /*--------------*\
107    ====** Constructors **=====================================================
108        \*--------------*/
109    /**
110     *  Creates a new {@code StAXParserBase} instance.
111     */
112    protected StAXParserBase()
113    {
114        m_Target = null;
115    }   //  StAXParserBase()
116
117    /**
118     *  Creates a new {@code StAXParser} instance.
119     *
120     *  @param  target  The target data structure.
121     */
122    protected StAXParserBase( final T target )
123    {
124        setTarget( target );
125    }   //  StAXParserBase()
126
127        /*---------*\
128    ====** Methods **==========================================================
129        \*---------*/
130    /**
131     *  Processes the given
132     *  {@linkplain XMLEventReader event reader}.
133     *
134     *  @param  eventReader The XML stream.
135     *  @return The target data structure.
136     *  @throws SAXException    Something went wrong.
137     */
138    @SuppressWarnings( "unchecked" )
139    @API( status = EXPERIMENTAL, since = "0.0.7" )
140    protected T parse( final XMLEventReader eventReader ) throws SAXException
141    {
142        final var documentTag = m_DocumentTag.orElseThrow( () -> new SAXException( "Undefined document", new IllegalStateException( "No document tag provided" ) ) );
143        try
144        {
145            ScanLoop: while( eventReader.hasNext() )
146            {
147                String elementName = null;
148                var xmlEvent = eventReader.nextEvent();
149                if( xmlEvent.isStartDocument() )
150                {
151                    elementName = documentTag;
152                    xmlEvent = eventReader.nextTag();
153                }
154                else if( xmlEvent.isEndDocument() )
155                {
156                    elementName = documentTag;
157                    if( eventReader.hasNext() ) xmlEvent = eventReader.nextTag();
158                }
159                else continue ScanLoop;
160
161                final var handler = isNotEmptyOrBlank( elementName ) ? (XMLParseEventHandler<T>) retrieveHandler( elementName ) : null;
162                setTarget( handler.process( eventReader, xmlEvent, m_Target, this::retrieveHandler ) );
163            }   //  ScanLoop:
164        }
165        catch( final XMLStreamException e )
166        {
167            final var message = "XML parse failed";
168            if( nonNull( e.getLocation() ) )
169            {
170                throw new SAXParseException( message, new LocationLocator( e.getLocation() ), e );
171            }
172            throw new SAXException( message, e );
173        }
174
175        final var retValue = m_Target;
176
177        //---* Done *----------------------------------------------------------
178        return retValue;
179    }   //  parse()
180
181    /**
182     *  Registers an element handler.
183     *
184     *  @param  elementName The element name.
185     *  @param  isDocument  {@code true} if the element name is the document
186     *      name.
187     *  @param  handler The parse event handler.
188     */
189    protected final void registerElementHandler( final String elementName, final boolean isDocument, final XMLParseEventHandler<?> handler )
190    {
191        if( isDocument && m_DocumentTag.isPresent() ) throw new IllegalStateException( "Document Tag was already set: %s".formatted( m_DocumentTag.get() ) );
192        m_Handlers.put( requireNotEmptyArgument( elementName, "elementName" ), requireNonNullArgument( handler, "handler" ) );
193        if( isDocument ) m_DocumentTag = Optional.of( elementName );
194    }   //  registerElementHandler()
195
196    /**
197     *  Throws an
198     *  {@link XMLStreamException}
199     *  that indicates an unexpected tag at the given location.
200     *
201     *  @param  elementName The encountered tag.
202     *  @param  location    The location.
203     *  @throws XMLStreamException  Always.
204     */
205    @API( status = EXPERIMENTAL, since = "0.0.7" )
206    protected static final void reportUnexpectedTag( final String elementName, final Location location ) throws XMLStreamException
207    {
208        throw new XMLStreamException( format( MSG_UnexpectedTag, requireNotEmptyArgument( elementName, "elementName" ) ), requireNonNullArgument( location, "location" ) );
209    }   //  reportUnexpectedTag()
210
211    /**
212     *  Retrieves the XML parse event handler for the given element name.
213     *
214     *  @param  elementName The name of the element to handle.
215     *  @return The requested instance of
216     *      {@link XMLParseEventHandler}.
217     *  @throws XMLStreamException  There is no registered handler for the
218     *      given element name.
219     */
220    private final XMLParseEventHandler<?> retrieveHandler( final String elementName ) throws XMLStreamException
221    {
222        final var retValue = m_Handlers.get( requireNotEmptyArgument( elementName, "elementName" ) );
223        if( isNull( retValue ) )
224        {
225            throw new XMLStreamException( format( MSG_NoHandler, elementName ) );
226        }
227
228        //---* Done *----------------------------------------------------------
229        return retValue;
230    }   //  retrieveHandler()
231
232    /**
233     *  Sets the target data structure.
234     *
235     *  @param  target  The target data structure.
236     */
237    private final void setTarget( final T target )
238    {
239        m_Target = requireNonNullArgument( target, "target" );
240    }   //  setTarget()
241}
242//  class StAXParserBase
243
244/*
245 *  End of File
246 */