View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.rat.ui;
20  
21  import java.util.HashMap;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.function.Function;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.apache.commons.cli.Option;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.rat.OptionCollection;
33  import org.apache.rat.utils.CasedString;
34  
35  /**
36   * Abstract class that provides the framework for UI-specific RAT options.
37   * In this context UI option means an option expressed in the specific UI.
38   * @param <T> the concrete implementation of AbstractOption.
39   */
40  public abstract class UIOption<T extends UIOption<T>> {
41      /**
42       * The pattern to match CLI options in text.
43       */
44      protected static final Pattern PATTERN = Pattern.compile("-?-([A-Za-z0-9]+-?)+"); // NOSONAR
45      /**
46       * The actual UI-specific name for the option.
47       */
48      protected final Option option;
49      /**
50       * The name for the option.
51       */
52      protected final CasedString name;
53      /**
54       * The argument type for this option.
55       */
56      protected final OptionCollection.ArgumentType argumentType;
57      /**
58       * The AbstractOptionCollection associated with this AbstractOption.
59       */
60      protected final UIOptionCollection<T> optionCollection;
61  
62      /**
63       * Constructor.
64       *
65       * @param builder UIOption builder.
66       */
67      protected UIOption(final Builder<T, ?> builder) {
68          this.optionCollection = builder.optionCollection;
69          this.option = builder.option;
70          this.name = builder.name;
71  
72          OptionCollection.ArgumentType argType;
73          if (option.hasArg()) {
74              if (option.getArgName() == null) {
75                  argType = OptionCollection.ArgumentType.ARG;
76              } else {
77                  // extract the name of the argument type.
78                  try {
79                      argType = OptionCollection.ArgumentType.valueOf(option.getArgName().toUpperCase(Locale.ROOT));
80                  } catch (IllegalArgumentException e) {
81                      argType = OptionCollection.ArgumentType.ARG;
82                  }
83              }
84          } else {
85              argType = OptionCollection.ArgumentType.NONE;
86          }
87          this.argumentType = argType;
88      }
89  
90      @Override
91      public boolean equals(final Object o) {
92          if (o == null || getClass() != o.getClass()) {
93              return false;
94          }
95          UIOption<?> uiOption = (UIOption<?>) o;
96          return getOption().equals(uiOption.getOption()) && name.equals(uiOption.name) && argumentType == uiOption.argumentType;
97      }
98  
99      @Override
100     public int hashCode() {
101         return name.hashCode();
102     }
103 
104     /**
105      * Gets the AbstractOptionCollection that this option is a member of.
106      * @return the AbstractOptionCollection that this option is a member of.
107      * @param <X> the collection type to return.
108      */
109     public final <X extends UIOptionCollection<T>> X getOptionCollection() {
110         return (X) optionCollection;
111     }
112 
113     /**
114      * Gets the option this abstract option is wrapping.
115      * @return the original option.
116      */
117     public final Option getOption() {
118         return option;
119     }
120 
121     /**
122      * Return default value.
123      * @return default value or {@code null} if no argument given.
124      */
125     public final String getDefaultValue() {
126         return optionCollection.defaultValue(option);
127     }
128 
129     /**
130      * Provide means to wrap the given option depending on the UI-specific option implementation.
131      * @param option the CLI option.
132      * @return the cleaned up option name.
133      */
134     protected abstract String cleanupName(Option option);
135 
136     /**
137      * Gets an example of how to use this option in the native UI.
138      * @return an example of how to use this option in the native UI.
139      */
140     public abstract String getExample();
141 
142     /**
143      * Replaces CLI pattern options with implementation specific pattern options.
144      * @param str the string to clean.
145      * @return the string with CLI names replaced with implementation specific names.
146      */
147     public String cleanup(final String str) {
148         String workingStr = str;
149         if (StringUtils.isNotBlank(workingStr)) {
150             Map<String, String> maps = new HashMap<>();
151             Matcher matcher = PATTERN.matcher(workingStr);
152             while (matcher.find()) {
153                 String key = matcher.group();
154                 String optKey = key.substring(1 == key.indexOf('-', 1) ?  2 : 1);
155                 Optional<Option> maybeResult = getOptionCollection().getOptions().getOptions().stream()
156                                 .filter(o -> optKey.equals(o.getOpt()) || optKey.equals(o.getLongOpt())).findFirst();
157                 maybeResult.ifPresent(value -> maps.put(key, cleanupName(value)));
158             }
159             for (Map.Entry<String, String> entry : maps.entrySet()) {
160                 workingStr = workingStr.replaceAll(Pattern.quote(entry.getKey()), entry.getValue());
161             }
162         }
163         return workingStr;
164     }
165 
166     /**
167      * Gets the implementation specific name for the native UI.
168      * @return the implementation specific name for the native UI.
169      */
170     public final String getName() {
171         return name.toString();
172     }
173 
174     /**
175      * Gets the CasedString version of the name for the native UI.
176      * @return the CasedString version of the name for the native UI.
177      */
178     public final CasedString getCasedName() {
179         return name;
180     }
181 
182     /**
183      * Return a string showing long and short options if they are available. Will return
184      * a string.
185      * @return a string showing long and short options if they are available. Never {@code null}.
186      */
187     public abstract String getText();
188 
189     /**
190      * Gets the description in implementation specific format.
191      *
192      * @return the description or an empty string.
193      */
194     public final String getDescription() {
195         return cleanup(option.getDescription());
196     }
197 
198     /**
199      * Gets the simple class name for the data type for this option.
200      * Normally "String".
201      * @return the simple class name for the type.
202      */
203     public final Class<?> getType() {
204         return option.hasArg() ? ((Class<?>) option.getType()) : boolean.class;
205     }
206 
207     /**
208      * Gets the argument name if there is one.
209      * @return the Argument name
210      */
211     public final String getArgName() {
212         return argumentType.getDisplayName();
213     }
214 
215     /**
216      * Gets the argument type if there is one.
217      * @return the Argument name
218      */
219     public final OptionCollection.ArgumentType getArgType() {
220         return argumentType;
221     }
222 
223     /**
224      * Determines if the option is deprecated.
225      * @return {@code true} if the option is deprecated.
226      */
227     public final boolean isDeprecated() {
228         return option.isDeprecated();
229     }
230 
231     /**
232      * Determines if the option is required.
233      * @return {@code true} if the option is required.
234      */
235     public final boolean isRequired() {
236         return option.isRequired();
237     }
238 
239     /**
240      * Determine if the enclosed option expects an argument.
241      * @return {@code true} if the enclosed option expects at least one argument.
242      */
243     public final boolean hasArg() {
244         return option.hasArg();
245     }
246 
247     /**
248      * Returns {@code true} if the option has multiple arguments.
249      * @return {@code true} if the option has multiple arguments.
250      */
251     public final boolean hasArgs() {
252         return option.hasArgs();
253     }
254 
255     /**
256      * The key value for the option.
257      * @return the key value for the CLI argument map.
258      */
259     public final String keyValue() {
260         return StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt());
261     }
262 
263     /**
264      * Gets the deprecated string if the option is deprecated, or an empty string otherwise.
265      * @return the deprecated string if the option is deprecated, or an empty string otherwise.
266      */
267     public final String getDeprecated() {
268         return option.isDeprecated() ? cleanup(StringUtils.defaultIfEmpty(option.getDeprecated().toString(), StringUtils.EMPTY)) : StringUtils.EMPTY;
269     }
270 
271     /**
272      * The abstract UIOption Builder.
273      * @param <T> the concrete type of the UIOption this builder produces.
274      * @param <B> the concrete type of this builder.
275      */
276     public abstract static class Builder<T extends UIOption<T>, B extends Builder<T, B>> {
277         /**
278          * The collection to add the new UI Option to.
279          */
280         private UIOptionCollection<T> optionCollection;
281         /**
282          * THe base option that is being mapped.
283          */
284         private Option option;
285         /**
286          * The UI name of this option.
287          */
288         private CasedString name = CasedString.NULL;
289         /**
290          * Constructor.
291          */
292         protected Builder() {
293         }
294 
295         /**
296          * Returns a function to convert an Option to a CasedString that is the native name for the option.
297          * @return a function to convert an Option to a CasedString that is the native name for the option.
298          */
299         protected abstract Function<Option, CasedString> getNameFactory();
300 
301         /**
302          * Gets the option.
303          * @return the option or {@code null} if the option is not set.
304          */
305         protected final Option option() {
306             return option;
307         }
308 
309         /**
310          * Gets the UIOptionCollection that the options will be added to.
311          * @return UIOptionCollection that the options will be added to.
312          */
313         protected final UIOptionCollection<T> optionCollection() {
314             return optionCollection;
315         }
316 
317         /**
318          * Returns this builder cast to the builder type.
319          * Useful for implementing fluent builders.
320          * @return this builder.
321          */
322         protected final B self() {
323             return (B) this;
324         }
325 
326         /**
327          * Sets the UIOptionCollection that the UIOptions will be added to.
328          * @param optionCollection the UIOptionCollection that the UIOptions will be added to.
329          * @return this
330          */
331         public B optionCollection(final UIOptionCollection<T> optionCollection) {
332             this.optionCollection = optionCollection;
333             return self();
334         }
335 
336         /**
337          * Sets the Option that the UIOption will be generated from.
338          * @param option to build the UIOption from.
339          * @return this
340          */
341         public B option(final Option option) {
342             Objects.requireNonNull(option, "Option may not be null");
343             this.option = option;
344             this.name = getNameFactory().apply(option);
345             if (this.name == null || this.name.isNull()) {
346                 throw new IllegalArgumentException("name for " + option + " may not be null or contain a null value");
347             }
348             return self();
349         }
350 
351         /**
352          * Executes the final build.
353          * @return An instance of the UIOption.
354          * @throws IllegalArgumentException if values are not set correctly.
355          */
356         protected abstract T doBuild() throws IllegalArgumentException;
357 
358         /**
359          * Builds the UIOption.
360          * @return the UIOption.
361          * @throws IllegalArgumentException if values are not set correctly.
362          */
363         public final T build() throws IllegalArgumentException {
364             Objects.requireNonNull(optionCollection, "OptionCollection may not be null");
365             Objects.requireNonNull(option, "Option may not be null");
366             if (name == null || name.isNull()) {
367                 throw new IllegalArgumentException("name must not be null or contain a null value");
368             }
369             return doBuild();
370         }
371     }
372 }