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