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