001/*
002 * ============================================================================
003 * Copyright © 2002-2025 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 static java.lang.String.format;
022import static java.util.stream.Collectors.joining;
023import static org.apiguardian.api.API.Status.INTERNAL;
024import static org.tquadrat.foundation.config.internal.Commons.retrieveText;
025import static org.tquadrat.foundation.i18n.I18nUtil.composeTextKey;
026import static org.tquadrat.foundation.i18n.I18nUtil.resolveText;
027import static org.tquadrat.foundation.i18n.TextUse.TXT;
028import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_Object_ARRAY;
029import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
030import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
031import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
032import static org.tquadrat.foundation.util.StringUtils.breakText;
033import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
034
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.List;
038import java.util.Map;
039import java.util.Optional;
040import java.util.ResourceBundle;
041import java.util.TreeMap;
042import java.util.stream.IntStream;
043import java.util.stream.Stream;
044import java.util.stream.Stream.Builder;
045
046import org.apiguardian.api.API;
047import org.tquadrat.foundation.annotation.ClassVersion;
048import org.tquadrat.foundation.annotation.NotRecord;
049import org.tquadrat.foundation.config.spi.CLIArgumentDefinition;
050import org.tquadrat.foundation.config.spi.CLIDefinition;
051import org.tquadrat.foundation.config.spi.CLIOptionDefinition;
052import org.tquadrat.foundation.i18n.Text;
053import org.tquadrat.foundation.i18n.Translation;
054
055/**
056 *  Builds the <i>usage</i> message that will be printed to the console (or
057 *  wherever) in case help is requested on the command line, or an invalid
058 *  option or argument is provided on it.
059 *
060 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
061 *  @version $Id: UsageBuilder.java 1151 2025-10-01 21:32:15Z tquadrat $
062 *  @since 0.0.1
063 *
064 *  @UMLGraph.link
065 */
066@ClassVersion( sourceVersion = "$Id: UsageBuilder.java 1151 2025-10-01 21:32:15Z tquadrat $" )
067@API( status = INTERNAL, since = "0.0.1" )
068@NotRecord
069public class UsageBuilder
070{
071        /*-----------*\
072    ====** Constants **========================================================
073        \*-----------*/
074    /**
075     *  The maximum line length: {@value}.
076     */
077    public static final int MAX_LINE_LENGTH = 80;
078
079    /**
080     *  The Text 'Arguments'.
081     */
082    @Text
083    (
084        description = "The text that introduces the 'Arguments' section in the usage message.",
085        use = TXT,
086        id = "Arguments",
087        translations =
088        {
089            @Translation( language = "en", text = "Arguments" ),
090            @Translation( language = "de", text = "Argumente" )
091        }
092    )
093    public static final String TXT_Arguments = composeTextKey( UsageBuilder.class, TXT, "Arguments" );
094
095    /**
096     *  The text 'Options'.
097     */
098    @Text
099    (
100        description = "The text that introduces the 'Options' section in the usage message.",
101        use = TXT,
102        id = "Options",
103        translations =
104        {
105            @Translation( language = "en", text = "Options" ),
106            @Translation( language = "de", text = "Optionen" )
107        }
108    )
109    public static final String TXT_Options = composeTextKey( UsageBuilder.class, TXT, "Options" );
110
111    /**
112     *  The text 'Usage: ' that introduces the <i>usage</i> text.
113     */
114    @Text
115    (
116        description = "The text that introduces the usage message.",
117        use = TXT,
118        id = "Usage",
119        translations =
120        {
121            @Translation( language = "en", text = "Usage: " ),
122            @Translation( language = "de", text = "Verwendung: " )
123        }
124    )
125    public static final String TXT_Usage = composeTextKey( UsageBuilder.class, TXT, "Usage" );
126
127        /*------------*\
128    ====** Attributes **=======================================================
129        \*------------*/
130    /**
131     *  The resource bundle that is used to translate the descriptions of
132     *  options and arguments.
133     */
134    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
135    private final Optional<ResourceBundle> m_CallerResourceBundle;
136
137        /*--------------*\
138    ====** Constructors **=====================================================
139        \*--------------*/
140    /**
141     *  Creates a new {@code UsageBuilder} instance.
142     *
143     *  @param  callerResourceBundle   The resource bundle that is used to
144     *      translate the descriptions of options and arguments.
145     */
146    public UsageBuilder( final ResourceBundle callerResourceBundle )
147    {
148        this( Optional.ofNullable( callerResourceBundle ) );
149    }   //  UsageBuilder()
150
151    /**
152     *  Creates a new {@code UsageBuilder} instance.
153     *
154     *  @param  callerResourceBundle   The resource bundle that is used to
155     *      translate the descriptions of options and arguments.
156     */
157    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
158    public UsageBuilder( final Optional<ResourceBundle> callerResourceBundle )
159    {
160        m_CallerResourceBundle = requireNonNullArgument( callerResourceBundle, "resources" );
161    }   //  UsageBuilder()
162
163        /*---------*\
164    ====** Methods **==========================================================
165        \*---------*/
166    /**
167     *  Adds the arguments to the output.
168     *
169     *  @param  buffer  The buffer for the result.
170     *  @param  arguments   The arguments.
171     */
172    private void addArguments( final StringBuilder buffer, final Map<String, ? extends CLIArgumentDefinition> arguments )
173    {
174        if( !arguments.isEmpty() )
175        {
176            buffer.append( '\n' )
177                .append( retrieveText( TXT_Arguments ) )
178                .append( ':' );
179            @SuppressWarnings( "OptionalGetWithoutIsPresent" )
180            final var widthLeft = arguments.values().stream()
181                .map( CLIDefinition::metaVar )
182                .mapToInt( String::length )
183                .max()
184                .getAsInt() + 2;
185            final var widthRight = MAX_LINE_LENGTH - widthLeft;
186
187            final List<String> leftLines = new ArrayList<>();
188            final List<String> rightLines = new ArrayList<>();
189
190            for( final var argument : arguments.values() )
191            {
192                leftLines.add( argument.metaVar() );
193
194                final var usageText = resolveMessage( m_CallerResourceBundle, argument );
195                rightLines.addAll( breakText( usageText, widthRight )
196                    .toList() );
197
198                padLines( leftLines, widthLeft, rightLines, widthRight );
199
200                buffer.append( IntStream.range( 0, leftLines.size() )
201                    .mapToObj( i -> leftLines.get( i ) + rightLines.get( i ) )
202                    .collect( joining( "\n", "\n", EMPTY_STRING ) ) );
203                leftLines.clear();
204                rightLines.clear();
205            }
206            buffer.append( '\n' );
207        }
208    }   //  addArguments()
209
210    /**
211     *  Adds the options to the output.
212     *
213     *  @param  buffer  The buffer for the result.
214     *  @param  options The options.
215     */
216    private void addOptions( final StringBuilder buffer, final Map<String, ? extends CLIOptionDefinition> options )
217    {
218        if( !options.isEmpty() )
219        {
220            buffer.append( "\n\n" )
221                .append( retrieveText( TXT_Options ) )
222                .append( ':' );
223            @SuppressWarnings( "OptionalGetWithoutIsPresent" )
224            final var widthLeft = options.values().stream()
225                .flatMap( definition ->
226                {
227                    final Builder<String> builder = Stream.builder();
228                    builder.add( format( "%s %s", definition.name(), definition.metaVar() ) );
229                    for( final var a : definition.aliases() )
230                    {
231                        builder.add( format( "%s %s", a, definition.metaVar() ) );
232                    }
233                    return builder.build();
234                } )
235                .mapToInt( String::length )
236                .max()
237                .getAsInt() + 2;
238            final var widthRight = MAX_LINE_LENGTH - widthLeft;
239
240            final List<String> leftLines = new ArrayList<>();
241            final List<String> rightLines = new ArrayList<>();
242
243            for( final var option : options.values() )
244            {
245                leftLines.add( format( "%s %s", option.name(), option.metaVar() ) );
246                for( final var a : option.aliases() )
247                {
248                    leftLines.add( format( "%s %s", a, option.metaVar() ) );
249                }
250
251                final var usageText = resolveMessage( m_CallerResourceBundle, option );
252                rightLines.addAll( breakText( usageText, widthRight )
253                    .toList() );
254
255                padLines( leftLines, widthLeft, rightLines, widthRight );
256
257                buffer.append( IntStream.range( 0, leftLines.size() )
258                    .mapToObj( i -> leftLines.get( i ) + rightLines.get( i ) )
259                    .collect( joining( "\n", "\n", EMPTY_STRING ) ) );
260                leftLines.clear();
261                rightLines.clear();
262            }
263            buffer.append( '\n' );
264        }
265    }   //  addOptions()
266
267    /**
268     *  Builds the <i>usage</i> text.
269     *
270     *  @param  command The command string.
271     *  @param  definitions The CLI definitions.
272     *  @return The usage text.
273     */
274    public final String build( final CharSequence command, final Collection<? extends CLIDefinition> definitions )
275    {
276        final Map<String,CLIArgumentDefinition> arguments = new TreeMap<>();
277        final Map<String,CLIOptionDefinition> options = new TreeMap<>();
278
279        requireNonNullArgument( definitions, "definitions" ).forEach( definition ->
280        {
281            if( definition.isArgument() )
282            {
283                arguments.put( definition.getSortKey(), (CLIArgumentDefinition) definition );
284            }
285            else
286            {
287                options.put( definition.getSortKey(), (CLIOptionDefinition) definition );
288            }
289        } );
290
291        final var buffer = new StringBuilder( composeCommandLine( requireNotEmptyArgument( command, "command" ), options, arguments ) );
292        addOptions( buffer, options );
293        addArguments( buffer, arguments );
294
295        final var retValue = buffer.toString();
296
297        //---* Done *----------------------------------------------------------
298        return retValue;
299    }   //  build()
300
301    /**
302     *  Composes the sample command line.
303     *
304     *  @param  command The command.
305     *  @param  options The options.
306     *  @param  arguments   The arguments.
307     *  @return The command line.
308     */
309    private final String composeCommandLine( final CharSequence command, final Map<String, ? extends CLIOptionDefinition> options, final Map<String, ? extends CLIArgumentDefinition> arguments )
310    {
311        final var buffer = new StringBuilder( retrieveText( TXT_Usage ) ).append( command );
312        for( final var definition : options.values() )
313        {
314            buffer.append( ' ' );
315            if( !definition.required() ) buffer.append( '[' );
316            buffer.append( definition.name() );
317            if( isNotEmptyOrBlank( definition.metaVar() ) )
318            {
319                buffer.append( '\u202f' )
320                    .append( definition.metaVar() );
321            }
322            if( !definition.required() ) buffer.append( ']' );
323        }
324        for( final var definition : arguments.values() )
325        {
326            buffer.append( ' ' );
327            if( !definition.required() ) buffer.append( '[' );
328            buffer.append( definition.metaVar() );
329            if( !definition.required() ) buffer.append( ']' );
330        }
331
332        final var retValue = breakText( buffer, MAX_LINE_LENGTH ).collect( joining( "\n" ) );
333
334        //---* Done *----------------------------------------------------------
335        return retValue;
336    }   //  composeCommandLine()
337
338    /**
339     *  Ensures that the left and the right part have the same number of
340     *  entries and that the lines have the same length.
341     *
342     *  @param  leftLines   The lines on the left side.
343     *  @param  widthLeft   The length for the lines on the left side.
344     *  @param  rightLines  The lines on the right side.
345     *  @param  widthRight  The length for the lines on the right side.
346     */
347    private static final void padLines( final List<String> leftLines, final int widthLeft, final Collection<? super String> rightLines, @SuppressWarnings( "unused" ) final int widthRight )
348    {
349        switch( Integer.signum( leftLines.size() - rightLines.size() ) )
350        {
351            case -1:
352                while( leftLines.size() < rightLines.size() )
353                {
354                    leftLines.add( EMPTY_STRING );
355                }
356                break;
357
358            case 0: break;
359
360            case 1:
361                while( rightLines.size() < leftLines.size() )
362                {
363                    rightLines.add( EMPTY_STRING );
364                }
365                break;
366
367            default: break; // Cannot happen!!
368        }
369
370        final List<String> list = new ArrayList<>( leftLines );
371        leftLines.clear();
372        final var buffer = new StringBuilder( list.getFirst() );
373        while( buffer.length() < widthLeft - 2 ) buffer.append( ' ' );
374        buffer.append( ": " );
375        leftLines.add( buffer.toString() );
376
377        for( var i = 1; i < list.size(); ++i )
378        {
379            buffer.setLength( 0 );
380            buffer.append( list.get( i ) );
381            while( buffer.length() < widthLeft ) buffer.append( ' ' );
382            leftLines.add( buffer.toString() );
383        }
384    }   //  padLines()
385
386    /**
387     *  Returns the message from the given
388     *  {@link CLIDefinition}
389     *
390     *  @param  bundle  The resource bundle.
391     *  @param  definition  The {@code CLIDefinition}.
392     *  @return The resolved message.
393     */
394    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
395    public final String resolveMessage( final Optional<ResourceBundle> bundle, final CLIDefinition definition )
396    {
397        final var retValue = resolveText( bundle, requireNonNullArgument( definition, "definition" ).usage(), definition.usageKey(), EMPTY_Object_ARRAY );
398
399        //---* Done *----------------------------------------------------------
400        return retValue;
401    }   //  resolveMessage()
402}
403//  class UsageBuilder
404
405/*
406 *  End of File
407 */