View Javadoc

1   /*
2    * Copyright 2014 Theo Willows
3    *
4    * This file is part of JArgP.
5    *
6    * JArgP is free software: you can redistribute it and/or modify it under the
7    * terms of the GNU Lesser General Public License as published by the Free
8    * Software Foundation, either version 3 of the License, or (at your option) any
9    * later version.
10   *
11   * JArgP is distributed in the hope that it will be useful, but WITHOUT ANY
12   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13   * A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
14   * details.
15   *
16   * You should have received a copy of the GNU Lesser General Public License
17   * along with JArgP.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  
20  package com.munkei;
21  
22  import java.io.PrintStream;
23  import java.lang.reflect.Field;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Iterator;
27  import java.util.List;
28  
29  import com.munkei.exception.ArgumentParsingException;
30  import com.munkei.exception.NoSuchOptionException;
31  
32  /**
33   *
34   * @author Theo 'Biffen' Willows <theo@willows.se>
35   *
36   * @since 0.0.1
37   */
38  public class JArgP {
39  
40    /**
41     * Make a string (ANSI) bold.
42     *
43     * @param string The string.
44     *
45     * @return The bold <code>string</code>.
46     */
47    protected static String bold(String string) {
48      boolean ansi = true; // TODO
49      if (ansi) {
50        return "\033[1m" + string + "\033[22m";
51      }
52  
53      return string;
54    }
55  
56    /**
57     * Make a string (ANSI) underlined.
58     *
59     * @param string The string.
60     *
61     * @return The underlined <code>string</code>.
62     */
63    protected static String underline(String string) {
64      boolean ansi = true; // TODO
65      if (ansi) {
66        return "\033[4m" + string + "\033[24m";
67      }
68  
69      return string;
70    }
71  
72    private List<Option> options;
73  
74    private String usageHeader;
75  
76    /**
77     * Creates a {@link JArgP} object to use for parsing, etc.
78     *
79     * @param subject The {@link Object} that has the annotated members.
80     *
81     * @throws NullPointerException
82     */
83    public JArgP(Object subject)
84      throws NullPointerException {
85      if (subject == null) {
86        throw new NullPointerException("Subject may not be null");
87      }
88  
89      options = new ArrayList<>();
90  
91      for (Field field : subject.getClass().getFields()) {
92        if (field.isAnnotationPresent(CommandLineOption.class)) {
93          options.add(new Option(subject, field));
94        }
95      }
96    }
97  
98    /**
99     * Parses command line arguments and sets the values to the subject.
100    *
101    * @param args The command line arguments.
102    *
103    * @return A list of the remaining parameters (i.e. arguments that are not
104    * options of values of options).
105    *
106    * @throws NullPointerException If <code>args</code> is <code>null</code>.
107    *
108    * @throws NoSuchOptionException If an option is encountered that is not
109    * configured.
110    *
111    * @throws ArgumentParsingException If something goes wrong with the parsing
112    * or setting of parameters.
113    */
114   public List<String> parse(String[] args)
115     throws NullPointerException,
116            NoSuchOptionException,
117            ArgumentParsingException {
118     if (args == null) {
119       throw new NullPointerException("Arguments may not be null");
120     }
121 
122     List<String> remaining = new ArrayList<>();
123 
124     Iterator<String> it = Arrays.asList(args).iterator();
125     while (it.hasNext()) {
126       String arg = it.next();
127 
128       // Stop at "--"
129       if (arg.equals("--")) {
130         while (it.hasNext()) {
131           remaining.add(it.next());
132         }
133         break;
134       }
135 
136       // Long option
137       if (arg.startsWith("--")) {
138         set(arg.substring(2), it);
139         continue;
140       }
141 
142       // Short option(s)
143       if (arg.startsWith("-")) {
144         char[] chars = new char[arg.length() - 1];
145         arg.getChars(1, arg.length(), chars, 0);
146         for (char c : chars) {
147           set(Character.toString(c), it);
148         }
149 
150         continue;
151       }
152 
153       // This wasn't an option
154       remaining.add(arg);
155     }
156 
157     // TODO set defaults
158     return remaining;
159   }
160 
161   /**
162    * Sets the text to be shown at the beginning of the usage text.
163    *
164    * @param usageHeader The header text.
165    *
166    * @see #printUsage(java.io.PrintStream)
167    */
168   public void setUsageHeader(String usageHeader) {
169     this.usageHeader = usageHeader;
170   }
171 
172   /**
173    * Prints the usage text, based on the <code>description</code> and
174    * <code>default</code> annotation elements and the usage header.
175    *
176    * @param output The target for printing.
177    *
178    * @throws NullPointerException If <code>output</code> is <code>null</code>.
179    *
180    * @see #setUsageHeader(java.lang.String)
181    *
182    * @see #printUsage()
183    */
184   public void printUsage(PrintStream output)
185     throws NullPointerException {
186     if (output == null) {
187       throw new NullPointerException("Output stream may not be null");
188     }
189 
190     // A null usageHeader is allowed, it just means no header will be printed
191     if (usageHeader != null) {
192       output.println(usageHeader);
193       output.println();
194     }
195 
196     if (!options.isEmpty()) {
197       output.println(bold("Options"));
198       output.println();
199     }
200 
201     for (Option option : options) {
202       // Print all names, including pattern if defined
203       List<String> names = new ArrayList<>(option.getNames());
204 
205       if (option.getCommandLineOption().pattern() != null
206         && !option.getCommandLineOption().pattern().isEmpty()) {
207         names.add("/" + option.getCommandLineOption().pattern() + "/");
208       }
209 
210       for (String name : names) {
211         output.print("  ");
212         output.print(bold(((name.length() == 1) ? "-" : "--") + name));
213         if (option.takesValue()) {
214           output.print(" ");
215           output.print(
216             underline(
217               (option.getCommandLineOption().placeholder() == null
218               || option.getCommandLineOption().placeholder().isEmpty())
219               ? "VALUE"
220               : option.getCommandLineOption().placeholder()
221             )
222           );
223         }
224         output.println();
225       }
226 
227       if (option.getCommandLineOption().description() != null
228         && !option.getCommandLineOption().description().isEmpty()) {
229         output.println();
230         output.print("    ");
231         output.println(option.getCommandLineOption().description());
232       }
233 
234       if (option.getCommandLineOption().defaultValue() != null
235         && !option.getCommandLineOption().defaultValue().isEmpty()) {
236         output.println();
237         output.print("    Default: ");
238         output.println(option.getCommandLineOption().defaultValue());
239       }
240 
241       output.println();         // Empty line between options
242     }
243   }
244 
245   /**
246    * Like {@link #printUsage(Stream output)}, but prints to {@link System#out}.
247    */
248   public void printUsage() {
249     printUsage(System.out);
250   }
251 
252   private void set(String name,
253                    Iterator<String> it)
254     throws NoSuchOptionException,
255            ArgumentParsingException {
256     Option option = optionForName(name);
257     option.set(name, it);
258   }
259 
260   private Option optionForName(String name)
261     throws NoSuchOptionException {
262     for (Option option : options) {
263       if (option.hasName(name)) {
264         return option;
265       }
266     }
267 
268     throw new NoSuchOptionException(name);
269   }
270 
271 }