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 }