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><p>First Text <b>Bold Text</b> Second Text</p></code></pre> 085 * <p>will be parsed as "First Text Second Text" for the {@code p} 086 * element and "Bold Text" 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 */