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