001/*
002 * ============================================================================
003 * Copyright © 2002-2024 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.config.internal;
020
021import org.apiguardian.api.API;
022import org.tquadrat.foundation.annotation.ClassVersion;
023import org.tquadrat.foundation.config.cli.CmdLineValueHandler;
024import org.tquadrat.foundation.config.cli.EnumValueHandler;
025import org.tquadrat.foundation.config.cli.SimpleCmdLineValueHandler;
026import org.tquadrat.foundation.config.spi.CLIArgumentDefinition;
027import org.tquadrat.foundation.config.spi.CLIDefinition;
028import org.tquadrat.foundation.config.spi.CLIOptionDefinition;
029import org.tquadrat.foundation.lang.StringConverter;
030import org.xml.sax.SAXException;
031import org.xml.sax.SAXParseException;
032
033import javax.xml.stream.*;
034import javax.xml.stream.events.Attribute;
035import javax.xml.stream.events.StartElement;
036import javax.xml.transform.stax.StAXSource;
037import javax.xml.validation.SchemaFactory;
038import java.io.IOException;
039import java.io.InputStream;
040import java.io.StringReader;
041import java.lang.reflect.Constructor;
042import java.lang.reflect.InvocationTargetException;
043import java.net.URL;
044import java.util.*;
045import java.util.function.BiConsumer;
046
047import static java.lang.String.format;
048import static java.lang.String.join;
049import static java.lang.System.out;
050import static java.util.Locale.ROOT;
051import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
052import static javax.xml.stream.XMLStreamConstants.*;
053import static org.apiguardian.api.API.Status.INTERNAL;
054import static org.tquadrat.foundation.config.internal.ClassRegistry.m_HandlerClasses;
055import static org.tquadrat.foundation.config.spi.CLIDefinition.validateOptionName;
056import static org.tquadrat.foundation.lang.CommonConstants.UTF8;
057import static org.tquadrat.foundation.lang.CommonConstants.XMLATTRIBUTE_Name;
058import static org.tquadrat.foundation.lang.Objects.*;
059import static org.tquadrat.foundation.util.JavaUtils.isValidName;
060import static org.tquadrat.foundation.util.JavaUtils.retrieveMethod;
061import static org.tquadrat.foundation.util.StringUtils.*;
062
063/**
064 *  Parses an XML CLI definition file.
065 *
066 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
067 *  @version $Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $
068 *  @since 0.0.1
069 *
070 *  @UMLGraph.link
071 */
072@SuppressWarnings( {"OverlyComplexClass", "ClassWithTooManyMethods"} )
073@ClassVersion( sourceVersion = "$Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $" )
074@API( status = INTERNAL, since = "0.0.1" )
075public final class CLIDefinitionParser
076{
077        /*---------------*\
078    ====** Inner Classes **====================================================
079        \*---------------*/
080    /**
081     *  An implementation of
082     *  {@link Location}.
083     *  that is based on an instance of
084     *  {@link SAXParseException}.
085     *
086     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
087     *  @version $Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $
088     *  @since 0.0.1
089     *
090     *  @UMLGraph.link
091     */
092    @ClassVersion( sourceVersion = "$Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $" )
093    @API( status = INTERNAL, since = "0.0.1" )
094    public static final class ExceptionLocation implements Location
095    {
096            /*------------*\
097        ====** Attributes **===================================================
098            \*------------*/
099        /**
100         *  The exception that provides the data.
101         */
102        private final SAXParseException m_Exception;
103
104            /*--------------*\
105        ====** Constructors **=================================================
106            \*--------------*/
107        /**
108         *  Creates a new {@code ExceptionLocation} instance from the given
109         *  instance of
110         *  {@link SAXParseException}.
111         *
112         *  @param  exception   The exception.
113         */
114        public ExceptionLocation( final SAXParseException exception ) { m_Exception = requireNonNullArgument( exception, "exception" ); }
115
116            /*---------*\
117        ====** Methods **==========================================================
118            \*---------*/
119        /**
120         *  {@inheritDoc}
121         */
122        @Override
123        public final int getCharacterOffset() { return -1; }
124
125        /**
126         *  {@inheritDoc}
127         */
128        @Override
129        public final int getColumnNumber() {return m_Exception.getColumnNumber(); }
130
131        /**
132         *  {@inheritDoc}
133         */
134        @Override
135        public final int getLineNumber() {return m_Exception.getLineNumber(); }
136
137        /**
138         *  {@inheritDoc}
139         */
140        @Override
141        public final String getPublicId() {return m_Exception.getPublicId(); }
142
143        /**
144         *  {@inheritDoc}
145         */
146        @Override
147        public final String getSystemId()  {return m_Exception.getSystemId(); }
148    }
149    //  class ExceptionLocation
150
151    /**
152     *  An implementation of
153     *  {@link XMLResolver}
154     *  for this parser.
155     *
156     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
157     *  @version $Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $
158     *  @since 0.0.1
159     *
160     *  @UMLGraph.link
161     */
162    @ClassVersion( sourceVersion = "$Id: CLIDefinitionParser.java 1120 2024-03-16 09:48:00Z tquadrat $" )
163    @API( status = INTERNAL, since = "0.0.1" )
164    private static class CLIDefinitionResolver implements XMLResolver
165    {
166            /*--------------*\
167        ====** Constructors **=================================================
168            \*--------------*/
169        /**
170         *  Creates a new instance of {@code CLIDefinitionResolver}.
171         */
172        public CLIDefinitionResolver() { /* Just exists */ }
173
174            /*---------*\
175        ====** Methods **======================================================
176            \*---------*/
177        /**
178         *  {@inheritDoc}
179         */
180        @SuppressWarnings({"UseOfSystemOutOrSystemErr", "RedundantThrows"})
181        @Override
182        public final Object resolveEntity( final String publicID, final String systemID, final String baseURI, final String namespace ) throws XMLStreamException
183        {
184            Object retValue = null;
185
186            if( CLI_DEFINITION_DTD.equals( systemID ) )
187            {
188                retValue = retrieveCLIDefinitionDTD();
189            }
190            else
191            {
192                out.printf( "PublicID = %s%n", publicID );
193                out.printf( "SystemID = %s%n", systemID );
194                out.printf( "BaseURI = %s%n", baseURI );
195                out.printf( "Namespace = %s%n", namespace );
196            }
197
198            //---* Done *------------------------------------------------------
199            return retValue;
200        } // resolveEntity()
201    }
202    //  class CLIDefinitionResolver
203
204        /*-----------*\
205    ====** Constants **========================================================
206        \*-----------*/
207    /**
208     *  The name for the CLI definition DTD file: {@value}.
209     */
210    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
211    @API( status = INTERNAL, since = "0.0.1" )
212    public static final String CLI_DEFINITION_DTD = "CLIDefinition.dtd";
213
214    /**
215     *  The name for the CLI definition XSD file: {@value}.
216     */
217    @API( status = INTERNAL, since = "0.0.1" )
218    public static final String CLI_DEFINITION_XSD = "CLIDefinition.xsd";
219
220    /**
221     *  The message indicating that a start element was expected, but another
222     *  event was encountered: {@value}.
223     */
224    public static final String MSG_ExpectedStartEvent = "Start event expected: %s";
225
226    /**
227     *  The message indicating that a provided
228     *  {@link StringConverter}
229     *  implementation is invalid:
230     *  {@value}.
231     */
232    public static final String MSG_InvalidStringConverter = "Invalid StringConverter implementation: %s";
233
234    /**
235     *  The message indicating that an attribute value is invalid:
236     *  {@value}.
237     */
238    public static final String MSG_InvalidValue = "Value '%2$s' for attribute '%1$s' is invalid";
239
240    /**
241     *  The message indicating that an attribute does not have a value:
242     *  {@value}.
243     */
244    public static final String MSG_MissingValue = "Value for attribute '%1$s' is missing";
245
246    /**
247     *  The message indicating that a specific end element was expected, but
248     *  another end element was encountered: {@value}.
249     */
250    public static final String MSG_UnexpectedEndEvent =
251        """
252        Unexpected End event: %1$s
253        '%3$s' does not match the expected '%2$s'
254        """;
255
256    /**
257     *  The message indicating that an unexpected event was encountered:
258     *  {@value}.
259     */
260    public static final String MSG_UnexpectedEvent = "Unexpected event: %1$s";
261
262    /**
263     *  The message indicating that an unexpected attribute was
264     *  encountered: {@value}.
265     */
266    public static final String MSG_WrongAttribute = "Unexpected attribute: %1$s";
267
268    /**
269     *  The message indicating that an unexpected start element was
270     *  encountered: {@value}.
271     */
272    public static final String MSG_WrongElement1 = "Expected '%1$s', but encountered '%2$s'";
273
274    /**
275     *  The message indicating that an unexpected start element was
276     *  encountered: {@value}.
277     */
278    public static final String MSG_WrongElement2 = "Encountered '%2$s' while expected one of: %1$s";
279
280    /**
281     *  The name for the XML attribute 'handler': {@value}.
282     */
283    public static final String XMLATTRIBUTE_Handler = "handler";
284
285    /**
286     *  The name for the XML attribute 'index': {@value}.
287     */
288    public static final String XMLATTRIBUTE_Index = "index";
289
290    /**
291     *  The name for the XML attribute 'isMultiValue': {@value}.
292     */
293    public static final String XMLATTRIBUTE_IsMultiValue = "isMultiValue";
294
295    /**
296     *  The name for the XML attribute 'isRequired': {@value}.
297     */
298    public static final String XMLATTRIBUTE_IsRequired = "isRequired";
299
300    /**
301     *  The name for the XML attribute 'key': {@value}.
302     */
303    public static final String XMLATTRIBUTE_Key = "key";
304
305    /**
306     *  The name for the XML attribute 'metaVar': {@value}.
307     */
308    public static final String XMLATTRIBUTE_MetaVar = "metaVar";
309
310    /**
311     *  The name for the XML attribute 'propertyName': {@value}.
312     */
313    public static final String XMLATTRIBUTE_PropertyName = "propertyName";
314
315    /**
316     *  The name for the XML attribute 'stringConversion': {@value}.
317     */
318    public static final String XMLATTRIBUTE_StringConversion = "stringConversion";
319
320    /**
321     *  The name for the XML attribute 'type': {@value}.
322     */
323    public static final String XMLATTRIBUTE_Type = "type";
324
325    /**
326     *  The name for the XML element 'alias': {@value}.
327     */
328    public static final String XMLELEMENT_Alias = "alias";
329
330    /**
331     *  The name for the XML element 'argument': {@value}.
332     */
333    public static final String XMLELEMENT_CLIArgument = "argument";
334
335    /**
336     *  The name for the XML element 'cliDefinition': {@value}.
337     */
338    public static final String XMLELEMENT_CLIDefinition = "cliDefinition";
339
340    /**
341     *  The name for the XML element 'option': {@value}.
342     */
343    public static final String XMLELEMENT_CLIOption = "option";
344
345    /**
346     *  The name for the XML element 'format': {@value}.
347     */
348    public static final String XMLELEMENT_Format = "format";
349
350    /**
351     *  The name for the XML element 'usage': {@value}.
352     */
353    public static final String XMLELEMENT_Usage = "usage";
354
355        /*------------*\
356    ====** Attributes **=======================================================
357        \*------------*/
358    /**
359     *  The XML event reader that provides the CLI definition.
360     */
361    private final XMLEventReader m_EventReader;
362
363    /**
364     *  The property map.
365     */
366    private final Map<String,Object> m_PropertyMap;
367
368    /**
369     *  The XML stream.
370     */
371    private final String m_XMLContents;
372
373        /*--------------*\
374    ====** Constructors **=====================================================
375        \*--------------*/
376    /**
377     *  Creates a new {@code CLIDefinitionParser} instance.
378     *
379     *  @param  inputStream The input stream that should contain the XML CLI
380     *      definition.
381     *  @param  propertyMap The target data structure for the values from the
382     *      command line.
383     *  @throws XMLStreamException  Cannot create a
384     *      {@link XMLEventReader}
385     *      instance for the given input stream.
386     *  @throws IOException Cannot read the given input stream.
387     */
388    private CLIDefinitionParser( final InputStream inputStream, final Map<String,Object> propertyMap ) throws XMLStreamException, IOException
389    {
390        m_PropertyMap = requireNonNullArgument( propertyMap, "propertyMap" );
391
392        //---* Get XML contents *----------------------------------------------
393        m_XMLContents = new String( requireNonNullArgument( inputStream, "inputStream" ).readAllBytes(), UTF8 );
394
395        //---* Create the event reader for parsing *---------------------------
396        final var xmlInputFactory = XMLInputFactory.newInstance();
397        xmlInputFactory.setXMLResolver( new CLIDefinitionResolver() );
398        m_EventReader = xmlInputFactory.createXMLEventReader( new StringReader( m_XMLContents ) );
399    }   //  CLIDefinitionParser()
400
401        /*---------*\
402    ====** Methods **==========================================================
403        \*---------*/
404    /**
405     *  <p>{@summary Creates the instance for the command line value handler
406     *  based on the given class for the type and class for the handler
407     *  implementation.} If {@code processClass} is {@code null}, the method
408     *  will search for a specialised handler class in the internal registry;
409     *  if none can be found, and {@code stringConverter} is not {@code null},
410     *  it creates an instance of
411     *  {@link org.tquadrat.foundation.config.cli.SimpleCmdLineValueHandler}
412     *  with it. But if {@code stringConverter} is {@code null}, an exception
413     *  will be thrown.</p>
414     *  <p>If {@code processClass} is not {@code null}, it has to be a class
415     *  that implements
416     *  {@link CmdLineValueHandler}.
417     *  In that case that class will be instantiated.</p>
418     *
419     *  @param  <T> The type of the property the set.
420     *  @param  type    The class for the property to set.
421     *  @param  processClass    This is either the handler class or
422     *      {@code null}.
423     *  @param  stringConverter The
424     *      {@link StringConverter}
425     *      instance for the property; can be {@code null}.
426     *  @return The command line value handler instance.
427     *  @throws IllegalArgumentException    A command line value handler
428     *      instance cannot be created.
429     */
430    @SuppressWarnings( {"unchecked", "rawtypes"} )
431    private final <T> CmdLineValueHandler<T> createHandler( final Class<? extends T> type, final Class<?> processClass, final StringConverter<? extends T> stringConverter ) throws IllegalArgumentException
432    {
433        final var propertyTypeIsEnum = requireNonNullArgument( type, "type" ).isEnum();
434        final BiConsumer<String, T> valueSetter = m_PropertyMap::put;
435        CmdLineValueHandler<T> retValue = null;
436        Class<? extends CmdLineValueHandler<?>> handlerClass = null;
437        if( isNull( processClass ) )
438        {
439            if( propertyTypeIsEnum )
440            {
441                retValue = new EnumValueHandler( type, valueSetter );
442            }
443            else
444            {
445                //---* Infer the handler class, if necessary *-----------------
446                final var foundHandlerClass = retrieveValueHandlerClass( type );
447                if( foundHandlerClass.isPresent() )
448                {
449                    handlerClass = foundHandlerClass.get();
450                }
451                else if( nonNull( stringConverter ) )
452                {
453                    retValue = new SimpleCmdLineValueHandler<>( valueSetter, stringConverter );
454                }
455            }
456        }
457        else
458        {
459            if( !CmdLineValueHandler.class.isAssignableFrom( processClass ) )
460            {
461                throw new IllegalArgumentException( "'%s' is neither a StringConverter nor a CmdLineValueHandler".formatted( processClass.getName() ) );
462            }
463
464            //---* We got a command line value handler *-----------------------
465            handlerClass = (Class<? extends CmdLineValueHandler<?>>) processClass;
466        }
467
468        if( isNull( retValue ) )
469        {
470            if( isNull( handlerClass ) )
471            {
472                throw new IllegalArgumentException( "Could not determine a class for the Command Line Value Handler" );
473            }
474            try
475            {
476                final var constructor = handlerClass.getConstructor( BiConsumer.class );
477                retValue = (CmdLineValueHandler<T>) constructor.newInstance( valueSetter );
478            }
479            catch( final InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e )
480            {
481                throw new IllegalArgumentException( "Unable to create value handler from '%s'".formatted( handlerClass.getName() ), e );
482            }
483        }
484
485        //---* Done *----------------------------------------------------------
486        return retValue;
487    }   //  createHandler()
488
489    /**
490     *  <p>{@summary Creates the instance for the string converter based on the
491     *  given class for the type and class for the string converter
492     *  implementation.} If {@code stringConverterClass} is {@code null}, the
493     *  method will search for an implementation class in the internal
494     *  registry; if none can be found, {@code null} will be returned.</p>
495     *
496     *  @param  <T> The type of the property to convert.
497     *  @param  type    The class for the property to convert.
498     *  @param  stringConverterClass    The String converter class or
499     *      {@code null}.
500     *  @return The String converter instance.
501     */
502    @SuppressWarnings( {"unchecked", "rawtypes"} )
503    private final <T> StringConverter<T> createStringConverter( final Class<?> type, final Class<? extends StringConverter<?>> stringConverterClass )
504    {
505        requireNonNullArgument( type, "type" );
506
507        final StringConverter<T> retValue;
508        if( isNull( stringConverterClass ) )
509        {
510            if( type.isEnum() )
511            {
512                retValue = (StringConverter<T>) StringConverter.forEnum( (Class<? extends Enum>) type );
513            }
514            else
515            {
516                retValue = (StringConverter<T>) StringConverter.forClass( type ).orElse( null );
517            }
518        }
519        else
520        {
521            final var foundProvider = retrieveMethod( stringConverterClass, "provider" );
522            if( foundProvider.isPresent() )
523            {
524                try
525                {
526                    retValue = (StringConverter<T>) foundProvider.get().invoke( null );
527                }
528                catch( final IllegalAccessException | InvocationTargetException e )
529                {
530                    throw new IllegalArgumentException( format( MSG_InvalidStringConverter, stringConverterClass.getName() ), e );
531                }
532            }
533            else
534            {
535                try
536                {
537                    @SuppressWarnings( "unchecked" )
538                    final var constructor = (Constructor<StringConverter<T>>) stringConverterClass.getConstructor();
539                    retValue = constructor.newInstance();
540                }
541                catch( final NoSuchMethodException e )
542                {
543                    throw new IllegalArgumentException( "No default constructor for StringConverter: %s".formatted( stringConverterClass.getName() ), e );
544                }
545                catch( final InstantiationException | InvocationTargetException | IllegalAccessException e )
546                {
547                    throw new IllegalArgumentException( format( MSG_InvalidStringConverter, stringConverterClass.getName() ), e );
548                }
549            }
550        }
551
552        //---* Done *----------------------------------------------------------
553        return retValue;
554    }   //  createStringConverter()
555
556    /**
557     *  Executes the parsing.
558     *
559     *  @return The parsed CLI definition.
560     *  @throws XMLStreamException  Cannot parse the given input stream.
561     */
562    @SuppressWarnings( "SwitchStatementWithTooManyBranches" )
563    private final List<CLIDefinition> execute() throws XMLStreamException
564    {
565        List<CLIDefinition> retValue = List.of();
566        try
567        {
568            while( m_EventReader.hasNext() )
569            {
570                final var event = m_EventReader.nextEvent();
571                switch( event.getEventType() )
572                {
573                    case ATTRIBUTE:
574                    case CDATA:
575                    case CHARACTERS:
576                    case END_ELEMENT:
577                        //noinspection UseOfSystemOutOrSystemErr
578                        out.printf( "%d: %s%n", event.getEventType(), event );
579                        throw new XMLStreamException( format( MSG_ExpectedStartEvent, event.toString() ), event.getLocation() );
580
581                    case START_ELEMENT:
582                    {
583                        final var startElement = event.asStartElement();
584                        final var name = startElement.getName();
585                        if( name.getLocalPart().equals( XMLELEMENT_CLIDefinition ) )
586                        {
587                            retValue = handleCLIDefinition( startElement );
588                        }
589                        else
590                        {
591                            throw new XMLStreamException( format( MSG_WrongElement1, XMLELEMENT_CLIDefinition, name.toString() ), event.getLocation() );
592                        }
593                        break;
594                    }
595
596                    case COMMENT:
597                    case DTD:
598                    case END_DOCUMENT:
599                    case ENTITY_DECLARATION:
600                    case NAMESPACE:
601                    case NOTATION_DECLARATION:
602                    case PROCESSING_INSTRUCTION:
603                    case SPACE:
604                    case START_DOCUMENT:
605                        break;
606
607                    default:
608                        //noinspection UseOfSystemOutOrSystemErr
609                        out.printf( "%d: %s%n", event.getEventType(), event );
610                        throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
611                }
612            }
613        }
614        finally
615        {
616            m_EventReader.close();
617        }
618
619        //---* Done *----------------------------------------------------------
620        return retValue;
621    }   //  execute()
622
623    /**
624     *  Retrieves the URL for the CLI definition DTD file from the resources.
625     *
626     *  @return The URL for the file.
627     */
628    @API( status = INTERNAL, since = "0.0.1" )
629    public static final URL getCLIDefinitionDTDURL()
630    {
631        final var retValue = CLIDefinitionParser.class.getResource( format( "/%s", CLI_DEFINITION_DTD ) );
632
633        //---* Done *----------------------------------------------------------
634        return retValue;
635    }   //  getCLIDefinitionDTDURL()
636
637    /**
638     *  Retrieves the URL for the CLI definition XSD file from the resources.
639     *
640     *  @return The URL for the file.
641     */
642    @API( status = INTERNAL, since = "0.0.1" )
643    public static final URL getCLIDefinitionXSDURL()
644    {
645        final var retValue = CLIDefinitionParser.class.getResource( format( "/%s", CLI_DEFINITION_XSD ) );
646
647        //---* Done *----------------------------------------------------------
648        return retValue;
649    }   //  getCLIDefinitionXSDURL()
650
651    /**
652     *  Handles the {@value #XMLELEMENT_Alias} element.
653     *
654     *  @param  element The current element.
655     *  @return The option alias.
656     *  @throws XMLStreamException  A problem occurred while parsing the
657     *      element.
658     */
659    @SuppressWarnings( {"SwitchStatementWithTooManyBranches", "SwitchStatementWithTooFewBranches"} )
660    private final String handleAlias( final StartElement element ) throws XMLStreamException
661    {
662        String retValue = null;
663
664        //---* Get the attributes *--------------------------------------------
665        final var attributes = element.getAttributes();
666        while( attributes.hasNext() )
667        {
668            final var attribute = attributes.next();
669            final var name = attribute.getName();
670            retValue = switch( name.getLocalPart() )
671            {
672                case XMLATTRIBUTE_Name -> processAttrName( attribute );
673                default -> throw new XMLStreamException( format( MSG_WrongAttribute, name.toString() ), element.getLocation() );
674            };
675        }
676
677        //---* Proceed parsing ... *-------------------------------------------
678        var proceed = true;
679        while( m_EventReader.hasNext() && proceed )
680        {
681            final var event = m_EventReader.nextEvent();
682            switch( event.getEventType() )
683            {
684                case ATTRIBUTE:
685                case CDATA:
686                case DTD:
687                case END_DOCUMENT:
688                case ENTITY_DECLARATION:
689                case NAMESPACE:
690                case NOTATION_DECLARATION:
691                case PROCESSING_INSTRUCTION:
692                case START_DOCUMENT:
693                case START_ELEMENT:
694                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
695
696                case END_ELEMENT:
697                {
698                    final var endElement = event.asEndElement();
699                    final var name = endElement.getName();
700                    if( !name.equals( element.getName() ) )
701                    {
702                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
703                    }
704                    proceed = false;
705                    break;
706                }
707
708                case CHARACTERS:
709                case COMMENT:
710                case SPACE:
711                    break;
712
713                default:
714                    //noinspection UseOfSystemOutOrSystemErr
715                    out.printf( "%d: %s\n", event.getEventType(), event );
716                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
717            }
718        }
719
720        if( isEmpty( retValue ) )
721        {
722            throw new XMLStreamException( format( MSG_MissingValue, XMLATTRIBUTE_Name ), element.getLocation() );
723        }
724
725        //---* Done *----------------------------------------------------------
726        return retValue;
727    }   //  handleAlias()
728
729    /**
730     *  Handles the {@value #XMLELEMENT_CLIArgument} element.
731     *
732     *  @param  element The current element.
733     *  @return The parsed CLI definition.
734     *  @throws XMLStreamException  A problem occurred while parsing the
735     *      element.
736     */
737    @SuppressWarnings( {"SwitchStatementWithTooManyBranches", "NestedSwitchStatement", "OverlyLongMethod", "OverlyComplexMethod"} )
738    private CLIDefinition handleArgument( final StartElement element ) throws XMLStreamException
739    {
740        String format = null;
741        Class<? extends CmdLineValueHandler<?>> processorClass = null;
742        Class<? extends StringConverter<?>> stringConverterClass = null;
743        var index = -1;
744        var isMultiValue = false;
745        var isRequired = false;
746        String metaVar = null;
747        String propertyName = null;
748        Class<?> type = null;
749        String usage = null;
750        String usageKey = null;
751        CLIArgumentDefinition retValue = null;
752
753        //---* Get the attributes *--------------------------------------------
754        final var attributes = element.getAttributes();
755        while( attributes.hasNext() )
756        {
757            final var attribute = attributes.next();
758            final var name = attribute.getName();
759            switch( name.getLocalPart() )
760            {
761                case XMLATTRIBUTE_Handler -> processorClass = processAttrHandler( attribute );
762                case XMLATTRIBUTE_Index -> {
763                    try
764                    {
765                        index = Integer.parseInt( attribute.getValue() );
766                        if( index < 0 )
767                        {
768                            throw new XMLStreamException( format( MSG_InvalidValue, XMLATTRIBUTE_Index, Integer.toString( index ) ), attribute.getLocation() );
769                        }
770                    }
771                    catch( final NumberFormatException e )
772                    {
773                        final var xse = new XMLStreamException( format( MSG_InvalidValue, XMLATTRIBUTE_Index, attribute.getValue() ), attribute.getLocation(), e );
774                        xse.initCause( e );
775                        throw xse;
776                    }
777                }
778
779                case XMLATTRIBUTE_IsMultiValue -> isMultiValue = Boolean.parseBoolean( attribute.getValue() );
780                case XMLATTRIBUTE_IsRequired -> isRequired = Boolean.parseBoolean( attribute.getValue() );
781                case XMLATTRIBUTE_MetaVar -> metaVar = attribute.getValue();
782                case XMLATTRIBUTE_PropertyName -> propertyName = processAttrPropertyName( attribute );
783                case XMLATTRIBUTE_StringConversion -> stringConverterClass = processAttrStringConversion( attribute );
784                case XMLATTRIBUTE_Type -> type = processAttrType( attribute );
785                default -> throw new XMLStreamException( format( MSG_WrongAttribute, name.toString() ), element.getLocation() );
786            }
787        }
788
789        //---* Get the StringConverter instance *------------------------------
790        final StringConverter<?> stringConverter = createStringConverter( type, stringConverterClass );
791
792        //---* Get the handler instance *--------------------------------------
793        final CmdLineValueHandler<?> handler;
794        try
795        {
796            handler = createHandler( type, processorClass, stringConverter );
797        }
798        catch( final IllegalArgumentException e )
799        {
800            throw new XMLStreamException( "Cannot create Command Line Value Handler", e );
801        }
802
803        //---* Proceed parsing ... *-------------------------------------------
804        var proceed = true;
805        while( m_EventReader.hasNext() && proceed )
806        {
807            final var event = m_EventReader.nextEvent();
808            switch( event.getEventType() )
809            {
810                case ATTRIBUTE:
811                case CDATA:
812                case DTD:
813                case END_DOCUMENT:
814                case ENTITY_DECLARATION:
815                case NAMESPACE:
816                case NOTATION_DECLARATION:
817                case PROCESSING_INSTRUCTION:
818                case START_DOCUMENT:
819                    //noinspection UseOfSystemOutOrSystemErr
820                    out.printf( "%d: %s\n", event.getEventType(), event );
821                    throw new XMLStreamException( format( MSG_ExpectedStartEvent, event.toString() ), event.getLocation() );
822
823                case END_ELEMENT:
824                {
825                    final var endElement = event.asEndElement();
826                    final var name = endElement.getName();
827                    if( !name.equals( element.getName() ) )
828                    {
829                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
830                    }
831                    proceed = false;
832                    break;
833                }
834
835                case START_ELEMENT:
836                {
837                    final var startElement = event.asStartElement();
838                    final var name = startElement.getName();
839                    switch( name.getLocalPart() )
840                    {
841                        case XMLELEMENT_Format -> format = handleFormat( startElement );
842
843                        case XMLELEMENT_Usage ->
844                        {
845                            final var result = handleUsage( startElement );
846                            usageKey = result.get( XMLATTRIBUTE_Key );
847                            usage = result.get( XMLELEMENT_Usage );
848                        }
849
850                        default -> throw new XMLStreamException( format( MSG_WrongElement2, join( ", ", XMLELEMENT_Alias, XMLELEMENT_Format, XMLELEMENT_Usage ), name.toString() ), event.getLocation() );
851                    }
852                    break;
853                }
854
855                case CHARACTERS:
856                case COMMENT:
857                case SPACE:
858                    break;
859
860                default:
861                    //noinspection UseOfSystemOutOrSystemErr
862                    out.printf( "%d: %s\n", event.getEventType(), event );
863                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
864            }
865        }
866
867        //---* Create the return value *---------------------------------------
868        if( isEmptyOrBlank( metaVar ) )
869        {
870            //noinspection DataFlowIssue
871            metaVar = type.getSimpleName().toUpperCase( ROOT );
872        }
873        retValue = new CLIArgumentDefinition( propertyName, index, usage, usageKey, metaVar, isRequired, handler, isMultiValue, format );
874
875        //---* Done *----------------------------------------------------------
876        return retValue;
877    }   //  handleArgument()
878
879    /**
880     *  Handles the {@value #XMLELEMENT_CLIDefinition} element.
881     *
882     *  @param  element The current element.
883     *  @return The parsed CLI definition.
884     *  @throws XMLStreamException  A problem occurred while parsing the
885     *      element.
886     */
887    @SuppressWarnings( {"SwitchStatementWithTooManyBranches", "NestedSwitchStatement"} )
888    private final List<CLIDefinition> handleCLIDefinition( final StartElement element ) throws XMLStreamException
889    {
890        final List<CLIDefinition> retValue = new ArrayList<>();
891        var proceed = true;
892        while( m_EventReader.hasNext() && proceed )
893        {
894            final var event = m_EventReader.nextEvent();
895            switch( event.getEventType() )
896            {
897                case ATTRIBUTE:
898                case CDATA:
899                case DTD:
900                case END_DOCUMENT:
901                case ENTITY_DECLARATION:
902                case NAMESPACE:
903                case NOTATION_DECLARATION:
904                case PROCESSING_INSTRUCTION:
905                case START_DOCUMENT:
906                    throw new XMLStreamException( format( MSG_ExpectedStartEvent, event.toString() ), event.getLocation() );
907
908                case END_ELEMENT:
909                {
910                    final var endElement = event.asEndElement();
911                    final var name = endElement.getName();
912                    if( !name.equals( element.getName() ) )
913                    {
914                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
915                    }
916                    proceed = false;
917                    break;
918                }
919
920                case START_ELEMENT:
921                {
922                    final var startElement = event.asStartElement();
923                    final var name = startElement.getName();
924                    switch( name.getLocalPart() )
925                    {
926                        case XMLELEMENT_CLIArgument -> retValue.add( handleArgument( startElement ) );
927                        case XMLELEMENT_CLIOption -> retValue.add( handleOption( startElement ) );
928                        default -> throw new XMLStreamException( format( MSG_WrongElement2, join( ", ", XMLELEMENT_CLIOption, XMLELEMENT_CLIArgument ), name.toString() ), event.getLocation() );
929                    }
930                    break;
931                }
932
933                case CHARACTERS:
934                case COMMENT:
935                case SPACE:
936                    break;
937
938                default:
939                    //noinspection UseOfSystemOutOrSystemErr
940                    out.printf( "%d: %s\n", event.getEventType(), event );
941                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
942            }
943        }
944
945        //---* Done *----------------------------------------------------------
946        return retValue;
947    }   //  handleCLIDefinition()
948
949    /**
950     *  Handles the {@value #XMLELEMENT_Format} element.
951     *
952     *  @param  element The current element.
953     *  @return The format.
954     *  @throws XMLStreamException  A problem occurred while parsing the
955     *      element.
956     */
957    @SuppressWarnings( {"SwitchStatementWithTooManyBranches", "AssignmentToNull"} )
958    private final String handleFormat( final StartElement element ) throws XMLStreamException
959    {
960        String retValue = null;
961
962        var proceed = true;
963        while( m_EventReader.hasNext() && proceed )
964        {
965            final var event = m_EventReader.nextEvent();
966            switch( event.getEventType() )
967            {
968                case ATTRIBUTE:
969                case DTD:
970                case END_DOCUMENT:
971                case ENTITY_DECLARATION:
972                case NAMESPACE:
973                case NOTATION_DECLARATION:
974                case PROCESSING_INSTRUCTION:
975                case START_DOCUMENT:
976                case START_ELEMENT:
977                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
978
979                case END_ELEMENT:
980                {
981                    final var endElement = event.asEndElement();
982                    final var name = endElement.getName();
983                    if( !name.equals( element.getName() ) )
984                    {
985                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
986                    }
987                    proceed = false;
988                    break;
989                }
990
991                case CDATA:
992                case CHARACTERS:
993                {
994                    final var characters = event.asCharacters();
995                    retValue = characters.getData();
996                    break;
997                }
998
999                case COMMENT:
1000                case SPACE:
1001                    break;
1002
1003                default:
1004                    //noinspection UseOfSystemOutOrSystemErr
1005                    out.printf( "%d: %s\n", event.getEventType(), event );
1006                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
1007            }
1008        }
1009
1010        if( isEmpty( retValue ) ) retValue = null;
1011
1012        //---* Done *----------------------------------------------------------
1013        return retValue;
1014    }   //  handleFormat()
1015
1016    /**
1017     *  Handles the {@value #XMLELEMENT_CLIOption} element.
1018     *
1019     *  @param  element The current element.
1020     *  @return The parsed CLI definition.
1021     *  @throws XMLStreamException  A problem occurred while parsing the
1022     *      element.
1023     */
1024    @SuppressWarnings( {"SwitchStatementWithTooManyBranches", "NestedSwitchStatement", "OverlyLongMethod", "OverlyComplexMethod"} )
1025    private final CLIDefinition handleOption( final StartElement element ) throws XMLStreamException
1026    {
1027        String format = null;
1028        Class<? extends CmdLineValueHandler<?>> processorClass = null;
1029        Class<? extends StringConverter<?>> stringConverterClass = null;
1030        var isMultiValue = false;
1031        var isRequired = false;
1032        String metaVar = null;
1033        final List<String> names = new ArrayList<>();
1034        String propertyName = null;
1035        Class<?> type = null;
1036        String usage = null;
1037        String usageKey = null;
1038        CLIOptionDefinition retValue = null;
1039
1040        //---* Get the attributes *--------------------------------------------
1041        final var attributes = element.getAttributes();
1042        while( attributes.hasNext() )
1043        {
1044            final var attribute = attributes.next();
1045            final var name = attribute.getName();
1046            switch( name.getLocalPart() )
1047            {
1048                case XMLATTRIBUTE_Handler -> processorClass = processAttrHandler( attribute );
1049                case XMLATTRIBUTE_IsMultiValue -> isMultiValue = Boolean.parseBoolean( attribute.getValue() );
1050                case XMLATTRIBUTE_IsRequired -> isRequired = Boolean.parseBoolean( attribute.getValue() );
1051                case XMLATTRIBUTE_MetaVar -> metaVar = attribute.getValue();
1052                case XMLATTRIBUTE_Name -> names.add( processAttrName( attribute ) );
1053                case XMLATTRIBUTE_PropertyName -> propertyName = processAttrPropertyName( attribute );
1054                case XMLATTRIBUTE_StringConversion -> stringConverterClass = processAttrStringConversion( attribute );
1055                case XMLATTRIBUTE_Type -> type = processAttrType( attribute );
1056                default -> throw new XMLStreamException( format( MSG_WrongAttribute, name.toString() ), element.getLocation() );
1057            }
1058        }
1059
1060        //---* Get the StringConverter instance *------------------------------
1061        final StringConverter<?> stringConverter = createStringConverter( type, stringConverterClass );
1062
1063        //---* Get the handler instance *--------------------------------------
1064        final CmdLineValueHandler<?> handler;
1065        try
1066        {
1067            handler = createHandler( type, processorClass, stringConverter );
1068        }
1069        catch( final IllegalArgumentException e )
1070        {
1071            throw new XMLStreamException( "Cannot create Command Line Value Handler", e );
1072        }
1073
1074        //---* Proceed parsing ... *-------------------------------------------
1075        var proceed = true;
1076        while( m_EventReader.hasNext() && proceed )
1077        {
1078            final var event = m_EventReader.nextEvent();
1079            switch( event.getEventType() )
1080            {
1081                case ATTRIBUTE:
1082                case CDATA:
1083                case DTD:
1084                case END_DOCUMENT:
1085                case ENTITY_DECLARATION:
1086                case NAMESPACE:
1087                case NOTATION_DECLARATION:
1088                case PROCESSING_INSTRUCTION:
1089                case START_DOCUMENT:
1090                    //noinspection UseOfSystemOutOrSystemErr
1091                    out.printf( "%d: %s\n", event.getEventType(), event );
1092                    throw new XMLStreamException( format( MSG_ExpectedStartEvent, event.toString() ), event.getLocation() );
1093
1094                case END_ELEMENT:
1095                {
1096                    final var endElement = event.asEndElement();
1097                    final var name = endElement.getName();
1098                    if( !name.equals( element.getName() ) )
1099                    {
1100                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
1101                    }
1102                    proceed = false;
1103                    break;
1104                }
1105
1106                case START_ELEMENT:
1107                {
1108                    final var startElement = event.asStartElement();
1109                    final var name = startElement.getName();
1110                    switch( name.getLocalPart() )
1111                    {
1112                        case XMLELEMENT_Alias -> names.add( handleAlias( startElement ) );
1113                        case XMLELEMENT_Format -> format = handleFormat( startElement );
1114                        case XMLELEMENT_Usage ->
1115                        {
1116                            final var result = handleUsage( startElement );
1117                            usageKey = result.get( XMLATTRIBUTE_Key );
1118                            usage = result.get( XMLELEMENT_Usage );
1119                        }
1120                        default -> throw new XMLStreamException( format( MSG_WrongElement2, join( ", ", XMLELEMENT_Alias, XMLELEMENT_Format, XMLELEMENT_Usage ), name.toString() ), event.getLocation() );
1121                    }
1122                    break;
1123                }
1124
1125                case CHARACTERS:
1126                case COMMENT:
1127                case SPACE:
1128                    break;
1129
1130                default:
1131                    //noinspection UseOfSystemOutOrSystemErr
1132                    out.printf( "%d: %s\n", event.getEventType(), event );
1133                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
1134            }
1135        }
1136
1137        //---* Create the return value *---------------------------------------
1138        if( names.size() != new HashSet<>( names ).size() )
1139        {
1140            throw new XMLStreamException( "Duplicate option names", element.getLocation() );
1141        }
1142        if( isEmptyOrBlank( metaVar ) )
1143        {
1144            //noinspection DataFlowIssue
1145            metaVar = type.getSimpleName().toUpperCase( ROOT );
1146        }
1147        retValue = new CLIOptionDefinition( propertyName, names, usage, usageKey, metaVar, isRequired, handler, isMultiValue, format );
1148
1149        //---* Done *----------------------------------------------------------
1150        return retValue;
1151    }   //  handleOption()
1152
1153    /**
1154     *  Handles the {@value #XMLELEMENT_Usage} element.
1155     *
1156     *  @param  element The current element.
1157     *  @return The usage and the usage key.
1158     *  @throws XMLStreamException  A problem occurred while parsing the
1159     *      element.
1160     */
1161    @SuppressWarnings( {"SwitchStatementWithTooFewBranches", "SwitchStatementWithTooManyBranches"} )
1162    private final Map<String,String> handleUsage( final StartElement element ) throws XMLStreamException
1163    {
1164        final Map<String,String> retValue = new HashMap<>();
1165
1166        //---* Get the attributes *--------------------------------------------
1167        final var attributes = element.getAttributes();
1168        while( attributes.hasNext() )
1169        {
1170            final var attribute = attributes.next();
1171            final var name = attribute.getName();
1172            switch( name.getLocalPart() )
1173            {
1174                case XMLATTRIBUTE_Key -> retValue.put( XMLATTRIBUTE_Key, attribute.getValue() );
1175                default -> throw new XMLStreamException( format( MSG_WrongAttribute, name.toString() ), element.getLocation() );
1176            }
1177        }
1178
1179        //---* Proceed parsing ... *-------------------------------------------
1180        var proceed = true;
1181        while( m_EventReader.hasNext() && proceed )
1182        {
1183            final var event = m_EventReader.nextEvent();
1184            switch( event.getEventType() )
1185            {
1186                case ATTRIBUTE:
1187                case DTD:
1188                case END_DOCUMENT:
1189                case ENTITY_DECLARATION:
1190                case NAMESPACE:
1191                case NOTATION_DECLARATION:
1192                case PROCESSING_INSTRUCTION:
1193                case START_DOCUMENT:
1194                case START_ELEMENT:
1195                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
1196
1197                case END_ELEMENT:
1198                {
1199                    final var endElement = event.asEndElement();
1200                    final var name = endElement.getName();
1201                    if( !name.equals( element.getName() ) )
1202                    {
1203                        throw new XMLStreamException( format( MSG_UnexpectedEndEvent, event.toString(), element.getName(), name ), event.getLocation() );
1204                    }
1205                    proceed = false;
1206                    break;
1207                }
1208
1209                case CDATA:
1210                case CHARACTERS:
1211                {
1212                    final var characters = event.asCharacters();
1213                    retValue.put( XMLELEMENT_Usage, characters.getData() );
1214                    break;
1215                }
1216
1217                case COMMENT:
1218                case SPACE:
1219                    break;
1220
1221                default:
1222                    //noinspection UseOfSystemOutOrSystemErr
1223                    out.printf( "%d: %s\n", event.getEventType(), event );
1224                    throw new XMLStreamException( format( MSG_UnexpectedEvent, event.toString() ), event.getLocation() );
1225            }
1226        }
1227
1228        //---* Done *----------------------------------------------------------
1229        return retValue;
1230    }   //  handleUsage()
1231
1232    /**
1233     *  Parses the given
1234     *  {@link InputStream}.
1235     *
1236     *  @param  inputStream The input stream that should contain the XML CLI
1237     *      definition.
1238     *  @param  propertyMap The target data structure for the values from the
1239     *      command line.
1240     *  @param  validate    {@code true} if the given XML should be validated
1241     *      against the schema {@code CLIDefinition.xsd} previous to parsing
1242     *      it, {@code false} if the validation can be omitted.
1243     *
1244     *  @return The parsed CLI definition.
1245     *  @throws XMLStreamException  Cannot parse the given input stream.
1246     *  @throws IOException Cannot read the given input stream.
1247     */
1248    public static final List<CLIDefinition> parse( final InputStream inputStream, final Map<String,Object> propertyMap, final boolean validate ) throws XMLStreamException, IOException
1249    {
1250        final var parser = new CLIDefinitionParser( inputStream, propertyMap );
1251        if( validate ) parser.validate();
1252        final var retValue = parser.execute();
1253
1254        //---* Done *----------------------------------------------------------
1255        return retValue;
1256    }   //  parse()
1257
1258    /**
1259     *  Processes the attribute {@value #XMLATTRIBUTE_Handler}.
1260     *
1261     *  @param  attribute   The attribute.
1262     *  @return The handler class.
1263     *  @throws XMLStreamException  The attribute is somehow invalid.
1264     */
1265    @SuppressWarnings( "unchecked" )
1266    private static final Class<? extends CmdLineValueHandler<?>> processAttrHandler( final Attribute attribute ) throws XMLStreamException
1267    {
1268
1269        final var name = attribute.getName();
1270
1271        final var value = attribute.getValue();
1272        final Class<?> result;
1273        if( isNotEmptyOrBlank( value ) )
1274        {
1275            try
1276            {
1277                result = Class.forName( value );
1278            }
1279            catch( final ClassNotFoundException e )
1280            {
1281                final var xse = new XMLStreamException( format( MSG_InvalidValue, name.toString(), value ), attribute.getLocation(), e );
1282                xse.initCause( e );
1283                throw xse;
1284            }
1285            if( !CmdLineValueHandler.class.isAssignableFrom( result ) )
1286            {
1287                throw new XMLStreamException( format( MSG_InvalidValue, name.toString(), result.getName() ), attribute.getLocation() );
1288            }
1289        }
1290        else
1291        {
1292            throw new XMLStreamException( format( MSG_MissingValue, name.toString() ), attribute.getLocation() );
1293        }
1294
1295        @SuppressWarnings( "RedundantExplicitVariableType" )
1296        final Class<? extends CmdLineValueHandler<?>> retValue = (Class<? extends CmdLineValueHandler<?>>) result;
1297
1298        //---* Done *----------------------------------------------------------
1299        return retValue;
1300    }   //  processAttrHandler()
1301
1302    /**
1303     *  Processes the attribute {@value org.tquadrat.foundation.lang.CommonConstants#XMLATTRIBUTE_Name}.
1304     *
1305     *  @param  attribute   The attribute.
1306     *  @return The property name.
1307     *  @throws XMLStreamException  The attribute is somehow invalid.
1308     */
1309    private static final String processAttrName( final Attribute attribute ) throws XMLStreamException
1310    {
1311        final var name = attribute.getName();
1312
1313        final var retValue = attribute.getValue();
1314        try
1315        {
1316            validateOptionName( retValue );
1317        }
1318        catch( final IllegalArgumentException e )
1319        {
1320            final var xse = new XMLStreamException( join( "\n", format( MSG_InvalidValue, name.toString(), retValue ), e.getMessage() ), attribute.getLocation(), e );
1321            xse.initCause( e );
1322            throw xse;
1323        }
1324
1325        //---* Done *----------------------------------------------------------
1326        return retValue;
1327    }   //  processAttrPropertyName()
1328
1329    /**
1330     *  Processes the attribute {@value #XMLATTRIBUTE_PropertyName}.
1331     *
1332     *  @param  attribute   The attribute.
1333     *  @return The property name.
1334     *  @throws XMLStreamException  The attribute is somehow invalid.
1335     */
1336    private static final String processAttrPropertyName( final Attribute attribute ) throws XMLStreamException
1337    {
1338        final var name = attribute.getName();
1339
1340        final var retValue = attribute.getValue();
1341        if( isNotEmptyOrBlank( retValue ) )
1342        {
1343            if( !isValidName( retValue ) )
1344            {
1345                throw new XMLStreamException( format( MSG_InvalidValue, name.toString(), retValue ), attribute.getLocation() );
1346            }
1347        }
1348        else
1349        {
1350            throw new XMLStreamException( format( MSG_MissingValue, name.toString() ), attribute.getLocation() );
1351        }
1352
1353        //---* Done *----------------------------------------------------------
1354        return retValue;
1355    }   //  processAttrPropertyName()
1356
1357    /**
1358     *  Processes the attribute {@value #XMLATTRIBUTE_StringConversion}.
1359     *
1360     *  @param  attribute   The attribute.
1361     *  @return The
1362     *      {@link StringConverter}
1363     *      implementation class.
1364     *  @throws XMLStreamException  The attribute is somehow invalid.
1365     */
1366    private static final Class<? extends StringConverter<?>> processAttrStringConversion( final Attribute attribute ) throws XMLStreamException
1367    {
1368        final var name = attribute.getName();
1369
1370        final var value = attribute.getValue();
1371        final Class<?> result;
1372        if( isNotEmptyOrBlank( value ) )
1373        {
1374            try
1375            {
1376                result = Class.forName( value );
1377            }
1378            catch( final ClassNotFoundException e )
1379            {
1380                final var xse = new XMLStreamException( format( MSG_InvalidValue, name.toString(), value ), attribute.getLocation(), e );
1381                xse.initCause( e );
1382                throw xse;
1383            }
1384            if( !StringConverter.class.isAssignableFrom( result ) )
1385            {
1386                throw new XMLStreamException( format( MSG_InvalidStringConverter, result.getName() ), attribute.getLocation() );
1387            }
1388        }
1389        else
1390        {
1391            throw new XMLStreamException( format( MSG_MissingValue, name.toString() ), attribute.getLocation() );
1392        }
1393
1394        @SuppressWarnings( "unchecked" )
1395        final var retValue = (Class<? extends StringConverter<?>>) result;
1396
1397        //---* Done *----------------------------------------------------------
1398        return retValue;
1399    }   //  processAttrStringConversion()
1400
1401    /**
1402     *  Processes the attribute {@value #XMLATTRIBUTE_Type}.
1403     *
1404     *  @param  attribute   The attribute.
1405     *  @return The class for the type.
1406     *  @throws XMLStreamException  The attribute is somehow invalid.
1407     */
1408    private static final Class<?> processAttrType( final Attribute attribute ) throws XMLStreamException
1409    {
1410        final Class<?> retValue;
1411
1412        final var name = attribute.getName();
1413
1414        final var value = attribute.getValue();
1415        if( isNotEmptyOrBlank( value ) )
1416        {
1417            try
1418            {
1419                retValue = Class.forName( value );
1420            }
1421            catch( final ClassNotFoundException e )
1422            {
1423                final var xse = new XMLStreamException( format( MSG_InvalidValue, name.toString(), value ), attribute.getLocation(), e );
1424                xse.initCause( e );
1425                throw xse;
1426            }
1427        }
1428        else
1429        {
1430            throw new XMLStreamException( format( MSG_MissingValue, name.toString() ), attribute.getLocation() );
1431        }
1432
1433        //---* Done *----------------------------------------------------------
1434        return retValue;
1435    }   //  processAttrType()
1436
1437    /**
1438     *  Retrieves the CLI definition DTD file from the resources.
1439     *
1440     *  @return The input stream for the file.
1441     */
1442    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
1443    @API( status = INTERNAL, since = "0.0.1" )
1444    public static final InputStream retrieveCLIDefinitionDTD()
1445    {
1446        final var retValue = CLIDefinitionParser.class.getResourceAsStream( format( "/%s", CLI_DEFINITION_DTD ) );
1447
1448        //---* Done *----------------------------------------------------------
1449        return retValue;
1450    }   //  retrieveCLIDefinitionDTD()
1451
1452    /**
1453     *  Retrieves the CLI definition XSD file from the resources.
1454     *
1455     *  @return The input stream for the file.
1456     */
1457    @API( status = INTERNAL, since = "0.0.1" )
1458    public static final InputStream retrieveCLIDefinitionXSD()
1459    {
1460        final var retValue = CLIDefinitionParser.class.getResourceAsStream( format( "/%s", CLI_DEFINITION_XSD ) );
1461
1462        //---* Done *----------------------------------------------------------
1463        return retValue;
1464    }   //  retrieveCLIDefinitionXSD()
1465
1466    /**
1467     *  <p>{@summary Retrieves the class for the value handler if the property
1468     *  class is not an {@code enum} type.}</p>
1469     *  <p>{@code enum} types have to be handled separately.</p>
1470     *
1471     *  @param  propertyClass   The class of the property that should be set.
1472     *  @return An instance of
1473     *      {@link Optional}
1474     *      that holds the effective handler class if the property class is not
1475     *      an {@code enum}.
1476     */
1477    private final Optional<Class<? extends CmdLineValueHandler<?>>> retrieveValueHandlerClass( final Class<?> propertyClass )
1478    {
1479        final Optional<Class<? extends CmdLineValueHandler<?>>> retValue = requireNonNullArgument( propertyClass, "propertyClass" ).isEnum()
1480            ? Optional.empty()
1481            : Optional.ofNullable( m_HandlerClasses.get( propertyClass ) );
1482
1483        //---* Done *----------------------------------------------------------
1484        return retValue;
1485    }   //  retrieveValueHandlerClass()
1486
1487    /**
1488     *  Validates the given XML.
1489     *
1490     *  @throws XMLStreamException  The validation failed.
1491     *  @throws IOException Cannot read the given input stream.
1492     */
1493    private final void validate() throws IOException, XMLStreamException
1494    {
1495        //---* Create the stream reader for validation *-----------------------
1496        final var xmlInputFactory = XMLInputFactory.newInstance();
1497        final var streamReader = xmlInputFactory.createXMLStreamReader( new StringReader( m_XMLContents ) );
1498
1499        //---* Create the schema factory for the validation *------------------
1500        final var schemaFactory = SchemaFactory.newInstance( W3C_XML_SCHEMA_NS_URI );
1501        try
1502        {
1503            final var schema = schemaFactory.newSchema( getCLIDefinitionXSDURL() );
1504
1505            //---* Create the validator *--------------------------------------
1506            final var validator = schema.newValidator();
1507
1508            //---* Validate *--------------------------------------------------
1509            validator.validate( new StAXSource( streamReader ) );
1510        }
1511        catch( @SuppressWarnings( "CaughtExceptionImmediatelyRethrown" ) final IOException e )
1512        {
1513            //---* Should not happen ... *-------------------------------------
1514            throw e;
1515        }
1516        catch( final SAXParseException e )
1517        {
1518            final var xse = new XMLStreamException( e.getMessage(), new ExceptionLocation( e ), e );
1519            xse.initCause( e );
1520            throw xse;
1521        }
1522        catch( final SAXException e )
1523        {
1524            throw new XMLStreamException( e.getMessage(), e );
1525        }
1526    }   //  validate()
1527}
1528//  class CLIDefinitionParser
1529
1530/*
1531 *  End of File
1532 */