1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package jdk.internal.jvmci.options;
  24 
  25 import static jdk.internal.jvmci.inittimer.InitTimer.*;
  26 
  27 import java.io.*;
  28 import java.util.*;
  29 
  30 import jdk.internal.jvmci.inittimer.*;
  31 
  32 /**
  33  * This class contains methods for parsing JVMCI options and matching them against a set of
  34  * {@link OptionDescriptors}. The {@link OptionDescriptors} are loaded from JVMCI jars, either
  35  * {@linkplain JVMCIJarsOptionDescriptorsProvider directly} or via a {@link ServiceLoader}.
  36  */
  37 public class OptionsParser {
  38 
  39     /**
  40      * Character used to escape a space or a literal % in a JVMCI option value.
  41      */
  42     private static final char ESCAPE = '%';
  43 
  44     private static final OptionValue<Boolean> PrintFlags = new OptionValue<>(false);
  45 
  46     /**
  47      * A service for looking up {@link OptionDescriptor}s.
  48      */
  49     public interface OptionDescriptorsProvider {
  50         /**
  51          * Gets the {@link OptionDescriptor} matching a given option {@linkplain Option#name() name}
  52          * or null if no option of that name is provided by this object.
  53          */
  54         OptionDescriptor get(String name);
  55     }
  56 
  57     public interface OptionConsumer {
  58         void set(OptionDescriptor desc, Object value);
  59     }
  60 
  61     /**
  62      * Finds the index of the next character in {@code s} starting at {@code from} that is a
  63      * {@linkplain #ESCAPE non-escaped} space iff {@code spaces == true}.
  64      */
  65     private static int skip(String s, int from, boolean spaces) {
  66         int len = s.length();
  67         int i = from;
  68         while (i < len) {
  69             char ch = s.charAt(i);
  70             if (ch == ESCAPE) {
  71                 if (i == len - 1) {
  72                     throw new InternalError("Escape character " + ESCAPE + " cannot be at end of jvmci.options value: " + s);
  73                 }
  74                 ch = s.charAt(i + 1);
  75                 if (ch != ESCAPE && ch != ' ') {
  76                     throw new InternalError("Escape character " + ESCAPE + " must be followed by space or another " + ESCAPE + " character");
  77                 }
  78                 if (spaces) {
  79                     return i;
  80                 }
  81                 i++;
  82             } else if (ch == ' ' != spaces) {
  83                 return i;
  84             }
  85             i++;
  86         }
  87         return len;
  88     }
  89 
  90     private static String unescape(String s) {
  91         int esc = s.indexOf(ESCAPE);
  92         if (esc == -1) {
  93             return s;
  94         }
  95         StringBuilder sb = new StringBuilder(s.length());
  96         int start = 0;
  97         do {
  98             sb.append(s.substring(start, esc));
  99             char escaped = s.charAt(esc + 1);
 100             if (escaped == ' ') {
 101                 sb.append(' ');
 102             } else {
 103                 assert escaped == ESCAPE;
 104                 sb.append(ESCAPE);
 105             }
 106             start = esc + 2;
 107             esc = s.indexOf(ESCAPE, start);
 108         } while (esc != -1);
 109         if (start < s.length()) {
 110             sb.append(s.substring(start));
 111         }
 112         return sb.toString();
 113     }
 114 
 115     /**
 116      * Parses the options in {@code <jre>/lib/jvmci/options} if {@code parseOptionsFile == true} and
 117      * the file exists followed by the {@linkplain #ESCAPE non-escaped} space separated JVMCI
 118      * options in {@code options} if {@code options != null}.
 119      *
 120      * Called from VM. This method has an object return type to allow it to be called with a VM
 121      * utility function used to call other static initialization methods.
 122      *
 123      * @param options {@linkplain #ESCAPE non-escaped} space separated set of JVMCI options to parse
 124      * @param parseOptionsFile specifies whether to look for and parse
 125      *            {@code <jre>/lib/jvmci.options}
 126      */
 127     @SuppressWarnings("try")
 128     public static Boolean parseOptionsFromVM(String options, boolean parseOptionsFile) {
 129         try (InitTimer t = timer("ParseOptions")) {
 130             JVMCIJarsOptionDescriptorsProvider odp = new JVMCIJarsOptionDescriptorsProvider();
 131 
 132             if (parseOptionsFile) {
 133                 File javaHome = new File(System.getProperty("java.home"));
 134                 File lib = new File(javaHome, "lib");
 135                 File jvmci = new File(lib, "jvmci");
 136                 File jvmciOptions = new File(jvmci, "options");
 137                 if (jvmciOptions.exists()) {
 138                     try (BufferedReader br = new BufferedReader(new FileReader(jvmciOptions))) {
 139                         String option = null;
 140                         while ((option = br.readLine()) != null) {
 141                             option = option.trim();
 142                             if (!option.isEmpty() && option.charAt(0) != '#') {
 143                                 parseOption(option, null, odp);
 144                             }
 145                         }
 146                     } catch (IOException e) {
 147                         throw new InternalError("Error reading " + jvmciOptions, e);
 148                     }
 149                 }
 150             }
 151 
 152             if (options != null) {
 153                 int index = skip(options, 0, true);
 154                 while (index < options.length()) {
 155                     int end = skip(options, index, false);
 156                     String option = unescape(options.substring(index, end));
 157                     parseOption(option, null, odp);
 158                     index = skip(options, end, true);
 159                 }
 160             }
 161         }
 162         return Boolean.TRUE;
 163     }
 164 
 165     public static void parseOption(String option, OptionConsumer setter, OptionDescriptorsProvider odp) {
 166         parseOption(OptionsLoader.options, option, setter, odp);
 167     }
 168 
 169     /**
 170      * Parses a given option value specification.
 171      *
 172      * @param option the specification of an option and its value
 173      * @param setter the object to notify of the parsed option and value
 174      * @throws IllegalArgumentException if there's a problem parsing {@code option}
 175      */
 176     public static void parseOption(SortedMap<String, OptionDescriptor> options, String option, OptionConsumer setter, OptionDescriptorsProvider odp) {
 177         if (option.length() == 0) {
 178             return;
 179         }
 180 
 181         Object value = null;
 182         String optionName = null;
 183         String valueString = null;
 184 
 185         char first = option.charAt(0);
 186         if (first == '+' || first == '-') {
 187             optionName = option.substring(1);
 188             value = (first == '+');
 189         } else {
 190             int index = option.indexOf('=');
 191             if (index == -1) {
 192                 optionName = option;
 193                 valueString = null;
 194             } else {
 195                 optionName = option.substring(0, index);
 196                 valueString = option.substring(index + 1);
 197             }
 198         }
 199 
 200         OptionDescriptor desc = odp == null ? options.get(optionName) : odp.get(optionName);
 201         if (desc == null && value != null) {
 202             int index = option.indexOf('=');
 203             if (index != -1) {
 204                 optionName = option.substring(1, index);
 205                 desc = odp == null ? options.get(optionName) : odp.get(optionName);
 206             }
 207             if (desc == null && optionName.equals("PrintFlags")) {
 208                 desc = OptionDescriptor.create("PrintFlags", Boolean.class, "Prints all JVMCI flags and exits", OptionsParser.class, "PrintFlags", PrintFlags);
 209             }
 210         }
 211         if (desc == null) {
 212             List<OptionDescriptor> matches = fuzzyMatch(options, optionName);
 213             Formatter msg = new Formatter();
 214             msg.format("Could not find option %s", optionName);
 215             if (!matches.isEmpty()) {
 216                 msg.format("%nDid you mean one of the following?");
 217                 for (OptionDescriptor match : matches) {
 218                     boolean isBoolean = match.getType() == Boolean.class;
 219                     msg.format("%n    %s%s%s", isBoolean ? "(+/-)" : "", match.getName(), isBoolean ? "" : "=<value>");
 220                 }
 221             }
 222             throw new IllegalArgumentException(msg.toString());
 223         }
 224 
 225         Class<?> optionType = desc.getType();
 226 
 227         if (value == null) {
 228             if (optionType == Boolean.TYPE || optionType == Boolean.class) {
 229                 throw new IllegalArgumentException("Boolean option '" + optionName + "' must use +/- prefix");
 230             }
 231 
 232             if (valueString == null) {
 233                 throw new IllegalArgumentException("Missing value for non-boolean option '" + optionName + "' must use " + optionName + "=<value> format");
 234             }
 235 
 236             if (optionType == Float.class) {
 237                 value = Float.parseFloat(valueString);
 238             } else if (optionType == Double.class) {
 239                 value = Double.parseDouble(valueString);
 240             } else if (optionType == Integer.class) {
 241                 value = Integer.valueOf((int) parseLong(valueString));
 242             } else if (optionType == Long.class) {
 243                 value = Long.valueOf(parseLong(valueString));
 244             } else if (optionType == String.class) {
 245                 value = valueString;
 246             } else {
 247                 throw new IllegalArgumentException("Wrong value for option '" + optionName + "'");
 248             }
 249         } else {
 250             if (optionType != Boolean.class) {
 251                 throw new IllegalArgumentException("Non-boolean option '" + optionName + "' can not use +/- prefix. Use " + optionName + "=<value> format");
 252             }
 253         }
 254         if (setter == null) {
 255             desc.getOptionValue().setValue(value);
 256         } else {
 257             setter.set(desc, value);
 258         }
 259 
 260         if (PrintFlags.getValue()) {
 261             printFlags(options, "JVMCI", System.out);
 262             System.exit(0);
 263         }
 264     }
 265 
 266     private static long parseLong(String v) {
 267         String valueString = v.toLowerCase();
 268         long scale = 1;
 269         if (valueString.endsWith("k")) {
 270             scale = 1024L;
 271         } else if (valueString.endsWith("m")) {
 272             scale = 1024L * 1024L;
 273         } else if (valueString.endsWith("g")) {
 274             scale = 1024L * 1024L * 1024L;
 275         } else if (valueString.endsWith("t")) {
 276             scale = 1024L * 1024L * 1024L * 1024L;
 277         }
 278 
 279         if (scale != 1) {
 280             /* Remove trailing scale character. */
 281             valueString = valueString.substring(0, valueString.length() - 1);
 282         }
 283 
 284         return Long.parseLong(valueString) * scale;
 285     }
 286 
 287     /**
 288      * Wraps some given text to one or more lines of a given maximum width.
 289      *
 290      * @param text text to wrap
 291      * @param width maximum width of an output line, exception for words in {@code text} longer than
 292      *            this value
 293      * @return {@code text} broken into lines
 294      */
 295     private static List<String> wrap(String text, int width) {
 296         List<String> lines = Collections.singletonList(text);
 297         if (text.length() > width) {
 298             String[] chunks = text.split("\\s+");
 299             lines = new ArrayList<>();
 300             StringBuilder line = new StringBuilder();
 301             for (String chunk : chunks) {
 302                 if (line.length() + chunk.length() > width) {
 303                     lines.add(line.toString());
 304                     line.setLength(0);
 305                 }
 306                 if (line.length() != 0) {
 307                     line.append(' ');
 308                 }
 309                 String[] embeddedLines = chunk.split("%n", -2);
 310                 if (embeddedLines.length == 1) {
 311                     line.append(chunk);
 312                 } else {
 313                     for (int i = 0; i < embeddedLines.length; i++) {
 314                         line.append(embeddedLines[i]);
 315                         if (i < embeddedLines.length - 1) {
 316                             lines.add(line.toString());
 317                             line.setLength(0);
 318                         }
 319                     }
 320                 }
 321             }
 322             if (line.length() != 0) {
 323                 lines.add(line.toString());
 324             }
 325         }
 326         return lines;
 327     }
 328 
 329     public static void printFlags(SortedMap<String, OptionDescriptor> sortedOptions, String prefix, PrintStream out) {
 330         out.println("[List of " + prefix + " options]");
 331         for (Map.Entry<String, OptionDescriptor> e : sortedOptions.entrySet()) {
 332             e.getKey();
 333             OptionDescriptor desc = e.getValue();
 334             Object value = desc.getOptionValue().getValue();
 335             List<String> helpLines = wrap(desc.getHelp(), 70);
 336             out.println(String.format("%9s %-40s = %-14s %s", desc.getType().getSimpleName(), e.getKey(), value, helpLines.get(0)));
 337             for (int i = 1; i < helpLines.size(); i++) {
 338                 out.println(String.format("%67s %s", " ", helpLines.get(i)));
 339             }
 340         }
 341     }
 342 
 343     /**
 344      * Compute string similarity based on Dice's coefficient.
 345      *
 346      * Ported from str_similar() in globals.cpp.
 347      */
 348     static float stringSimiliarity(String str1, String str2) {
 349         int hit = 0;
 350         for (int i = 0; i < str1.length() - 1; ++i) {
 351             for (int j = 0; j < str2.length() - 1; ++j) {
 352                 if ((str1.charAt(i) == str2.charAt(j)) && (str1.charAt(i + 1) == str2.charAt(j + 1))) {
 353                     ++hit;
 354                     break;
 355                 }
 356             }
 357         }
 358         return 2.0f * hit / (str1.length() + str2.length());
 359     }
 360 
 361     private static final float FUZZY_MATCH_THRESHOLD = 0.7F;
 362 
 363     /**
 364      * Returns the set of options that fuzzy match a given option name.
 365      */
 366     private static List<OptionDescriptor> fuzzyMatch(SortedMap<String, OptionDescriptor> options, String optionName) {
 367         List<OptionDescriptor> matches = new ArrayList<>();
 368         for (Map.Entry<String, OptionDescriptor> e : options.entrySet()) {
 369             float score = stringSimiliarity(e.getKey(), optionName);
 370             if (score >= FUZZY_MATCH_THRESHOLD) {
 371                 matches.add(e.getValue());
 372             }
 373         }
 374         return matches;
 375     }
 376 }