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 }