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