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 */