1 package org.djutils.cli;
2
3 import java.lang.reflect.Field;
4 import java.text.NumberFormat;
5 import java.util.Arrays;
6 import java.util.HashSet;
7 import java.util.LinkedHashMap;
8 import java.util.List;
9 import java.util.Locale;
10 import java.util.Map;
11
12 import org.djutils.reflection.ClassUtil;
13
14 import picocli.CommandLine;
15 import picocli.CommandLine.Command;
16 import picocli.CommandLine.Help;
17 import picocli.CommandLine.IHelpSectionRenderer;
18 import picocli.CommandLine.ITypeConverter;
19 import picocli.CommandLine.IVersionProvider;
20 import picocli.CommandLine.Model.ArgSpec;
21 import picocli.CommandLine.Option;
22 import picocli.CommandLine.ParseResult;
23 import picocli.CommandLine.Unmatched;
24
25 /**
26 * CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
27 *
28 * <pre>
29 * public static void main(final String[] args) throws Exception
30 * {
31 * Program program = new Program(); // initialize the Checkable class with the @Option information
32 * CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
33 * // do rest of what the main method should do
34 * }
35 * </pre>
36 *
37 * When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
38 * checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
39 * of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
40 * initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
41 * Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
42 * the real program class. The real program can then take the values of the program from the Options class. An example:
43 *
44 * <pre>
45 * public class Program
46 * {
47 * @Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
48 * public static class Options implements Checkable
49 * {
50 * @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
51 * private int port;
52 *
53 * public int getPort()
54 * {
55 * return this.port;
56 * }
57 *
58 * @Override
59 * public void check() throws Exception
60 * {
61 * if (this.port <= 0 || this.port > 65535)
62 * throw new Exception("Port should be between 1 and 65535");
63 * }
64 * }
65 *
66 * public Program()
67 * {
68 * // initialization for the program; avoid really starting things
69 * }
70 *
71 * public static void main(final String[] args)
72 * {
73 * Options options = new Options();
74 * CliUtil.execute(options, args);
75 * System.out.println("port = " + options.getPort());
76 * // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
77 * }
78 * }
79 * </pre>
80 *
81 * <br>
82 * Copyright (c) 2019-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
83 * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
84 * source code and binary code of this software is proprietary information of Delft University of Technology.
85 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
86 */
87 public final class CliUtil
88 {
89 /** Mixin class to provide a --locale option, and a default locale. */
90 public static class InitLocale
91 {
92 /** the locale. */
93 @Option(names = {"--locale"}, defaultValue = "en-US", description = "locale for variables with units.")
94 private String locale;
95 }
96
97 /** Retrieval class to provide a --locale option, and a default locale. */
98 public static class RetrieveLocale
99 {
100 /** the locale. */
101 @Option(names = {"--locale"}, defaultValue = "en-US", description = "locale for variables with units.")
102 private String locale;
103
104 /**
105 * @return the locale from --locale
106 */
107 String getLocale()
108 {
109 return this.locale;
110 }
111
112 /** The other options. */
113 @Unmatched
114 private List<String> remainder;
115 }
116
117 /** Utility class constructor. */
118 private CliUtil()
119 {
120 // Utility class
121 }
122
123 /**
124 * The map with overrides for default values and other Option and Program annotation values. values in the map are:
125 * <ul>
126 * <li>className%fieldName%propertyName for the @Option annotation for field fieldName within the class named className,
127 * and the annotation property propertyName. An example of the propertyName is "defaultValue"</li>
128 * <li>className%propertyName for the @Command annotation for the annotation property with propertyName in the named
129 * class. Examples of the propertyName are "name", "version", and "description"</li>
130 * </ul>
131 */
132 @SuppressWarnings("checkstyle:visibilitymodifier")
133 static Map<String, Object> overrideMap = new LinkedHashMap<>();
134
135 /** The set locale for the last defaultValue to be parsed. */
136 @SuppressWarnings("checkstyle:visibilitymodifier")
137 private static Locale defaultValueLocale = Locale.US;
138
139 /** The locale as set in the --locale option. */
140 @SuppressWarnings("checkstyle:visibilitymodifier")
141 private static Locale localeOption = null;
142
143 /** Whether we are parsing a default value or not. */
144 @SuppressWarnings("checkstyle:visibilitymodifier")
145 private static boolean parseDefaultValue = false;
146
147 /** Whether we are in test mode and should throw an exception instead of System.exit() with -v or -h. */
148 @SuppressWarnings("checkstyle:visibilitymodifier")
149 static boolean testMode = false;
150
151 /**
152 * Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
153 * errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
154 * further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
155 * executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
156 * information, or when the arguments are not complete or not correct.
157 * @param program the potentially checkable program with the @Option information
158 * @param args the arguments from the command line
159 */
160 public static void execute(final Object program, final String[] args)
161 {
162 execute(new CommandLine(program), args);
163 }
164
165 /**
166 * Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
167 * line, catch --help, --version, --locale, and errors. If the program implements the Checkable interface, it calls the
168 * "check" method of the class that can take care of further checks of the CLI arguments. Potentially, check() can also
169 * provide other initialization of the program to be executed, but this can better be provided by other methods in main().
170 * The method will exit on requesting help or version information, or when the arguments are not complete or not correct.
171 * @param commandLine the CommandLine object for the program with the @Option information
172 * @param args the arguments from the command line
173 */
174 @SuppressWarnings("checkstyle:methodlength")
175 public static void execute(final CommandLine commandLine, final String[] args)
176 {
177 // Issue #13. add the --locale option
178 var initLocale = new InitLocale();
179 commandLine.addMixin("locale", initLocale);
180
181 // set-up a new provider for default @Option values that can be overridden
182 CommandLine.IDefaultValueProvider vp = new CommandLine.IDefaultValueProvider()
183 {
184 @Override
185 public String defaultValue(final ArgSpec argSpec) throws Exception
186 {
187 defaultValueLocale = Locale.US; // set to default if no annotation
188 String fieldName = ((Field) argSpec.userObject()).getName();
189 Class<?> fieldClass = null;
190 try
191 {
192 Field field = ClassUtil.resolveField(commandLine.getCommand().getClass(), fieldName);
193 fieldClass = field.getDeclaringClass();
194 if (field.isAnnotationPresent(DefaultValueLocale.class))
195 {
196 var loc = field.getAnnotation(DefaultValueLocale.class).value();
197 parseDefaultValue = true; // the next parse will be an annotated default value
198 defaultValueLocale = parseLocale(loc);
199 }
200 }
201 catch (NoSuchFieldException nsfe)
202 {
203 fieldClass = commandLine.getCommand().getClass();
204 }
205 String key = CliUtil.makeOverrideKeyProperty(fieldClass, fieldName, "defaultValue");
206 if (CliUtil.overrideMap.containsKey(key))
207 {
208 return CliUtil.overrideMap.get(key).toString();
209 }
210 else
211 {
212 return argSpec.defaultValue();
213 }
214 }
215 };
216 commandLine.setDefaultValueProvider(vp);
217
218 // check @Program name override
219 String programKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "name");
220 if (overrideMap.containsKey(programKey))
221 {
222 commandLine.setCommandName(overrideMap.get(programKey).toString());
223 }
224
225 // set-up the version provider that provides a version number that can be overridden
226 String versionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "version");
227 if (overrideMap.containsKey(versionKey))
228 {
229 commandLine.getCommandSpec().versionProvider(new IVersionProvider()
230 {
231 @Override
232 public String[] getVersion() throws Exception
233 {
234 if (overrideMap.get(versionKey) instanceof String[])
235 {
236 return (String[]) overrideMap.get(versionKey);
237 }
238 return new String[] {overrideMap.get(versionKey).toString()};
239 }
240 });
241 }
242
243 // set-up the description provider that provides a description that can be overridden
244 Map<String, IHelpSectionRenderer> helpMap = commandLine.getHelpSectionMap();
245 final IHelpSectionRenderer defaultDescriptionRenderer = helpMap.get("description");
246 helpMap.put("description", new IHelpSectionRenderer()
247 {
248 @Override
249 public String render(final Help help)
250 {
251 String descriptionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "description");
252 if (overrideMap.containsKey(descriptionKey))
253 {
254 if (overrideMap.get(descriptionKey) instanceof String[])
255 {
256 StringBuilder sb = new StringBuilder();
257 for (String line : (String[]) overrideMap.get(descriptionKey))
258 {
259 sb.append(line);
260 sb.append("\n");
261 }
262 return sb.toString();
263 }
264 return overrideMap.get(descriptionKey).toString();
265 }
266 return defaultDescriptionRenderer.render(help);
267 }
268 });
269 commandLine.setHelpSectionMap(helpMap);
270
271 // register the DJUNITS converters
272 CliUnitConverters.registerAll(commandLine);
273 registerLocaleFloatConverters(commandLine);
274
275 // Issue #13. set the locale, and store the old one
276 Locale saveLocale = Locale.getDefault();
277 RetrieveLocale retrieveLocale = new RetrieveLocale();
278 CommandLine cmdLocale = new CommandLine(retrieveLocale);
279 cmdLocale.parseArgs(args);
280 localeOption =
281 new HashSet<String>(Arrays.asList(args)).contains("--locale") ? parseLocale(retrieveLocale.getLocale()) : null;
282
283 // parse the command line arguments and handle errors, now based on the set locale
284 commandLine.getCommandSpec().parser().collectErrors(true);
285 ParseResult parseResult = commandLine.parseArgs(args);
286 List<Exception> parseErrors = parseResult.errors();
287 if (parseErrors.size() > 0)
288 {
289 for (Exception e : parseErrors)
290 {
291 System.err.println(e.getMessage());
292 }
293 if (testMode)
294 {
295 throw new CliRuntimeException("parse errors");
296 }
297 System.exit(-1);
298 }
299
300 // process help and usage (using overridden values)
301 if (parseResult.isUsageHelpRequested())
302 {
303 commandLine.usage(System.out);
304 if (testMode)
305 {
306 throw new CliRuntimeException("usage help requested");
307 }
308 System.exit(0);
309 }
310 else if (parseResult.isVersionHelpRequested())
311 {
312 commandLine.printVersionHelp(System.out);
313 if (testMode)
314 {
315 throw new CliRuntimeException("version help requested");
316 }
317 System.exit(0);
318 }
319
320 // check the values for the variables
321 Object program = commandLine.getCommand();
322 if (program instanceof Checkable)
323 {
324 try
325 {
326 ((Checkable) program).check();
327 }
328 catch (Exception exception)
329 {
330 System.err.println(exception.getMessage());
331 if (testMode)
332 {
333 throw new CliRuntimeException("check error");
334 }
335 System.exit(-1);
336 }
337 }
338
339 // Issue #13. reset the locale
340 Locale.setDefault(saveLocale);
341 }
342
343 /**
344 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
345 * @param programClass the class of the program for which the options should be changed
346 * @param fieldName the field for which the defaultValue in @Option should be changed
347 * @param propertyName the name of the property to change the value of
348 * @param newValue the new value of the property
349 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
350 * @throws NoSuchFieldException when the field with the name does not exist in the program object
351 */
352 public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
353 final Object newValue) throws CliException, NoSuchFieldException
354 {
355 Field field = ClassUtil.resolveField(programClass, fieldName);
356 Option optionAnnotation = field.getAnnotation(Option.class);
357 if (optionAnnotation == null)
358 {
359 throw new CliException(
360 String.format("@Option annotation not found for field %s in class %s", fieldName, programClass.getName()));
361 }
362 String key = makeOverrideKeyProperty(field.getDeclaringClass(), fieldName, propertyName);
363 overrideMap.put(key, newValue);
364 }
365
366 /**
367 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
368 * @param program the program for which the options should be changed
369 * @param fieldName the field for which the defaultValue in @Option should be changed
370 * @param propertyName the name of the property to change the value of
371 * @param newValue the new value of the property
372 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
373 * @throws NoSuchFieldException when the field with the name does not exist in the program object
374 */
375 public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
376 final Object newValue) throws CliException, NoSuchFieldException
377 {
378 changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
379 }
380
381 /**
382 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
383 * superclass.
384 * @param program the program for which the options should be changed
385 * @param fieldName the field for which the defaultValue in @Option should be changed
386 * @param newDefaultValue the new value of the defaultValue
387 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
388 * @throws NoSuchFieldException when the field with the name does not exist in the program object
389 */
390 public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
391 throws CliException, NoSuchFieldException
392 {
393 changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
394 }
395
396 /**
397 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
398 * superclass.
399 * @param programClass the class of the program for which the options should be changed
400 * @param fieldName the field for which the defaultValue in @Option should be changed
401 * @param newDefaultValue the new value of the defaultValue
402 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
403 * @throws NoSuchFieldException when the field with the name does not exist in the program object
404 */
405 public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
406 throws CliException, NoSuchFieldException
407 {
408 changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
409 }
410
411 /**
412 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
413 * @param program the program for which the cli property should be changed
414 * @param propertyName the name of the property to change the value of
415 * @param newValue the new value of the property
416 * @throws CliException when the class is not annotated with @Command
417 */
418 private static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
419 throws CliException
420 {
421 changeCommandProperty(program.getClass(), propertyName, newValue);
422 }
423
424 /**
425 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
426 * @param programClass the class of the program for which the options should be changed
427 * @param propertyName the name of the property to change the value of
428 * @param newValue the new value of the property
429 * @throws CliException when the class is not annotated with @Command
430 */
431 private static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
432 throws CliException
433 {
434 Class<?> declaringClass = getCommandAnnotationClass(programClass);
435 String key = makeOverrideKeyCommand(declaringClass, propertyName);
436 overrideMap.put(key, newValue);
437 }
438
439 /**
440 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
441 * class.
442 * @param program the program for which the cli property should be changed
443 * @param newName the new value of the name
444 * @throws CliException when the class is not annotated with @Command
445 */
446 public static void changeCommandName(final Object program, final String newName) throws CliException
447 {
448 changeCommandProperty(program, "name", newName);
449 }
450
451 /**
452 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
453 * class.
454 * @param programClass the class of the program for which the options should be changed
455 * @param newName the new value of the name
456 * @throws CliException when the class is not annotated with @Command
457 */
458 public static void changeCommandName(final Class<?> programClass, final String newName) throws CliException
459 {
460 changeCommandProperty(programClass, "name", newName);
461 }
462
463 /**
464 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
465 * that class.
466 * @param program the program for which the cli property should be changed
467 * @param newDescription the new value of the description
468 * @throws CliException when the class is not annotated with @Command
469 */
470 public static void changeCommandDescription(final Object program, final String newDescription) throws CliException
471 {
472 changeCommandProperty(program, "description", new String[] {newDescription});
473 }
474
475 /**
476 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
477 * that class.
478 * @param programClass the class of the program for which the options should be changed
479 * @param newDescription the new value of the description
480 * @throws CliException when the class is not annotated with @Command
481 */
482 public static void changeCommandDescription(final Class<?> programClass, final String newDescription) throws CliException
483 {
484 changeCommandProperty(programClass, "description", new String[] {newDescription});
485 }
486
487 /**
488 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
489 * class.
490 * @param program the program for which the cli property should be changed
491 * @param newVersion the new value of the version
492 * @throws CliException when the class is not annotated with @Command
493 */
494 public static void changeCommandVersion(final Object program, final String newVersion) throws CliException
495 {
496 changeCommandProperty(program, "version", new String[] {newVersion});
497 }
498
499 /**
500 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
501 * class.
502 * @param programClass the class of the program for which the options should be changed
503 * @param newVersion the new value of the version
504 * @throws CliException when the class is not annotated with @Command
505 */
506 public static void changeCommandVersion(final Class<?> programClass, final String newVersion) throws CliException
507 {
508 changeCommandProperty(programClass, "version", new String[] {newVersion});
509 }
510
511 /**
512 * Return the @Command annotation of a class or one of its superclasses.
513 * @param programClass the class of the program for which the annotation should be retrieved
514 * @return the @Command annotation of the class or one of its superclasses
515 * @throws CliException when the class or one of its superclasses is not annotated with @Command
516 */
517 public static Command getCommandAnnotation(final Class<?> programClass) throws CliException
518 {
519 return getCommandAnnotationClass(programClass).getDeclaredAnnotation(Command.class);
520 }
521
522 /**
523 * Return the @Command annotation of a class or one of its superclasses.
524 * @param programClass the class of the program for which the annotation should be retrieved
525 * @return the class or superclass in which the @Command annotation was found
526 * @throws CliException when the class or one of its superclasses is not annotated with @Command
527 */
528 public static Class<?> getCommandAnnotationClass(final Class<?> programClass) throws CliException
529 {
530 Class<?> clazz = programClass;
531 while (clazz != null)
532 {
533 Command commandAnnotation = clazz.getDeclaredAnnotation(Command.class);
534 if (commandAnnotation != null)
535 {
536 return clazz;
537 }
538 clazz = clazz.getSuperclass();
539 }
540 throw new CliException(
541 String.format("@Command annotation not found for class %s or one of its superclasses", programClass.getName()));
542 }
543
544 /**
545 * @param programClass the class for which to retrieve the version. The class should be annotated with @Command
546 * @return String[] the version string
547 * @throws CliException when the class is not annotated with @Command
548 */
549 public static String[] getCommandVersion(final Class<?> programClass) throws CliException
550 {
551 String versionKey = makeOverrideKeyCommand(programClass, "version");
552 if (overrideMap.containsKey(versionKey))
553 {
554 Object version = overrideMap.get(versionKey);
555 if (version instanceof String[])
556 {
557 return (String[]) version;
558 }
559 return new String[] {version.toString()};
560 }
561 return getCommandAnnotation(programClass).version();
562 }
563
564 /**
565 * @param program the program for which to retrieve the version. The program's class should be annotated with @Command
566 * @return String[] the version string
567 * @throws CliException when the class is not annotated with @Command
568 */
569 public static String[] getCommandVersion(final Object program) throws CliException
570 {
571 return getCommandVersion(program.getClass());
572 }
573
574 /**
575 * @param programClass the class for which to retrieve the program name. The class should be annotated with @Command
576 * @return String the name string
577 * @throws CliException when the class is not annotated with @Command
578 */
579 public static String getCommandName(final Class<?> programClass) throws CliException
580 {
581 String nameKey = makeOverrideKeyCommand(programClass, "name");
582 if (overrideMap.containsKey(nameKey))
583 {
584 return overrideMap.get(nameKey).toString();
585 }
586 return getCommandAnnotation(programClass).name();
587 }
588
589 /**
590 * @param program the program for which to retrieve the program name. The program's class should be annotated with
591 * @Command
592 * @return String the name string
593 * @throws CliException when the class is not annotated with @Command
594 */
595 public static String getCommandName(final Object program) throws CliException
596 {
597 return getCommandName(program.getClass());
598 }
599
600 /**
601 * @param programClass the class for which to retrieve the description. The class should be annotated with @Command
602 * @return String[] the description string
603 * @throws CliException when the class is not annotated with @Command
604 */
605 public static String[] getCommandDescription(final Class<?> programClass) throws CliException
606 {
607 String descriptionKey = makeOverrideKeyCommand(programClass, "description");
608 if (overrideMap.containsKey(descriptionKey))
609 {
610 Object description = overrideMap.get(descriptionKey);
611 if (description instanceof String[])
612 {
613 return (String[]) description;
614 }
615 return new String[] {description.toString()};
616 }
617 return getCommandAnnotation(programClass).description();
618 }
619
620 /**
621 * @param program the program for which to retrieve the description. The program's class should be annotated with
622 * @Command
623 * @return String[] the description string
624 * @throws CliException when the class is not annotated with @Command
625 */
626 public static String[] getCommandDescription(final Object program) throws CliException
627 {
628 return getCommandDescription(program.getClass());
629 }
630
631 /**
632 * Make the override key for an option property.
633 * @param programClass the class of the program for which the options should be changed
634 * @param fieldName the field for which the defaultValue in @Option should be changed
635 * @param propertyName the name of the property to change the value of
636 * @return the override key for an option property
637 */
638 static String makeOverrideKeyProperty(final Class<?> programClass, final String fieldName, final String propertyName)
639 {
640 return programClass.getName() + "%" + fieldName + "%" + propertyName;
641 }
642
643 /**
644 * Make the override key for the Command annotation.
645 * @param programClass the class of the program for which the options should be changed
646 * @param propertyName the name of the annotation property to change the value of
647 * @return the override key for an option property
648 */
649 static String makeOverrideKeyCommand(final Class<?> programClass, final String propertyName)
650 {
651 return programClass.getName() + "%" + propertyName;
652 }
653
654 /**
655 * Register the locale-dependent converters for double, Double, float, Float.
656 * @param cmd the CommandLine for which the DJUNITS converters should be registered
657 */
658 static void registerLocaleFloatConverters(final CommandLine cmd)
659 {
660 cmd.registerConverter(Double.class, new DoubleConverter());
661 cmd.registerConverter(double.class, new DoubleConverter());
662 cmd.registerConverter(Float.class, new FloatConverter());
663 cmd.registerConverter(float.class, new FloatConverter());
664 }
665
666 /**
667 * Convert a Double String on the command line to a Double, taking into account the locale.
668 */
669 public static class DoubleConverter implements ITypeConverter<Double>
670 {
671 @Override
672 public Double convert(final String value) throws Exception
673 {
674 prepareLocale();
675 NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
676 Number number;
677 double ret;
678 try
679 {
680 number = format.parse(value);
681 ret = number.doubleValue();
682 }
683 catch (Exception e)
684 {
685 System.err.println("ERROR parsing double value " + value + ": " + e.getMessage());
686 ret = Double.NaN;
687 }
688 restoreLocale();
689 return ret;
690 }
691 }
692
693 /**
694 * Convert a Double String on the command line to a Double, taking into account the locale.
695 */
696 public static class FloatConverter implements ITypeConverter<Float>
697 {
698 @Override
699 public Float convert(final String value) throws Exception
700 {
701 prepareLocale();
702 NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
703 Number number;
704 float ret;
705 try
706 {
707 number = format.parse(value);
708 ret = number.floatValue();
709 }
710 catch (Exception e)
711 {
712 System.err.println("ERROR parsing double value " + value + ": " + e.getMessage());
713 ret = Float.NaN;
714 }
715 restoreLocale();
716 return ret;
717 }
718 }
719
720 /** temporarily save the locale. */
721 private static Locale saveLocaleForDefault;
722
723 /**
724 * Prepare the setting of a Locale for a default value or a value.
725 */
726 static void prepareLocale()
727 {
728 saveLocaleForDefault = Locale.getDefault();
729 Locale.setDefault(
730 CliUtil.localeOption != null && !CliUtil.parseDefaultValue ? CliUtil.localeOption : CliUtil.defaultValueLocale);
731 }
732
733 /**
734 * Restore the setting of a Locale for a default value or a value.
735 */
736 static void restoreLocale()
737 {
738 Locale.setDefault(saveLocaleForDefault);
739 CliUtil.defaultValueLocale = Locale.US;
740 CliUtil.parseDefaultValue = false;
741 }
742
743 /**
744 * Parse a locale string like "en-US" or "en_GB" or "nl" or "nl_NL".
745 * @param localeStr the string to parse
746 * @return the Locale belonging to the string
747 */
748 private static Locale parseLocale(final String localeStr)
749 {
750 var s = localeStr.replaceAll("_", "-");
751 var sa = s.split("\\-", 3);
752 return sa.length == 3 ? new Locale(sa[0], sa[1], sa[2])
753 : sa.length == 2 ? new Locale(sa[0], sa[1]) : sa.length == 1 ? new Locale(sa[0]) : new Locale(s);
754 }
755
756 }