001/*
002 * ============================================================================
003 * Copyright © 2002-2024 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.config.spi;
019
020import org.apiguardian.api.API;
021import org.tquadrat.foundation.annotation.ClassVersion;
022import org.tquadrat.foundation.config.CmdLineException;
023import org.tquadrat.foundation.config.cli.CmdLineValueHandler;
024import org.tquadrat.foundation.i18n.Message;
025import org.tquadrat.foundation.i18n.Translation;
026
027import java.util.Optional;
028
029import static java.lang.Character.isWhitespace;
030import static java.util.Locale.ROOT;
031import static org.apiguardian.api.API.Status.STABLE;
032import static org.tquadrat.foundation.config.CLIBeanSpec.LEAD_IN;
033import static org.tquadrat.foundation.config.internal.Commons.retrieveMessage;
034import static org.tquadrat.foundation.i18n.TextUse.USAGE;
035import static org.tquadrat.foundation.lang.Objects.*;
036
037/**
038 *  Base class for the run-time copies of the
039 *  {@link org.tquadrat.foundation.config.Option @Option} or
040 *  {@link org.tquadrat.foundation.config.Argument @Argument}
041 *  annotation. By definition, unnamed options are arguments, and named options
042 *  are <i>real</i> command line options.
043 *
044 *  @see CLIOptionDefinition
045 *  @see CLIArgumentDefinition
046 *
047 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
048 *  @thanks Mark Sinke
049 *  @version $Id: CLIDefinition.java 1120 2024-03-16 09:48:00Z tquadrat $
050 *  @since 0.0.1
051 *
052 *  @UMLGraph.link
053 */
054@SuppressWarnings( {"ConstantExpression", "BooleanMethodNameMustStartWithQuestion"} )
055@ClassVersion( sourceVersion = "$Id: CLIDefinition.java 1120 2024-03-16 09:48:00Z tquadrat $" )
056@API( status = STABLE, since = "0.0.1" )
057public abstract class CLIDefinition
058{
059        /*-----------*\
060    ====** Constants **========================================================
061        \*-----------*/
062    /**
063     *  The message indicating that the empty String is not allowed as an
064     *  option name.
065     */
066    @SuppressWarnings( "SimplifiableAnnotation" )
067    @API( status = STABLE, since = "0.1.0" )
068    @Message
069    (
070        description = "The message indicating that the empty String is not allowed as an option name.",
071        translations =
072        {
073            @Translation( language = "en", text = "The empty String is not a valid option name" )
074        }
075    )
076    public static final int MSGKEY_EmptyIsInvalid = 31;
077
078    /**
079     *  The message indicating an invalid option name.
080     */
081    @SuppressWarnings( "SimplifiableAnnotation" )
082    @API( status = STABLE, since = "0.1.0" )
083    @Message
084    (
085        description = "The message indicating an invalid option name.",
086        translations =
087        {
088            @Translation( language = "en", text = "'%1$s' is not a valid option name" )
089        }
090    )
091    public static final int MSGKEY_InvalidName = 32;
092
093    /**
094     *  The message indicating that the option name is reserved.
095     */
096    @SuppressWarnings( "SimplifiableAnnotation" )
097    @API( status = STABLE, since = "0.1.0" )
098    @Message
099    (
100        description = "The message indicating that the option name is reserved.",
101        translations =
102        {
103            @Translation( language = "en", text = "'%1$s' is a reserved name" )
104        }
105    )
106    public static final int MSGKEY_ReservedName = 33;
107
108    /**
109     *  The message indicating a whitespace character as option name.
110     */
111    @SuppressWarnings( "SimplifiableAnnotation" )
112    @API( status = STABLE, since = "0.1.0" )
113    @Message
114    (
115        description = "The message indicating a whitespace character as option name.",
116        translations =
117        {
118            @Translation( language = "en", text = "Character after '%s' may not be whitespace" )
119        }
120    )
121    public static final int MSGKEY_Whitespace1 = 34;
122
123    /**
124     *  The message indicating a whitespace character as part of an option
125     *  name.
126     */
127    @SuppressWarnings( "SimplifiableAnnotation" )
128    @API( status = STABLE, since = "0.1.0" )
129    @Message
130    (
131        description = "The message indicating a whitespace character as part of an option name.",
132        translations =
133        {
134            @Translation( language = "en", text = "An option name may not contain any whitespace characters" )
135        }
136    )
137    public static final int MSGKEY_Whitespace2 = 35;
138
139    /**
140     *  The message indicating an invalid lead-in for an option name.
141     */
142    @SuppressWarnings( "SimplifiableAnnotation" )
143    @API( status = STABLE, since = "0.0.1" )
144    @Message
145    (
146        description = "The message indicating an invalid lead-in for an option name.",
147        translations =
148        {
149            @Translation( language = "en", text = "The name '%2$s' must start with '%1$s'" )
150        }
151    )
152    public static final int MSGKEY_WrongLeadIn = 36;
153
154    /**
155     *  <p>{@summary The format for the default usage keys.}</p>
156     *  <p>The resource bundle key for a
157     *  {@link org.tquadrat.foundation.i18n.TextUse#USAGE USAGE}
158     *  text is prepended with the name of the class that defines it.</p>
159     */
160    public static final String USAGE_KEY_FORMAT = String.format( "%%s.%s_%%s", USAGE.name() );
161
162        /*------------*\
163    ====** Attributes **=======================================================
164        \*------------*/
165    /**
166     *  The optional format string.
167     */
168    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
169    private final Optional<String> m_Format;
170
171    /**
172     *  The handler that is used to parse and store the option or argument
173     *  value.
174     */
175    private final CmdLineValueHandler<?> m_Handler;
176
177    /**
178     *  {@code true} if this is an argument, {@code false} if it is
179     *  an option.
180     */
181    private final boolean m_IsArgument;
182
183    /**
184     *  {@code true} if the target property is multivalued,
185     *  {@code false} otherwise.
186     */
187    private final boolean m_IsMultiValued;
188
189    /**
190     *  {@code true} if the option or argument is required,
191     *  {@code false} otherwise.
192     */
193    private final boolean m_IsRequired;
194
195    /**
196     *  The name of the meta variable that is used in examples.
197     */
198    private final String m_MetaVar;
199
200    /**
201     *  The name of the property.
202     */
203    private final String m_Property;
204
205    /**
206     *  The usage text.
207     */
208    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
209    private final Optional<String> m_Usage;
210
211    /**
212     *  The resource key for the usage text.
213     */
214    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
215    private final Optional<String> m_UsageKey;
216
217        /*--------------*\
218    ====** Constructors **=====================================================
219        \*--------------*/
220    /**
221     *  Creates a new {@code CLIDefinition} instance.
222     *
223     *  @param  property    The name of the property.
224     *  @param  isArgument  {@code true} for an argument,
225     *      {@code false} for an option.
226     *  @param  usage   The usage text.
227     *  @param  usageKey    The resource bundle key for the usage text.
228     *  @param  metaVar The meta variable name.
229     *  @param  required    {@code true} if the argument or option is
230     *      mandatory.
231     *  @param  handler The handler for the option or argument value.
232     *  @param  multiValued {@code true} if the option or argument allows
233     *      more than one value.
234     *  @param  format  The optional format.
235     */
236    @SuppressWarnings( "ConstructorWithTooManyParameters" )
237    protected CLIDefinition( final String property, final boolean isArgument, final String usage, final String usageKey, final String metaVar, final boolean required, final CmdLineValueHandler<?> handler, final boolean multiValued, final String format )
238    {
239        m_Property = requireNotEmptyArgument( property, "property" );
240        m_IsArgument = isArgument;
241        m_Usage = Optional.ofNullable( usage );
242        m_UsageKey = Optional.ofNullable( usageKey );
243        m_MetaVar = nonNull( metaVar ) ? metaVar :  property.toUpperCase( ROOT );
244        m_IsRequired = required;
245        m_Handler = requireNonNullArgument( handler, "handler" );
246        //noinspection ThisEscapedInObjectConstruction
247        m_Handler.setContext( this );
248        m_IsMultiValued = multiValued;
249        m_Format = Optional.ofNullable( format );
250    }   //  CLIDefinition()
251
252        /*---------*\
253    ====** Methods **==========================================================
254        \*---------*/
255    /**
256     *  Returns the optional format.
257     *
258     *  @return An instance of
259     *      {@link Optional}
260     *      that holds the format.
261     *
262     *  @see org.tquadrat.foundation.config.Argument#format()
263     *  @see org.tquadrat.foundation.config.Option#format()
264     */
265    public final Optional<String> format() { return m_Format; }
266
267    /**
268     *  Returns the sort key for the option or argument.
269     *
270     *  @return The sort key.
271     */
272    public abstract String getSortKey();
273
274    /**
275     *  Returns the handler.
276     *
277     *  @return The handler
278     */
279    public final CmdLineValueHandler<?> handler() { return m_Handler; }
280
281    /**
282     *  Returns a flag that indicates whether this is the definition for an
283     *  argument or an option.
284     *
285     *  @return {@code true} if this instance of {@code OptionDef} is defined
286     *      by an
287     *      {@link org.tquadrat.foundation.config.Argument &#64;Argument}
288     *      annotation, {@code false} if it is defined by an
289     *      {@link org.tquadrat.foundation.config.Option &#64;Option}
290     *      annotation.
291     */
292    public final boolean isArgument() { return m_IsArgument; }
293
294    /**
295     *  Returns a flag that indicates whether this option or argument is
296     *  multivalued.
297     *
298     *  @return {@code true} if multivalued, {@code false}
299     *      otherwise.
300     */
301    public final boolean isMultiValued() { return m_IsMultiValued; }
302
303    /**
304     *  The name of the meta variable.
305     *
306     *  @return The meta variable.
307     */
308    public final String metaVar() { return m_MetaVar; }
309
310    /**
311     *  Processes the given parameter(s).
312     *
313     *  @param  params  A reference to the command line arguments as for this
314     *      option or argument definition. This method can use this object to
315     *      access the values if necessary. The object is valid only during the
316     *      method call.
317     *  @return The number of command line arguments consumed by this method.
318     *      For example, it will return 0 if option defined by this instance
319     *      does not take any parameters.
320     *  @throws CmdLineException    Parsing the parameter(s) failed.
321     */
322    public final int processParameters( final Parameters params ) throws CmdLineException
323    {
324        final var retValue = m_Handler.parseCmdLine( params );
325
326        //---* Done *----------------------------------------------------------
327        return retValue;
328    }   //  processParameters()
329
330    /**
331     *  Returns the property name for this CLI element.
332     *
333     *  @return The property name.
334     */
335    public final String propertyName() { return m_Property; }
336
337    /**
338     *  Returns a flag indicating if the option or argument is mandatory.
339     *
340     *  @return {@code true} if the argument or option is required,
341     *      {@code false} otherwise.
342     *
343     *  @see org.tquadrat.foundation.config.Argument#required()
344     *  @see org.tquadrat.foundation.config.Option#required()
345     */
346    public final boolean required() { return m_IsRequired; }
347
348    /**
349     *  {@inheritDoc}<br>
350     */
351    @Override
352    public abstract String toString();
353
354    /**
355     *  Returns the usage text.
356     *
357     *  @return An instance of
358     *      {@link Optional}
359     *      that holds the usage text.
360     */
361    public final Optional<String> usage() { return m_Usage; }
362
363    /**
364     *  Returns the resource key for the usage text.
365     *
366     *  @return An instance of
367     *      {@link Optional}
368     *      that holds the key for the usage text.
369     */
370    public final Optional<String> usageKey() { return m_UsageKey; }
371
372    /**
373     *  Checks whether the given name for a command line option is valid.
374     *
375     *  @param  name    The intended name for the command line option.
376     *  @return {@code true} if the given name is valid, {@code false}
377     *      otherwise.
378     *  @throws IllegalArgumentException    The given name is invalid.
379     */
380    @SuppressWarnings({"StaticMethodOnlyUsedInOneClass", "ConstantValue"})
381    public static final boolean validateOptionName( final String name ) throws IllegalArgumentException
382    {
383        final var retValue = switch( requireNonNullArgument( name, "name" ).length() )
384        {
385            case 0 ->  throw new IllegalArgumentException( retrieveMessage( MSGKEY_EmptyIsInvalid, true ) );
386            case 1 ->
387            {
388                if( name.equals( LEAD_IN ) )
389                {
390                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_InvalidName, true, LEAD_IN ) );
391                }
392                throw new IllegalArgumentException( retrieveMessage( MSGKEY_WrongLeadIn, true, LEAD_IN, name ) );
393            }
394
395            case 2 ->
396            {
397                if( name.equals( LEAD_IN + LEAD_IN ) )
398                {
399                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_ReservedName, true, LEAD_IN + LEAD_IN ) );
400                }
401                else if( !name.startsWith( LEAD_IN ) )
402                {
403                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_WrongLeadIn, true, LEAD_IN, name ) );
404                }
405                else if( isWhitespace( name.charAt( 1 ) ) )
406                {
407                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_Whitespace1, true, LEAD_IN ) );
408                }
409                yield true;
410            }
411
412            default ->
413            {
414                if( !name.startsWith( LEAD_IN + LEAD_IN ) )
415                {
416                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_WrongLeadIn, true, LEAD_IN + LEAD_IN, name ) );
417                }
418                else if( name.codePoints().anyMatch( Character::isWhitespace ) )
419                {
420                    throw new IllegalArgumentException( retrieveMessage( MSGKEY_Whitespace2, true ) );
421                }
422                yield true;
423            }
424        };
425
426        //---* Done *----------------------------------------------------------
427        return retValue;
428    }   //  validateOptionName()
429}
430//  class CLIDefinition
431
432/*
433 *  End of File
434 */