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 }