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.CmdLineException;
024import org.tquadrat.foundation.config.spi.CLIArgumentDefinition;
025import org.tquadrat.foundation.config.spi.CLIDefinition;
026import org.tquadrat.foundation.config.spi.CLIOptionDefinition;
027import org.tquadrat.foundation.config.spi.Parameters;
028import org.tquadrat.foundation.util.StringUtils;
029
030import java.io.File;
031import java.io.IOException;
032import java.util.*;
033import java.util.stream.Stream;
034import java.util.stream.StreamSupport;
035
036import static java.lang.String.format;
037import static java.nio.file.Files.lines;
038import static java.util.Spliterator.IMMUTABLE;
039import static java.util.Spliterator.NONNULL;
040import static java.util.Spliterators.spliterator;
041import static java.util.stream.Collectors.joining;
042import static java.util.stream.Collectors.toList;
043import static org.apiguardian.api.API.Status.INTERNAL;
044import static org.tquadrat.foundation.config.CLIBeanSpec.ARG_FILE_ESCAPE;
045import static org.tquadrat.foundation.config.CLIBeanSpec.LEAD_IN;
046import static org.tquadrat.foundation.config.CmdLineException.*;
047import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
048import static org.tquadrat.foundation.lang.Objects.*;
049import static org.tquadrat.foundation.util.StringUtils.isNotEmpty;
050import static org.tquadrat.foundation.util.SystemUtils.systemPropertiesAsStringMap;
051import static org.tquadrat.foundation.util.Template.replaceVariable;
052
053/**
054 *  The parser for the command line arguments.
055 *
056 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
057 *  @version $Id: ArgumentParser.java 1120 2024-03-16 09:48:00Z tquadrat $
058 *  @since 0.0.1
059 *
060 *  @UMLGraph.link
061 */
062@ClassVersion( sourceVersion = "$Id: ArgumentParser.java 1120 2024-03-16 09:48:00Z tquadrat $" )
063@API( status = INTERNAL, since = "0.0.1" )
064public class ArgumentParser
065{
066        /*---------------*\
067    ====** Inner Classes **====================================================
068        \*---------------*/
069    /**
070     *  <p>{@summary This class is essentially a pointer over the
071     *  {@link String}
072     *  array with the command line arguments.} It can move forward, and it can
073     *  look ahead.</p>
074     *  <p>But it will also resolve the references to argument files, and it
075     *  will translate single letter options without blanks between option and
076     *  value into two entries, as well as long entries where an equal sign is
077     *  used.</p>
078     *
079     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
080     *  @version $Id: ArgumentParser.java 1120 2024-03-16 09:48:00Z tquadrat $
081     *  @since 0.0.1
082     *
083     *  @UMLGraph.link
084     */
085    @ClassVersion( sourceVersion = "$Id: ArgumentParser.java 1120 2024-03-16 09:48:00Z tquadrat $" )
086    @API( status = INTERNAL, since = "0.0.1" )
087    private final class CmdLineImpl implements Iterator<String>, Parameters
088    {
089            /*------------*\
090        ====** Attributes **===================================================
091            \*------------*/
092        /**
093         *  The arguments list.
094         */
095        private final List<String> m_ArgumentList = new ArrayList<>();
096
097        /**
098         *  The current position in the arguments list.
099         */
100        private int m_CurrentPos;
101
102            /*--------------*\
103        ====** Constructors **=================================================
104            \*--------------*/
105        /**
106         *  Creates a new object for CmdLineImpl.
107         *
108         *  @param  args    The arguments list.
109         */
110        @SuppressWarnings( {"IfStatementWithTooManyBranches", "OverlyComplexMethod"} )
111        public CmdLineImpl( final String... args )
112        {
113            assert nonNull( args ) : "args is null";
114
115            final Collection<String> failedFiles = new HashSet<>();
116            var expandedToFile = true; // Flag that indicates whether an argument file was encountered
117            var inBuffer = List.of( args );
118            List<String> outBuffer;
119
120            while( expandedToFile )
121            {
122                expandedToFile = false;
123                outBuffer = new ArrayList<>( inBuffer.size() );
124                for( final var arg : inBuffer )
125                {
126                    if( arg.startsWith( ARG_FILE_ESCAPE ) && !failedFiles.contains( arg ) )
127                    {
128                        outBuffer.addAll( loadArgumentFile( arg, failedFiles ) );
129                        expandedToFile = true;
130                    }
131                    else
132                    {
133                        outBuffer.add( arg );
134                    }
135                }
136                inBuffer = outBuffer;
137            }
138
139            var parsingOptions = parsingOptions();
140            for( final var arg : inBuffer )
141            {
142                if( parsingOptions )
143                {
144                    //noinspection ConstantExpression
145                    if( arg.equals( LEAD_IN + LEAD_IN ) )
146                    {
147                        //---* The 'Stop Options Processing' token *-----------
148                        m_ArgumentList.add( arg );
149                        parsingOptions = false;
150                    }
151                    else //noinspection ConstantExpression
152                        if( arg.startsWith( LEAD_IN + LEAD_IN ) )
153                    /*
154                     * Sequence is crucial! We need to check for the '--'
155                     * prefix before we check for the '-' prefix, otherwise
156                     * the result is ... interesting
157                     */
158                    {
159                        //---* Long option *-----------------------------------
160                        final var pos = arg.indexOf( '=' );
161                        if( pos > 3 )
162                        {
163                            m_ArgumentList.add( arg.substring( 0, pos ) );
164                            m_ArgumentList.add( arg.substring( pos + 1 ) );
165                        }
166                        else
167                        {
168                            m_ArgumentList.add( arg );
169                        }
170                    }
171                    else if( arg.startsWith( LEAD_IN ) )
172                    {
173                        //---* Single letter option *--------------------------
174                        if( arg.length() > 2 )
175                        {
176                            m_ArgumentList.add( arg.substring( 0, 2 ) );
177                            m_ArgumentList.add( arg.substring( 2 ) );
178                        }
179                        else
180                        {
181                            m_ArgumentList.add( arg );
182                        }
183                    }
184                    else
185                    {
186                        //---* No option at all, or an option argument *-------
187                        m_ArgumentList.add( arg );
188                    }
189                }
190                else
191                {
192                    m_ArgumentList.add( arg );
193                }
194            }
195
196            //---* We start at 0 *---------------------------------------------
197            m_CurrentPos = 0;
198        }   //  CmdLineImpl()
199
200            /*---------*\
201        ====** Methods **======================================================
202            \*---------*/
203        /**
204         *  Returns the current token from the arguments list.
205         *
206         *  @return The current token.
207         */
208        public final String getCurrentToken() { return m_ArgumentList.get( m_CurrentPos ); }
209
210        /**
211         *  {@inheritDoc}
212         */
213        @Override
214        public final String getParameter( final int index ) throws CmdLineException
215        {
216            assert index >= 0 : "index is less than 0";
217
218            String retValue = null;
219            if( m_CurrentPos + index < m_ArgumentList.size() )
220            {
221                retValue = m_ArgumentList.get( m_CurrentPos + index );
222                if( retValue.startsWith( LEAD_IN ) && parsingOptions() )
223                {
224                    //---* We found the next option *--------------------------
225                    throw new CmdLineException( getCurrentOptionDefinition(), MSG_MissingOperand, MSGKEY_MissingOperand, getOptionName() );
226                }
227            }
228            else
229            {
230                throw new CmdLineException( getCurrentOptionDefinition(), MSG_MissingOperand, MSGKEY_MissingOperand, getOptionName() );
231            }
232
233            //---* Done *------------------------------------------------------
234            return retValue;
235        }   //  getParameter()
236
237        /**
238         *  Checks if there are more entries.
239         *
240         *  @return {@code true} if there are more entries,
241         *      {@code false} otherwise.
242         */
243        @Override
244        public final boolean hasNext() { return m_CurrentPos < m_ArgumentList.size(); }
245
246        /**
247         *  {@inheritDoc}
248         */
249        @Override
250        public final boolean isParameter( final int index ) throws CmdLineException
251        {
252            assert index >= 0 : "index is less than 0";
253
254            var retValue = m_CurrentPos + index < m_ArgumentList.size();
255            if( retValue && parsingOptions() )
256            {
257                var pos = m_CurrentPos + index;
258                while( retValue && (pos < m_ArgumentList.size()) )
259                {
260                    retValue = !(m_ArgumentList.get( pos++ ).startsWith( LEAD_IN ) );
261                }
262            }
263
264            //---* Done *------------------------------------------------------
265            return retValue;
266        }   //  getParameter()
267
268        /**
269         *  <p>{@summary Loads an argument file as specified by the given
270         *  argument and returns the contents of that file as additional
271         *  command line arguments.}</p>
272         *  <p>If no file could be retrieved for the name given with the
273         *  argument, that argument will be added to the list of failed files
274         *  and the unchanged argument will be returned.</p>
275         *  <p>Variables of the form <code>${<i>&lt;name&gt;</i>}</code> will
276         *  be replaced by the value for <i>name</i> from the system properties
277         *  ({@link System#getProperty(String)}).</p>
278         *  <p>Lines that starts with &quot;#&quot; are comments; if an
279         *  argument has to start with &quot;#&quot;, it has to be escaped with
280         *  &quot;\#&quot;.</p>
281         *
282         *  @param  arg The command line argument.
283         *  @param  failedFiles The list of failed files.
284         *  @return The additional command line arguments as read from the
285         *      file, if it could be opened.
286         */
287        @SuppressWarnings( "resource" )
288        private final Collection<String> loadArgumentFile( final String arg, @SuppressWarnings( "BoundedWildcard" ) final Collection<String> failedFiles )
289        {
290            final var systemProperties = systemPropertiesAsStringMap();
291
292            Collection<String> retValue;
293            if( failedFiles.contains( arg ) )
294            {
295                retValue = List.of( arg );
296            }
297            else
298            {
299                try
300                {
301                    final var argumentFile = new File( arg.substring( 1 ) )
302                        .getCanonicalFile()
303                        .getAbsoluteFile();
304                    retValue = lines( argumentFile.toPath() )
305                        .filter( line -> !line.startsWith( "#" ) ) // Drop the comments
306                        .map( line -> line.startsWith( "\\" ) ? line.substring( 1 ) : line )
307                        .filter( StringUtils::isNotEmptyOrBlank ) // Drop empty lines
308                        .map( line -> replaceVariable( line, systemProperties ) )
309                        .collect( toList() );
310                }
311                catch( final IOException ignored )
312                {
313                    retValue = List.of( arg );
314                    failedFiles.add( arg );
315                }
316            }
317
318            //---* Done *------------------------------------------------------
319            return retValue;
320        }   //  loadArgumentFile()
321
322        /**
323         *  {@inheritDoc}
324         */
325        @SuppressWarnings( "NewExceptionWithoutArguments" )
326        @Override
327        public final String next() throws NoSuchElementException
328        {
329            if( !hasNext() ) throw new NoSuchElementException();
330            final var retValue = getCurrentToken();
331            proceed( 1 );
332
333            //---* Done *------------------------------------------------------
334            return retValue;
335        }   //  next()
336
337        /**
338         *  Skip the given number of entries.
339         *
340         *  @param  n   The number of entries to skip; must be greater 0.
341         */
342        public final void proceed( final int n )
343        {
344            assert n >= 0 : "n less than 0";
345
346            m_CurrentPos += n;
347        }   //  proceed()
348
349        /**
350         *  In case the entry is a combination from option and the related
351         *  parameter (like {@code --arg value} or, for single character
352         *  options, {@code -p value}), this method is used to put it back to
353         *  the command line.
354         *
355         *  @param  part1   The first part, usually the option.
356         *  @param  part2   The second part, usually the value.
357         */
358        @SuppressWarnings( "unused" )
359        public final void putback( final String part1, final String part2 )
360        {
361            assert isNotEmpty( part1 ) : "part1 is empty";
362            assert isNotEmpty( part2 ) : "part2 is empty";
363
364            m_ArgumentList.set( m_CurrentPos, part2 );
365            m_ArgumentList.add( m_CurrentPos, part1 );
366        }   //  putback()
367    }
368    //  class CmdLineImpl
369
370        /*------------*\
371    ====** Attributes **=======================================================
372        \*------------*/
373    /**
374     *  The
375     *  {@link CLIDefinition}
376     *  instances for arguments.
377     */
378    private final List<CLIArgumentDefinition> m_ArgumentDefinitions = new ArrayList<>();
379
380    /**
381     *  The definition for the current command line entry.
382     */
383    @SuppressWarnings( "UseOfConcreteClass" )
384    private CLIOptionDefinition m_CurrentOptionDefinition = null;
385
386    /**
387     *  The
388     *  {@link CLIDefinition}
389     *  instances for options.
390     */
391    private final Map<String,CLIOptionDefinition> m_OptionDefinitions = new TreeMap<>();
392
393    /**
394     *  {@code true} (the default) if options has to be parsed. If set to
395     *  {@code false}, only arguments are parsed.
396     *
397     *  @see #stopOptionParsing()
398     */
399    private boolean m_ParsingOptions = false;
400
401        /*--------------*\
402    ====** Constructors **=====================================================
403        \*--------------*/
404    /**
405     *  Creates a new {@code ArgumentParser} instance.
406     *
407     *  @param  cliDefinitions  The definition for the command line options and
408     *      arguments from the configuration bean specification.
409     */
410    public ArgumentParser( final Collection<? extends CLIDefinition> cliDefinitions )
411    {
412        //---* Add the definitions to the registry *---------------------------
413        for( final var cliDefinition : requireNonNullArgument( cliDefinitions, "cliDefinitions" ) )
414        {
415            if( cliDefinition.isArgument() )
416            {
417                addArgument( cliDefinition );
418            }
419            else
420            {
421                addOption( cliDefinition );
422            }
423        }
424
425        //---* Check the consistency of the arguments list *-------------------
426        if( m_ArgumentDefinitions.stream().anyMatch( Objects::isNull ) )
427        {
428            final var indexes = Stream.iterate( 0, i -> i < m_ArgumentDefinitions.size(), i -> i + 1 )
429                .filter( i -> isNull( m_ArgumentDefinitions.get( i ) ) )
430                .map( i -> Integer.toString( i ) )
431                .collect( joining( ", " ) );
432            throw new IllegalArgumentException( "Missing index: %s - Gap in Sequence".formatted( indexes ) );
433        }
434    }   //  ArgumentParser()
435
436        /*---------*\
437    ====** Methods **==========================================================
438        \*---------*/
439    /**
440     *  Adds an argument definition to the registry for arguments.
441     *
442     *  @param  definition  The argument definition to add.
443     */
444    private final void addArgument( final CLIDefinition definition )
445    {
446        final var argumentDefinition = (CLIArgumentDefinition) requireNonNullArgument( definition, "definition" );
447
448        //--* Make sure the argument will fit in the list *--------------------
449        final var index = argumentDefinition.index();
450        while( index >= m_ArgumentDefinitions.size() )
451        {
452            m_ArgumentDefinitions.add( null );
453        }
454        if( nonNull( m_ArgumentDefinitions.get( index ) ) )
455        {
456            throw new IllegalArgumentException( "Argument index '%d' is used more than once".formatted( index ) );
457        }
458        m_ArgumentDefinitions.set( index, argumentDefinition );
459    }   //  addArgument()
460
461    /**
462     *  Adds an option definition to the registry for options.
463     *
464     *  @param  definition  The option definition to add.
465     */
466    private final void addOption( final CLIDefinition definition )
467    {
468        //---* We have at least one option ... *-------------------------------
469        m_ParsingOptions = true;
470
471        final var optionDefinition = (CLIOptionDefinition) requireNonNullArgument( definition, "definition" );
472        checkOptionNotYetUsed( optionDefinition.name() );
473        m_OptionDefinitions.put( optionDefinition.name(), optionDefinition );
474        for( final var alias : optionDefinition.aliases() )
475        {
476            checkOptionNotYetUsed( alias );
477            m_OptionDefinitions.put( alias, optionDefinition );
478        }
479}   //  addOption()
480
481    /**
482     *  Finds an option definition by the given option name.
483     *
484     *  @param  name    The option name.
485     *  @return The option definition.
486     *  @throws CmdLineException    There is no option definition for the
487     *      given option name.
488     */
489    private final CLIOptionDefinition findOptionDefinition( final String name )
490    {
491        assert nonNull( name ) : "name is null";
492
493        final var retValue = m_OptionDefinitions.get( name );
494        if( isNull( retValue ) )
495        {
496            throw new CmdLineException( format( MSG_OptionInvalid, name ), MSGKEY_OptionInvalid, name );
497        }
498
499        //---* Done *----------------------------------------------------------
500        return retValue;
501    }   //  findOptionDefinition()
502
503    /**
504     *  Returns the current CLI option definition.
505     *
506     *  @return The current CLI option definition.
507     */
508    final CLIOptionDefinition getCurrentOptionDefinition() { return m_CurrentOptionDefinition; }
509
510    /**
511     *  Returns the name of the option that is being processed currently.
512     *
513     *  @return The name of the current option.
514     */
515    final String getOptionName()
516    {
517        final var retValue = nonNull( m_CurrentOptionDefinition ) ? m_CurrentOptionDefinition.name() : null;
518
519        //---* Done *----------------------------------------------------------
520        return retValue;
521    }   //  getOptionName()
522
523    /**
524     *  Checks if the given token is an option (as opposed to an argument).
525     *  Option tokens will have a hyphen
526     *  ({@value org.tquadrat.foundation.config.CLIBeanSpec#LEAD_IN})
527     *  as their first character.
528     *
529     *  @param  token   The token to test.
530     *  @return {@code true} if the given token is an option,
531     *      {@code false} if it is an argument, or if no (more) options are
532     *      expected at all.
533     *
534     *  @see #stopOptionParsing()
535     *  @see #m_ParsingOptions
536     */
537    private final boolean isOption( final String token )
538    {
539        assert nonNull( token ) : "token is null";
540        assert !EMPTY_STRING.equals( token ) : "token is empty";
541
542        final var retValue = m_ParsingOptions && token.startsWith( LEAD_IN );
543
544        //---* Done *----------------------------------------------------------
545        return retValue;
546    }   //  isOption()
547
548    /**
549     *  Checks the command line definition whether the given name is not yet
550     *  used, either as a name or an alias.
551     *
552     *  @param  name    The name to check.
553     *  @throws IllegalArgumentException    The given name is already in use.
554     */
555    private final void checkOptionNotYetUsed( final String name ) throws IllegalArgumentException
556    {
557        assert nonNull( name ) : "name is null";
558
559        if( m_OptionDefinitions.containsKey( name ) )
560        {
561            throw new IllegalArgumentException( "Option name '%s' is used more than once".formatted( name ) );
562        }
563    }   //  checkOptionNotYetUsed()
564
565    /**
566     *  Parses the given command line arguments and sets the retrieved values
567     *  to the configuration bean.
568     *
569     *  @param  args    The command line arguments to parse.
570     *  @throws CmdLineException    An error occurred while parsing the
571     *      arguments or a mandatory option or argument is missing on the
572     *      command line.
573     */
574    @SuppressWarnings( "OverlyComplexMethod" )
575    public final void parse( final String... args ) throws CmdLineException
576    {
577        final var cmdLine = new CmdLineImpl( requireNonNullArgument( args, "args" ) );
578
579        final Collection<CLIDefinition> present = new HashSet<>();
580        var argIndex = 0;
581        ParseLoop: while( cmdLine.hasNext() )
582        {
583            final var arg = cmdLine.getCurrentToken();
584            if( isOption( arg ) )
585            {
586                m_CurrentOptionDefinition = findOptionDefinition( arg );
587                present.add( m_CurrentOptionDefinition );
588
589                //---* We know the option; skip its name *---------------------
590                cmdLine.proceed( 1 );
591
592                //---* Set the value *-----------------------------------------
593                cmdLine.proceed( m_CurrentOptionDefinition.processParameters( cmdLine ) );
594            }
595            else
596            {
597                if( argIndex >= m_ArgumentDefinitions.size() )
598                {
599                    final var message = m_ArgumentDefinitions.isEmpty() ? MSG_NoArgumentAllowed : MSG_TooManyArguments;
600                    final var messageKey = m_ArgumentDefinitions.isEmpty() ? MSGKEY_NoArgumentAllowed : MSGKEY_TooManyArguments;
601                    throw new CmdLineException( message, messageKey, arg );
602                }
603
604                //---* We know the argument ... *------------------------------
605                final var currentArgumentDefinition = m_ArgumentDefinitions.get( argIndex );
606                present.add( currentArgumentDefinition );
607                if( !currentArgumentDefinition.isMultiValued() )
608                {
609                    /*
610                     * Multivalued arguments are only allowed as the last
611                     * argument, and we can have as many values for them as we
612                     * want (or the operating systems allows on the command
613                     * line).
614                     */
615                    ++argIndex;
616                }
617
618                //---* Set the value *-----------------------------------------
619                cmdLine.proceed( currentArgumentDefinition.processParameters( cmdLine ) );
620            }
621        }   //  ParseLoop:
622
623        //---* Make sure that all mandatory options are present *--------------
624        for( final var optionDefinition : m_OptionDefinitions.values() )
625        /*
626         * We can live with the fact that in case of an alias an option
627         * definition is inspected twice or even more often.
628         */
629        {
630            if( optionDefinition.required() && !present.contains( optionDefinition ) )
631            {
632                throw new CmdLineException( optionDefinition, MSG_OptionMissing, MSGKEY_OptionMissing, optionDefinition.name() );
633            }
634        }
635
636        //---* Make sure that all mandatory arguments are present *------------
637        for( final var argumentDefinition : m_ArgumentDefinitions )
638        {
639            if( argumentDefinition.required() && !present.contains( argumentDefinition ) )
640            {
641                throw new CmdLineException( argumentDefinition, MSG_ArgumentMissing, MSGKEY_ArgumentMissing, argumentDefinition.metaVar() );
642            }
643        }
644    }   //  parse()
645
646    /**
647     *  Returns {@code true} if this {@code ArgumentParser} will parse
648     *  options. This can be set to {@code false} either when no
649     *  {@link org.tquadrat.foundation.config.Option &#64;Option}
650     *  annotation was found on the configuration bean specification interface,
651     *  when the stop token (&quot;--&quot;) was encountered on the command
652     *  line, or when
653     *  {@link #stopOptionParsing()}
654     *  was called manually.
655     *
656     *  @return {@code true} when options are parsed, {@code false}
657     *      if not.
658     *
659     *  @see org.tquadrat.foundation.config.CLIBeanSpec#LEAD_IN
660     */
661    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
662    public final boolean parsingOptions() { return m_ParsingOptions; }
663
664    /**
665     *  <p>{@summary Resolves the given command line.}</p>
666     *  <p>The method is mainly meant for test and debugging purposes.</p>
667     *
668     *  @param  args    The command line arguments.
669     *  @return The resolved command line as a single String.
670     */
671    public final String resolveCommandLine( final String... args )
672    {
673        final Iterator<String> cmdLine = new CmdLineImpl( args );
674        @SuppressWarnings( "ConstantExpression" )
675        final var spliterator = spliterator( cmdLine, args.length, IMMUTABLE | NONNULL );
676        final var retValue = StreamSupport.stream( spliterator, false )
677            .map( a -> a.contains( " " ) ? format( "\"%s\"", a ) : a )
678            .collect( joining( " " ) );
679
680        //---* Done *----------------------------------------------------------
681        return retValue;
682    }   //  resolveCommandLine()
683
684    /**
685     *  Stops the parsing for options. After the call, the argument list will
686     *  be parsed only for
687     *  {@linkplain org.tquadrat.foundation.config.Argument arguments}.
688     */
689    public final void stopOptionParsing() { m_ParsingOptions = false; }
690}
691//  class ArgumentParserImpl
692
693/*
694 *  End of File
695 */