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 com.munkei.exception.ArgumentParsingException;
23  import java.lang.reflect.Constructor;
24  import java.lang.reflect.Field;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.lang.reflect.ParameterizedType;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.Iterator;
33  import java.util.List;
34  
35  /**
36   *
37   * @author Theo 'Biffen' Willows <theo@willows.se>
38   *
39   * @since 0.0.1
40   */
41  public class Option {
42  
43    private Object subject;
44  
45    private Field field;
46  
47    private List<String> names;
48  
49    Option(Object subject, Field field) {
50      if (subject == null) {
51        throw new NullPointerException("Subject may not be null");
52      }
53      if (field == null) {
54        throw new NullPointerException("Field may not be null");
55      }
56      if (!field.isAnnotationPresent(CommandLineOption.class)) {
57        throw new IllegalArgumentException(
58          "Field ''{}'' has not got an @Option annotation.");
59      }
60  
61      this.subject = subject;
62      this.field = field;
63    }
64  
65    void set(String name,
66             Iterator<String> it)
67      throws ArgumentParsingException {
68      Object value = ((takesValue())
69                      ? convert(it.next(), getEffectiveClass())
70                      : true);
71      String setterName = getCommandLineOption().setter();
72  
73      // Setter method
74      if (getCommandLineOption().setter() != null
75        && !getCommandLineOption().setter().isEmpty()) {
76        Method setter;
77        try {
78          try {
79            setter = subject.getClass().getMethod(setterName,
80                                                  String.class,
81                                                  getField().getType());
82          } catch (NoSuchMethodException |
83                   SecurityException ex) {
84            // Just try with single-parameter method
85            setter = subject.getClass().getMethod(setterName,
86                                                  getField().getType());
87          }
88          setter.invoke(subject, value);
89        } catch (NoSuchMethodException |
90                 SecurityException ex) {
91          throw new ArgumentParsingException(
92            ex,
93            "Could not get setter (''{0}(java.lang.String, {1})'') for field ''{2}''.",
94            setterName,
95            getField().getType().getName(),
96            getField().getName());
97        } catch (IllegalAccessException |
98                 InvocationTargetException ex) {
99          throw new ArgumentParsingException(
100           ex,
101           "Failed to invoke setter (''{0}(java.lang.String, {1})'') for field ''{2}'' with value ''{3}''",
102           setterName,
103           getField().getType().getName(),
104           getField().getName(),
105           value);
106       }
107       return;
108     }
109 
110     // Direct
111     try {
112       boolean accessible = getField().isAccessible();
113       getField().setAccessible(true);
114       if (isCollection()) {
115         Collection collection;
116         if (getField().get(subject) == null) {
117           try {
118             Constructor<?> constructor = getField().getType().getConstructor();
119             collection = (Collection) constructor.newInstance();
120             getField().set(subject, collection);
121           } catch (NoSuchMethodException |
122                    SecurityException |
123                    InstantiationException |
124                    IllegalAccessException |
125                    IllegalArgumentException |
126                    InvocationTargetException ex) {
127             throw new ArgumentParsingException(
128               ex,
129               "Could not create a collection for field ''{0}''.",
130               getField().getName());
131           }
132         } else {
133           collection = (Collection) getField().get(subject);
134         }
135         collection.add(value);
136       } else {
137         getField().set(subject, value);
138       }
139       getField().setAccessible(accessible);
140     } catch (IllegalArgumentException |
141              IllegalAccessException ex) {
142       throw new ArgumentParsingException(
143         ex,
144         "Could not directly set field ''{1}''.",
145         getField().getName());
146     }
147   }
148 
149   /**
150    * Helper method to convert command line options' values ({@link String}s) to
151    * objects of other classes.
152    * <p>
153    * Values are converted to objects of different classes thusly:
154    * <dl>
155    * <dt>{@link String}</dt>
156    * <dd>No conversion.</dd>
157    * <dt>{@link Double}</dt>
158    * <dt>{@link Float}</dt>
159    * <dt>{@link Integer}</dt>
160    * <dt>{@link Long}</dt>
161    * <dt>{@link Short}</dt>
162    * <dd>Each class' respective
163    * <code>parse[...]({@link String} s)</code> method.</dd>
164    * </dl>
165    * If none of the above matches the desired class, an attempt is made to
166    * create an object with a constructor taking a single {@link String}
167    * parameter. This works well with, for instance, {@link java.io.File}.
168    *
169    * @param value The value to be converted.
170    *
171    * @param to The class to which to convert.
172    *
173    * @return An object of the specified class converted from the value.
174    *
175    * @throws ArgumentParsingException
176    */
177   static Object convert(String value, Class<?> to)
178     throws ArgumentParsingException {
179     if (value == null) {
180       throw new NullPointerException("Value may not be null.");
181     }
182     if (to == null) {
183       throw new NullPointerException("To may not be null.");
184     }
185 
186     // String: no conversion needed
187     if (to.isAssignableFrom(String.class)) {
188       return value;
189     }
190 
191     // Character: if value is single character, use it
192     if (to.isAssignableFrom(Character.class)
193       && value.length() == 1) {
194       return new Character(value.charAt(0));
195     }
196 
197     // Numericals are parsed
198     if (to.isAssignableFrom(Double.class)) {
199       return new Double(Double.parseDouble(value));
200     }
201     if (to.isAssignableFrom(Float.class)) {
202       return new Float(Float.parseFloat(value));
203     }
204     if (to.isAssignableFrom(Integer.class)) {
205       return new Integer(Integer.parseInt(value));
206     }
207     if (to.isAssignableFrom(Long.class)) {
208       return new Long(Long.parseLong(value));
209     }
210     if (to.isAssignableFrom(Short.class)) {
211       return new Short(Short.parseShort(value));
212     }
213 
214     // Attempt to construct an object from a String
215     try {
216       Constructor<?> constructor = to.getConstructor(String.class);
217       Object instance = constructor.newInstance(value);
218       return instance;
219     } catch (NoSuchMethodException | SecurityException | InstantiationException |
220              IllegalAccessException | IllegalArgumentException |
221              InvocationTargetException ex) {
222       // We did our best
223     }
224 
225     throw new ArgumentParsingException("Can't convert from String to ''{0}''.",
226                                        to.getName());
227   }
228 
229   boolean takesValue() {
230     return !isBoolean();
231   }
232 
233   boolean isBoolean() {
234     return Boolean.class.isAssignableFrom(getField().getType());
235   }
236 
237   boolean hasName(String name) {
238     return getNames().contains(name);
239   }
240 
241   CommandLineOption getCommandLineOption() {
242     return getField().getAnnotation(CommandLineOption.class);
243   }
244 
245   List<String> getNames() {
246     // Lazy creation
247     if (names == null) {
248       names = new ArrayList<>();
249       names.addAll(Arrays.asList(getCommandLineOption().names()));
250 
251       if (names.isEmpty()) {
252         names.add(getField().getName());
253       }
254 
255       // If defined, make "--no-..." version(s)
256       if (isBoolean() && getCommandLineOption().opposite()) {
257         for (String name : new ArrayList<>(getNames())) {
258           names.add("no-" + name);
259         }
260       }
261     }
262 
263     return Collections.unmodifiableList(names);
264   }
265 
266   Field getField() {
267     return field;
268   }
269 
270   boolean isCollection() {
271     return Collection.class.isAssignableFrom(getField().getType());
272   }
273 
274   Class<?> getEffectiveClass() {
275     return ((isCollection())
276             ? (Class<?>) ((ParameterizedType) getField().getGenericType())
277       .getActualTypeArguments()[0]
278             : getField().getType());
279   }
280 
281 }