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 }