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.maven.plugin.compiler;
20  
21  import javax.tools.OptionChecker;
22  
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.function.UnaryOperator;
29  
30  import org.apache.maven.api.plugin.Log;
31  
32  /**
33   * An helper class for preparing the options to pass to the tool (compiler or document generator).
34   * It does <em>not</em> include the options related to paths (class-path, destination directory, <i>etc.</i>).
35   * If an option is unsupported by the tool, a message is logged at the warning level.
36   *
37   * @author Martin Desruisseaux
38   */
39  public final class Options {
40      /**
41       * The list of options with their values. For example, in order to compile for Java 17,
42       * {@code --release} and {@code 17} shall be two consecutive elements in this list.
43       */
44      final List<String> options;
45  
46      /**
47       * The tools to use for checking whether an option is supported.
48       * It can be the Java compiler or the Javadoc generator.
49       */
50      private final OptionChecker checker;
51  
52      /**
53       * Where to report warnings about unsupported options.
54       */
55      private final Log logger;
56  
57      /**
58       * The warning message to log. This is used when a warning is not logged immediately,
59       * but deferred for allowing the caller to complete the message before to log.
60       */
61      private String warning;
62  
63      /**
64       * Creates an initially empty list of options.
65       *
66       * @param checker the tools to use for checking whether an option is supported
67       * @param logger where to report warnings about unsupported options
68       */
69      Options(OptionChecker checker, Log logger) {
70          options = new ArrayList<>();
71          this.checker = checker;
72          this.logger = logger;
73      }
74  
75      /**
76       * Strips white spaces and returns the result if non-empty, or {@code null} otherwise.
77       *
78       * @param value  the value from which to strip white spaces, or {@code null}
79       * @return the stripped value, or {@code null} if the value was null or blank
80       */
81      private static String strip(String value) {
82          if (value != null) {
83              value = value.strip();
84              if (value.isEmpty()) {
85                  return null;
86              }
87          }
88          return value;
89      }
90  
91      /**
92       * Adds the given option if the given value is true and the option is supported.
93       * If the option is unsupported, then a warning is logged and the option is not added.
94       *
95       * @param option the option (e.g. {@code --enable-preview})
96       * @param value value of the option
97       * @return whether the option has been added
98       */
99      public boolean addIfTrue(String option, boolean value) {
100         if (value && checkNumberOfArguments(option, 0, true)) {
101             options.add(option);
102             return true;
103         }
104         return false;
105     }
106 
107     /**
108      * Adds the given option if a non-null and non-blank value is provided and if the option is supported.
109      * If the option is unsupported by the tool, then a warning is logged and the option is not added.
110      *
111      * @param option the option (e.g., {@code --release})
112      * @param value value of the option, or {@code null} or blank if none
113      * @return whether the option has been added
114      */
115     public boolean addIfNonBlank(String option, String value) {
116         value = strip(value);
117         if (value != null) {
118             if (checkNumberOfArguments(option, 1, true)) {
119                 options.add(option);
120                 options.add(value);
121                 return true;
122             }
123         }
124         return false;
125     }
126 
127     /**
128      * Adds the given option using the {@code option:values} syntax where {@code values} is a coma-separated list.
129      * The option is added only if at least one non-blank value is provided. Values are converted to lower cases.
130      * Leading and trailing spaces are removed. If a filter is specified, then that filter will receive the values
131      * specified by the users and shall return the values to append, or {@code null} for not appending the option.
132      *
133      * @param option the option (e.g. {@code -g})
134      * @param values coma-separated values of the option, or {@code null} if none
135      * @param valids valid values for better error message when needed, or {@code null} if unspecified
136      * @param filter filter to apply on the values before to add them, or {@code null} if none
137      * @return whether the option has been added
138      */
139     public boolean addComaSeparated(
140             final String option, String values, Collection<String> valids, UnaryOperator<String[]> filter) {
141         if (values == null) {
142             return false;
143         }
144         /*
145          * Rebuild the comma-separated list of options with spaces removed, empty values omitted and case
146          * changed to lower-case. The split list will be reused for diagnostic if the option is not accepted.
147          */
148         String[] split = values.split(",");
149         int count = 0;
150         for (String value : split) {
151             value = value.strip();
152             if (!value.isEmpty()) {
153                 split[count++] = value.toLowerCase(Locale.US);
154             }
155         }
156         /*
157          * If a filter is specified, replace the user-specified list by the filtered one.
158          * The filtering may result in an empty list, which is interpreted as an option without value.
159          * This is different than an absence of user-supplied values, which is interpreted as no option.
160          * This subtle difference explains why the check for absence of values is done before filtering,
161          * and is needed for making possible to replace "-g:all" by "-g" (because the "all" value is a
162          * Maven addition).
163          */
164         if (count == 0) {
165             return false;
166         }
167         if (filter != null) {
168             if (count != split.length) {
169                 split = Arrays.copyOfRange(split, 0, count);
170             }
171             split = filter.apply(split);
172             if (split == null) {
173                 return false;
174             }
175             count = split.length;
176         }
177         /*
178          * Format the option (possibly with no values), then validate.
179          */
180         var sb = new StringBuilder(option);
181         for (int i = 0; i < count; i++) {
182             sb.append(i == 0 ? ':' : ',').append(split[i]);
183         }
184         String s = sb.toString();
185         if (checkNumberOfArguments(s, 0, false)) {
186             options.add(s);
187             return true;
188         }
189         /*
190          * A log message has been prepared in the `warning` field for saying that the option is not supported.
191          * If a collection of valid options was provided, use it for identifying which option was invalid.
192          */
193         if (valids != null) {
194             for (int i = 0; i < count; i++) {
195                 String value = split[i];
196                 if (!valids.contains(value)) {
197                     sb.setLength(0);
198                     sb.append(warning);
199                     sb.setLength(sb.length() - 1); // Remove the trailing dot.
200                     sb.append(", because the specified ")
201                             .append(option)
202                             .append(" value '")
203                             .append(value)
204                             .append("' is unexpected. Legal values are: ");
205                     int j = 0;
206                     for (String valid : valids) {
207                         if (j++ != 0) {
208                             sb.append(", ");
209                             if (j == valids.size()) {
210                                 sb.append("and ");
211                             }
212                         }
213                         sb.append('\'').append(valid).append('\'');
214                     }
215                     warning = sb.append('.').toString();
216                     break;
217                 }
218             }
219         }
220         logger.warn(warning);
221         warning = null;
222         return false;
223     }
224 
225     /**
226      * Verifies the validity of the given memory setting and adds it as an option.
227      * If the value has no units and Maven defaults are enabled, appends "M" as the default units of measurement.
228      * Note: in the International System of Units, the symbol shall be upper-case "M". The lower-case "m" symbol
229      * is not correct as it stands for "milli".
230      *
231      * @param option the option (e.g. {@code -J-Xms})
232      * @param label name of the XML element or attribute, used only if a warning message needs to be produced
233      * @param value the memory setting, or {@code null} if none
234      * @param addDefaultUnit whether to add a default unit (currently 'M') if none is provided
235      * @return whether the option has been added
236      */
237     public boolean addMemoryValue(String option, String label, String value, boolean addDefaultUnit) {
238         value = strip(value);
239         if (value != null) {
240             int length = value.length();
241             for (int i = 0; i < length; i++) {
242                 char c = value.charAt(i);
243                 if (c < '0' || c > '9') { // Do no use `isDigit(…)` because we do not accept other languages.
244                     if (i == length - 1) {
245                         c = Character.toUpperCase(c);
246                         if (c == 'K' || c == 'M' || c == 'G') {
247                             addDefaultUnit = false;
248                             break;
249                         }
250                     }
251                     logger.warn("Invalid value for " + label + "=\"" + value + "\". Ignoring this option.");
252                     return false;
253                 }
254             }
255             if (addDefaultUnit) {
256                 value += 'M'; // Upper case because this is the correct case in International System of Units.
257                 logger.warn("Value " + label + "=\"" + value + "\" has been specified without unit. "
258                         + "An explicit \"M\" unit symbol should be appended for avoiding ambiguity.");
259             }
260             option += value;
261             if (checkNumberOfArguments(option, 0, true)) {
262                 options.add(option);
263                 return true;
264             }
265         }
266         return false;
267     }
268 
269     /**
270      * Verifies if the given option is supported and accepts the given number of arguments.
271      * If not, a warning is logged if {@code immediate} is {@code true}, or stored in the
272      * {@link #warning} field if {@code immediate} is {@code false}.
273      *
274      * <p>If a message is stored in {@link #warning}, then it will always end with a dot.
275      * This guarantee allows callers to delete the last character and replace it by a coma
276      * for continuing the sentence.</p>
277      *
278      * @param option the option to validate
279      * @param count the number of arguments that the caller wants to provide
280      * @param immediate whether to log immediately or to store the message in {@link #warning}
281      * @return whether the given option is supported and accepts the specified number of arguments
282      */
283     private boolean checkNumberOfArguments(String option, int count, boolean immediate) {
284         int expected = checker.isSupportedOption(option);
285         if (expected == count) {
286             warning = null;
287             return true;
288         } else if (expected < 1) {
289             if (checker instanceof ForkedCompiler) {
290                 return true; // That implementation actually knows nothing about which options are supported.
291             }
292             warning = "The '" + option + "' option is not supported.";
293         } else if (expected == 0) {
294             warning = "The '" + option + "' option does not expect any argument.";
295         } else if (expected == 1) {
296             warning = "The '" + option + "' option expects a single argument.";
297         } else {
298             warning = "The '" + option + "' option expects " + expected + " arguments.";
299         }
300         if (immediate) {
301             logger.warn(warning);
302             warning = null;
303         }
304         return false;
305     }
306 
307     /**
308      * Adds the non-null and non-empty elements without verifying their validity.
309      * This method is used for user-specified compiler arguments.
310      *
311      * @param arguments the arguments to add, or {@code null} or empty if none
312      */
313     public void addUnchecked(Iterable<String> arguments) {
314         if (arguments != null) {
315             for (String arg : arguments) {
316                 if (arg != null) {
317                     arg = arg.strip();
318                     if (!arg.isEmpty()) {
319                         options.add(arg);
320                     }
321                 }
322             }
323         }
324     }
325 
326     /**
327      * Splits the given arguments around spaces, then adds them without verifying their validity.
328      * This is used for user-specified arguments.
329      *
330      * @param arguments the arguments to add, or {@code null} if none
331      *
332      * @deprecated Use {@link #addUnchecked(List)} instead. This method does not check for quoted strings.
333      */
334     @Deprecated(since = "4.0.0")
335     void addUnchecked(String arguments) {
336         if (arguments != null) {
337             addUnchecked(Arrays.asList(arguments.split(" ")));
338         }
339     }
340 }