View Javadoc
1   package org.djutils.cli;
2   
3   import java.lang.annotation.Annotation;
4   import java.lang.reflect.Field;
5   import java.util.List;
6   
7   import org.djutils.reflection.ClassUtil;
8   
9   import picocli.CommandLine;
10  import picocli.CommandLine.Command;
11  import picocli.CommandLine.Option;
12  import picocli.CommandLine.ParseResult;
13  
14  /**
15   * CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
16   * 
17   * <pre>
18   * public static void main(final String[] args) throws Exception
19   * {
20   *     Program program = new Program(); // initialize the Checkable class with the &#64;Option information
21   *     CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
22   *     // do rest of what the main method should do
23   * }
24   * </pre>
25   * 
26   * When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
27   * checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
28   * of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
29   * initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
30   * Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
31   * the real program class. The real program can then take the values of the program from the Options class. An example:
32   * 
33   * <pre>
34   * public class Program
35   * {
36   *     &#64;Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
37   *     public static class Options implements Checkable
38   *     {
39   *         &#64;Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
40   *         private int port;
41   * 
42   *         public int getPort()
43   *         {
44   *             return this.port;
45   *         }
46   * 
47   *         &#64;Override
48   *         public void check() throws Exception
49   *         {
50   *             if (this.port &lt;= 0 || this.port &gt; 65535)
51   *                 throw new Exception("Port should be between 1 and 65535");
52   *         }
53   *     }
54   * 
55   *     public Program()
56   *     {
57   *         // initialization for the program; avoid really starting things
58   *     }
59   * 
60   *     public static void main(final String[] args)
61   *     {
62   *         Options options = new Options();
63   *         CliUtil.execute(options, args);
64   *         System.out.println("port = " + options.getPort());
65   *         // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
66   *     }
67   * }
68   * </pre>
69   * 
70   * <br>
71   * Copyright (c) 2019-2019 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
72   * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
73   * source code and binary code of this software is proprietary information of Delft University of Technology.
74   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
75   */
76  public class CliUtil
77  {
78      /**
79       * Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
80       * errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
81       * further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
82       * executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
83       * information, or when the arguments are not complete or not correct.
84       * @param program Object; the potentially checkable program with the &#64;Option information
85       * @param args String[]; the arguments from the command line
86       */
87      public static void execute(final Object program, final String[] args)
88      {
89          execute(new CommandLine(program), args);
90      }
91  
92      /**
93       * Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
94       * line, catch --help, --version and errors. If the program implements the Checkable interface, it calls the "check" method
95       * of the class that can take care of further checks of the CLI arguments. Potentially, check() can also provide other
96       * initialization of the program to be executed, but this can better be provided by other methods in main(). The method will
97       * exit on requesting help or version information, or when the arguments are not complete or not correct.
98       * @param commandLine CommandLine; the CommandLine object for the program with the &#64;Option information
99       * @param args String[]; the arguments from the command line
100      */
101     public static void execute(final CommandLine commandLine, final String[] args)
102     {
103         CliUnitConverters.registerAll(commandLine);
104         commandLine.getCommandSpec().parser().collectErrors(true);
105         ParseResult parseResult = commandLine.parseArgs(args);
106         List<Exception> parseErrors = parseResult.errors();
107         if (parseErrors.size() > 0)
108         {
109             for (Exception e : parseErrors)
110             {
111                 System.err.println(e.getMessage());
112             }
113             System.exit(-1);
114         }
115         if (parseResult.isUsageHelpRequested())
116         {
117             commandLine.usage(System.out);
118             System.exit(0);
119         }
120         else if (parseResult.isVersionHelpRequested())
121         {
122             commandLine.printVersionHelp(System.out);
123             System.exit(0);
124         }
125         Object program = commandLine.getCommand();
126         if (program instanceof Checkable)
127         {
128             try
129             {
130                 ((Checkable) program).check();
131             }
132             catch (Exception exception)
133             {
134                 System.err.println(exception.getMessage());
135                 System.exit(-1);
136             }
137         }
138     }
139 
140     /**
141      * Change the value of a property of an already present &#64;Option annotation of a field in a class or superclass.
142      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
143      * @param fieldName String; the field for which the defaultValue in &#64;Option should be changed
144      * @param propertyName String; the name of the property to change the value of
145      * @param newValue String; the new value of the property
146      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
147      * @throws NoSuchFieldException when the field with the name does not exist in the program object
148      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
149      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
150      */
151     public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
152             final Object newValue) throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
153     {
154         Field field = ClassUtil.resolveField(programClass, fieldName);
155         Option option = field.getAnnotation(Option.class);
156         ClassUtil.changeAnnotationValue(option, propertyName, newValue);
157     }
158 
159     /**
160      * Change the value of a property of an already present &#64;Option annotation of a field in a class or superclass.
161      * @param program Object; the program for which the options should be changed
162      * @param fieldName String; the field for which the defaultValue in &#64;Option should be changed
163      * @param propertyName String; the name of the property to change the value of
164      * @param newValue String; the new value of the property
165      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
166      * @throws NoSuchFieldException when the field with the name does not exist in the program object
167      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
168      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
169      */
170     public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
171             final Object newValue) throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
172     {
173         changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
174     }
175 
176     /**
177      * Change the default value of an already present &#64;Option annotation of the "defaultValue" field in a class or
178      * superclass.
179      * @param program Object; the program for which the options should be changed
180      * @param fieldName String; the field for which the defaultValue in &#64;Option should be changed
181      * @param newDefaultValue Object; the new value of the defaultValue
182      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
183      * @throws NoSuchFieldException when the field with the name does not exist in the program object
184      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
185      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
186      */
187     public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
188             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
189     {
190         changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
191     }
192 
193     /**
194      * Change the default value of an already present &#64;Option annotation of the "defaultValue" field in a class or
195      * superclass.
196      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
197      * @param fieldName String; the field for which the defaultValue in &#64;Option should be changed
198      * @param newDefaultValue Object; the new value of the defaultValue
199      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
200      * @throws NoSuchFieldException when the field with the name does not exist in the program object
201      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
202      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
203      */
204     public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
205             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
206     {
207         changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
208     }
209 
210     /**
211      * Change the value of a property of an already present &#64;Command annotation in a class or superclass of that class.
212      * @param program Object; the program for which the cli property should be changed
213      * @param propertyName String; the name of the property to change the value of
214      * @param newValue Object; the new value of the property
215      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
216      * @throws NoSuchFieldException when the field with the name does not exist in the program object
217      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
218      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
219      */
220     public static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
221             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
222     {
223         Annotation annotation = ClassUtil.resolveAnnotation(program.getClass(), Command.class);
224         ClassUtil.changeAnnotationValue(annotation, propertyName, newValue);
225     }
226 
227     /**
228      * Change the value of a property of an already present &#64;Command annotation in a class or superclass of that class.
229      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
230      * @param propertyName String; the name of the property to change the value of
231      * @param newValue Object; the new value of the property
232      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
233      * @throws NoSuchFieldException when the field with the name does not exist in the program object
234      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
235      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
236      */
237     public static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
238             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
239     {
240         Annotation annotation = ClassUtil.resolveAnnotation(programClass, Command.class);
241         ClassUtil.changeAnnotationValue(annotation, propertyName, newValue);
242     }
243 
244     /**
245      * Change the value of the 'name' property of an already present &#64;Command annotation in a class or superclass of that
246      * class.
247      * @param program Object; the program for which the cli property should be changed
248      * @param newName String; the new value of the name
249      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
250      * @throws NoSuchFieldException when the field with the name does not exist in the program object
251      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
252      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
253      */
254     public static void changeCommandName(final Object program, final String newName)
255             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
256     {
257         changeCommandProperty(program, "name", newName);
258     }
259 
260     /**
261      * Change the value of the 'name' property of an already present &#64;Command annotation in a class or superclass of that
262      * class.
263      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
264      * @param newName String; the new value of the name
265      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
266      * @throws NoSuchFieldException when the field with the name does not exist in the program object
267      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
268      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
269      */
270     public static void changeCommandName(final Class<?> programClass, final String newName)
271             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
272     {
273         changeCommandProperty(programClass, "name", newName);
274     }
275 
276     /**
277      * Change the value of the 'description' property of an already present &#64;Command annotation in a class or superclass of
278      * that class.
279      * @param program Object; the program for which the cli property should be changed
280      * @param newDescription String; the new value of the description
281      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
282      * @throws NoSuchFieldException when the field with the name does not exist in the program object
283      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
284      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
285      */
286     public static void changeCommandDescription(final Object program, final String newDescription)
287             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
288     {
289         changeCommandProperty(program, "description", new String[] {newDescription});
290     }
291 
292     /**
293      * Change the value of the 'description' property of an already present &#64;Command annotation in a class or superclass of
294      * that class.
295      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
296      * @param newDescription String; the new value of the description
297      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
298      * @throws NoSuchFieldException when the field with the name does not exist in the program object
299      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
300      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
301      */
302     public static void changeCommandDescription(final Class<?> programClass, final String newDescription)
303             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
304     {
305         changeCommandProperty(programClass, "description", new String[] {newDescription});
306     }
307 
308     /**
309      * Change the value of the 'version' property of an already present &#64;Command annotation in a class or superclass of that
310      * class.
311      * @param program Object; the program for which the cli property should be changed
312      * @param newVersion String; the new value of the version
313      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
314      * @throws NoSuchFieldException when the field with the name does not exist in the program object
315      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
316      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
317      */
318     public static void changeCommandVersion(final Object program, final String newVersion)
319             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
320     {
321         changeCommandProperty(program, "version", new String[] {newVersion});
322     }
323 
324     /**
325      * Change the value of the 'version' property of an already present &#64;Command annotation in a class or superclass of that
326      * class.
327      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
328      * @param newVersion String; the new value of the version
329      * @throws CliException when the field cannot be found, or when the &#64;Option annotation is not present in the field
330      * @throws NoSuchFieldException when the field with the name does not exist in the program object
331      * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
332      * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
333      */
334     public static void changeCommandVersion(final Class<?> programClass, final String newVersion)
335             throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
336     {
337         changeCommandProperty(programClass, "version", new String[] {newVersion});
338     }
339 
340 }