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 */