1 /*
   2  * Copyright (c) 2004, 2018, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.tools.jstat;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 
  31 /**
  32  * A class implementing a simple predictive parser for output format
  33  * specification language for the jstat command.
  34  *
  35  * @author Brian Doherty
  36  * @since 1.5
  37  */
  38 public class Parser {
  39 
  40     private static boolean pdebug = Boolean.getBoolean("jstat.parser.debug");
  41     private static boolean ldebug = Boolean.getBoolean("jstat.lex.debug");
  42 
  43     private static final char OPENBLOCK = '{';
  44     private static final char CLOSEBLOCK = '}';
  45     private static final char DOUBLEQUOTE = '"';
  46     private static final char PERCENT_CHAR = '%';
  47     private static final char OPENPAREN = '(';
  48     private static final char CLOSEPAREN = ')';
  49 
  50     private static final char OPERATOR_PLUS = '+';
  51     private static final char OPERATOR_MINUS = '-';
  52     private static final char OPERATOR_MULTIPLY = '*';
  53     private static final char OPERATOR_DIVIDE = '/';
  54 
  55     private static final String OPTION = "option";
  56     private static final String COLUMN = "column";
  57     private static final String DATA = "data";
  58     private static final String HEADER = "header";
  59     private static final String WIDTH = "width";
  60     private static final String FORMAT = "format";
  61     private static final String ALIGN = "align";
  62     private static final String SCALE = "scale";
  63     private static final String REQUIRED = "required";
  64 
  65     private static final String START = OPTION;
  66 
  67     private static final Set<String> scaleKeyWords = Scale.keySet();
  68     private static final Set<String> alignKeyWords = Alignment.keySet();
  69     private static final Set<String> boolKeyWords = Set.of("true", "false");
  70     private static String[] otherKeyWords = {
  71         OPTION, COLUMN, DATA, HEADER, WIDTH, FORMAT, ALIGN, SCALE, REQUIRED
  72     };
  73 
  74     private static char[] infixOps = {
  75         OPERATOR_PLUS, OPERATOR_MINUS, OPERATOR_MULTIPLY, OPERATOR_DIVIDE
  76     };
  77 
  78     private static char[] delimiters = {
  79         OPENBLOCK, CLOSEBLOCK, PERCENT_CHAR, OPENPAREN, CLOSEPAREN
  80     };
  81 
  82 
  83     private static Set<String> reservedWords;
  84 
  85     private StreamTokenizer st;
  86     private String filename;
  87     private Token lookahead;
  88     private Token previous;
  89     private int columnCount;
  90     private OptionFormat optionFormat;
  91 
  92     public Parser(String filename) throws FileNotFoundException {
  93         this.filename = filename;
  94         Reader r = new BufferedReader(new FileReader(filename));
  95     }
  96 
  97     public Parser(Reader r) {
  98         st = new StreamTokenizer(r);
  99 
 100         // allow both c++ style comments
 101         st.ordinaryChar('/');
 102         st.wordChars('_','_');
 103         st.slashSlashComments(true);
 104         st.slashStarComments(true);
 105 
 106         reservedWords = new HashSet<String>();
 107         for (int i = 0; i < otherKeyWords.length; i++) {
 108             reservedWords.add(otherKeyWords[i]);
 109         }
 110 
 111         for (int i = 0; i < delimiters.length; i++ ) {
 112             st.ordinaryChar(delimiters[i]);
 113         }
 114 
 115         for (int i = 0; i < infixOps.length; i++ ) {
 116             st.ordinaryChar(infixOps[i]);
 117         }
 118     }
 119 
 120     /**
 121      * push back the lookahead token and restore the lookahead token
 122      * to the previous token.
 123      */
 124     private void pushBack() {
 125         lookahead = previous;
 126         st.pushBack();
 127     }
 128 
 129     /**
 130      * retrieve the next token, placing the token value in the lookahead
 131      * member variable, storing its previous value in the previous member
 132      * variable.
 133      */
 134     private void nextToken() throws ParserException, IOException {
 135         int t = st.nextToken();
 136         previous = lookahead;
 137         lookahead = new Token(st.ttype, st.sval, st.nval);
 138         log(ldebug, "lookahead = " + lookahead);
 139     }
 140 
 141     /**
 142      * match one of the token values in the given set of key words
 143      * token is assumed to be of type TT_WORD, and the set is assumed
 144      * to contain String objects.
 145      */
 146     private Token matchOne(Set<String> keyWords) throws ParserException, IOException {
 147         if ((lookahead.ttype == StreamTokenizer.TT_WORD)
 148                 && keyWords.contains(lookahead.sval)) {
 149             Token t = lookahead;
 150             nextToken();
 151             return t;
 152         }
 153         throw new SyntaxException(st.lineno(), keyWords, lookahead);
 154     }
 155 
 156     /**
 157      * match a token with TT_TYPE=type, and the token value is a given sequence
 158      * of characters.
 159      */
 160     private void match(int ttype, String token)
 161                  throws ParserException, IOException {
 162         if (lookahead.ttype == ttype && lookahead.sval.compareTo(token) == 0) {
 163             nextToken();
 164         } else {
 165            throw new SyntaxException(st.lineno(), new Token(ttype, token),
 166                                      lookahead);
 167         }
 168     }
 169 
 170     /**
 171      * match a token with TT_TYPE=type
 172      */
 173     private void match(int ttype) throws ParserException, IOException {
 174         if (lookahead.ttype == ttype) {
 175             nextToken();
 176         } else {
 177            throw new SyntaxException(st.lineno(), new Token(ttype), lookahead);
 178         }
 179     }
 180 
 181     /**
 182      * match a token with TT_TYPE=char, where the token value is the given char.
 183      */
 184     private void match(char ttype) throws ParserException, IOException {
 185       if (lookahead.ttype == (int)ttype) {
 186           nextToken();
 187       }
 188       else {
 189           throw new SyntaxException(st.lineno(), new Token((int)ttype),
 190                                     lookahead);
 191       }
 192     }
 193 
 194     /**
 195      * match a token with TT_TYPE='"', where the token value is a sequence
 196      * of characters between matching quote characters.
 197      */
 198     private void matchQuotedString() throws ParserException, IOException {
 199         match(DOUBLEQUOTE);
 200     }
 201 
 202     /**
 203      * match a TT_NUMBER token that matches a parsed number value
 204      */
 205     private void matchNumber() throws ParserException, IOException {
 206         match(StreamTokenizer.TT_NUMBER);
 207     }
 208 
 209     /**
 210      * match a TT_WORD token that matches an arbitrary, not quoted token.
 211      */
 212     private void matchID() throws ParserException, IOException {
 213         match(StreamTokenizer.TT_WORD);
 214     }
 215 
 216     /**
 217      * match a TT_WORD token that matches the given string
 218      */
 219     private void match(String token) throws ParserException, IOException {
 220         match(StreamTokenizer.TT_WORD, token);
 221     }
 222 
 223     /**
 224      * determine if the given word is a reserved key word
 225      */
 226     private boolean isReservedWord(String word) {
 227         return reservedWords.contains(word);
 228     }
 229 
 230     /**
 231      * determine if the give work is a reserved key word
 232      */
 233     private boolean isInfixOperator(char op) {
 234         for (int i = 0; i < infixOps.length; i++) {
 235             if (op == infixOps[i]) {
 236                 return true;
 237             }
 238         }
 239         return false;
 240     }
 241 
 242     /**
 243      * scalestmt -> 'scale' scalespec
 244      * scalespec -> <see above scaleTerminals array>
 245      */
 246     private void scaleStmt(ColumnFormat cf)
 247                  throws ParserException, IOException {
 248         match(SCALE);
 249         Token t = matchOne(scaleKeyWords);
 250         cf.setScale(Scale.toScale(t.sval));
 251         String scaleString = t.sval;
 252         log(pdebug, "Parsed: scale -> " + scaleString);
 253     }
 254 
 255     /**
 256      * alignstmt -> 'align' alignspec
 257      * alignspec -> <see above alignTerminals array>
 258      */
 259     private void alignStmt(ColumnFormat cf)
 260                  throws ParserException, IOException {
 261         match(ALIGN);
 262         Token t = matchOne(alignKeyWords);
 263         cf.setAlignment(Alignment.toAlignment(t.sval));
 264         String alignString = t.sval;
 265         log(pdebug, "Parsed: align -> " + alignString);
 266     }
 267 
 268     /**
 269      * headerstmt -> 'header' quotedstring
 270      */
 271     private void headerStmt(ColumnFormat cf)
 272                  throws ParserException, IOException {
 273         match(HEADER);
 274         String headerString = lookahead.sval;
 275         matchQuotedString();
 276         cf.setHeader(headerString);
 277         log(pdebug, "Parsed: header -> " + headerString);
 278     }
 279 
 280     /**
 281      * widthstmt -> 'width' integer
 282      */
 283     private void widthStmt(ColumnFormat cf)
 284                  throws ParserException, IOException {
 285         match(WIDTH);
 286         double width = lookahead.nval;
 287         matchNumber();
 288         cf.setWidth((int)width);
 289         log(pdebug, "Parsed: width -> " + width );
 290     }
 291 
 292     /**
 293      * formatstmt -> 'format' quotedstring
 294      */
 295     private void formatStmt(ColumnFormat cf)
 296                  throws ParserException, IOException {
 297         match(FORMAT);
 298         String formatString = lookahead.sval;
 299         matchQuotedString();
 300         cf.setFormat(formatString);
 301         log(pdebug, "Parsed: format -> " + formatString);
 302     }
 303 
 304     /**
 305      *  Primary -> Literal | Identifier | '(' Expression ')'
 306      */
 307     private Expression primary() throws ParserException, IOException {
 308         Expression e = null;
 309 
 310         switch (lookahead.ttype) {
 311         case OPENPAREN:
 312             match(OPENPAREN);
 313             e = expression();
 314             match(CLOSEPAREN);
 315             break;
 316         case StreamTokenizer.TT_WORD:
 317             String s = lookahead.sval;
 318             if (isReservedWord(s)) {
 319                 throw new SyntaxException(st.lineno(), "IDENTIFIER",
 320                                           "Reserved Word: " + lookahead.sval);
 321             }
 322             matchID();
 323             e = new Identifier(s);
 324             log(pdebug, "Parsed: ID -> " + s);
 325             break;
 326         case StreamTokenizer.TT_NUMBER:
 327             double literal = lookahead.nval;
 328             matchNumber();
 329             e = new Literal(Double.valueOf(literal));
 330             log(pdebug, "Parsed: number -> " + literal);
 331             break;
 332         default:
 333             throw new SyntaxException(st.lineno(), "IDENTIFIER", lookahead);
 334         }
 335         log(pdebug, "Parsed: primary -> " + e);
 336         return e;
 337     }
 338 
 339     /**
 340      * Unary -> ('+'|'-') Unary | Primary
 341      */
 342     private Expression unary() throws ParserException, IOException {
 343         Expression e = null;
 344         Operator op = null;
 345 
 346         while (true) {
 347             switch (lookahead.ttype) {
 348             case OPERATOR_PLUS:
 349                 match(OPERATOR_PLUS);
 350                 op = Operator.PLUS;
 351                 break;
 352             case OPERATOR_MINUS:
 353                 match(OPERATOR_MINUS);
 354                 op = Operator.MINUS;
 355                 break;
 356             default:
 357                 e = primary();
 358                 log(pdebug, "Parsed: unary -> " + e);
 359                 return e;
 360             }
 361             Expression e1 = new Expression();
 362             e1.setOperator(op);
 363             e1.setRight(e);
 364             log(pdebug, "Parsed: unary -> " + e1);
 365             e1.setLeft(new Literal(Double.valueOf(0)));
 366             e = e1;
 367         }
 368     }
 369 
 370     /**
 371      *  MultExpression -> Unary (('*' | '/') Unary)*
 372      */
 373     private Expression multExpression() throws ParserException, IOException {
 374         Expression e = unary();
 375         Operator op = null;
 376 
 377         while (true) {
 378             switch (lookahead.ttype) {
 379             case OPERATOR_MULTIPLY:
 380                 match(OPERATOR_MULTIPLY);
 381                 op = Operator.MULTIPLY;
 382                 break;
 383             case OPERATOR_DIVIDE:
 384                 match(OPERATOR_DIVIDE);
 385                 op = Operator.DIVIDE;
 386                 break;
 387             default:
 388                 log(pdebug, "Parsed: multExpression -> " + e);
 389                 return e;
 390             }
 391             Expression e1 = new Expression();
 392             e1.setOperator(op);
 393             e1.setLeft(e);
 394             e1.setRight(unary());
 395             e = e1;
 396             log(pdebug, "Parsed: multExpression -> " + e);
 397         }
 398     }
 399 
 400     /**
 401      *  AddExpression -> MultExpression (('+' | '-') MultExpression)*
 402      */
 403     private Expression addExpression() throws ParserException, IOException {
 404         Expression e = multExpression();
 405         Operator op = null;
 406 
 407         while (true) {
 408             switch (lookahead.ttype) {
 409             case OPERATOR_PLUS:
 410                 match(OPERATOR_PLUS);
 411                 op = Operator.PLUS;
 412                 break;
 413             case OPERATOR_MINUS:
 414                 match(OPERATOR_MINUS);
 415                 op = Operator.MINUS;
 416                 break;
 417             default:
 418                 log(pdebug, "Parsed: addExpression -> " + e);
 419                 return e;
 420             }
 421             Expression e1 = new Expression();
 422             e1.setOperator(op);
 423             e1.setLeft(e);
 424             e1.setRight(multExpression());
 425             e = e1;
 426             log(pdebug, "Parsed: addExpression -> " + e);
 427         }
 428     }
 429 
 430     /**
 431      *  Expression -> AddExpression
 432      */
 433     private Expression expression() throws ParserException, IOException {
 434         Expression e = addExpression();
 435         log(pdebug, "Parsed: expression -> " + e);
 436         return e;
 437     }
 438 
 439     /**
 440      * datastmt -> 'data' expression
 441      */
 442     private void dataStmt(ColumnFormat cf) throws ParserException, IOException {
 443         match(DATA);
 444         Expression e = expression();
 445         cf.setExpression(e);
 446         log(pdebug, "Parsed: data -> " + e);
 447     }
 448 
 449     /**
 450      * requiredstmt -> 'required' expression
 451      */
 452     private void requiredStmt(ColumnFormat cf) throws ParserException, IOException {
 453         match(REQUIRED);
 454         Token t = matchOne(boolKeyWords);
 455         cf.setRequired(Boolean.parseBoolean(t.sval));
 456         log(pdebug, "Parsed: required -> " + cf.isRequired());
 457     }
 458 
 459     /**
 460      * statementlist -> optionalstmt statementlist
 461      * optionalstmt -> 'data' expression
 462      *                 'header' quotedstring
 463      *                 'width' integer
 464      *                 'format' formatstring
 465      *                 'align' alignspec
 466      *                 'scale' scalespec
 467      *                 'required' boolean
 468      */
 469     private void statementList(ColumnFormat cf)
 470                  throws ParserException, IOException {
 471         while (true) {
 472             if (lookahead.ttype != StreamTokenizer.TT_WORD) {
 473                 return;
 474             }
 475 
 476             if (lookahead.sval.compareTo(DATA) == 0) {
 477                 dataStmt(cf);
 478             } else if (lookahead.sval.compareTo(HEADER) == 0) {
 479                 headerStmt(cf);
 480             } else if (lookahead.sval.compareTo(WIDTH) == 0) {
 481                 widthStmt(cf);
 482             } else if (lookahead.sval.compareTo(FORMAT) == 0) {
 483                 formatStmt(cf);
 484             } else if (lookahead.sval.compareTo(ALIGN) == 0) {
 485                 alignStmt(cf);
 486             } else if (lookahead.sval.compareTo(SCALE) == 0) {
 487                 scaleStmt(cf);
 488             } else if (lookahead.sval.compareTo(REQUIRED) == 0) {
 489                 requiredStmt(cf);
 490             } else {
 491                 return;
 492             }
 493         }
 494     }
 495 
 496     /**
 497      * optionlist -> columspec optionlist
 498      *               null
 499      * columspec -> 'column' '{' statementlist '}'
 500      */
 501     private void optionList(OptionFormat of)
 502                  throws ParserException, IOException {
 503         while (true) {
 504             if (lookahead.ttype != StreamTokenizer.TT_WORD) {
 505                 return;
 506             }
 507 
 508             match(COLUMN);
 509             match(OPENBLOCK);
 510             ColumnFormat cf = new ColumnFormat(columnCount++);
 511             statementList(cf);
 512               match(CLOSEBLOCK);
 513             cf.validate();
 514             of.addSubFormat(cf);
 515         }
 516     }
 517 
 518     /**
 519      * optionstmt -> 'option' ID '{' optionlist '}'
 520      */
 521     private OptionFormat optionStmt() throws ParserException, IOException {
 522         match(OPTION);
 523         String optionName=lookahead.sval;
 524         matchID();
 525         match(OPENBLOCK);
 526         OptionFormat of = new OptionFormat(optionName);
 527         optionList(of);
 528         match(CLOSEBLOCK);
 529         return of;
 530     }
 531 
 532     /**
 533      * parse the specification for the given option identifier
 534      */
 535     public OptionFormat parse(String option)
 536                         throws ParserException, IOException {
 537         nextToken();
 538 
 539         /*
 540          * this search stops on the first occurance of an option
 541          * statement with a name matching the given option. Any
 542          * duplicate options are ignored.
 543          */
 544         while (lookahead.ttype != StreamTokenizer.TT_EOF) {
 545             // look for the start symbol
 546             if ((lookahead.ttype != StreamTokenizer.TT_WORD)
 547                     || (lookahead.sval.compareTo(START) != 0)) {
 548                 // skip tokens until a start symbol is found
 549                 nextToken();
 550                 continue;
 551             }
 552 
 553             // check if the option name is the one we are interested in
 554             match(START);
 555 
 556             if ((lookahead.ttype == StreamTokenizer.TT_WORD)
 557                     && (lookahead.sval.compareTo(option) == 0)) {
 558                 // this is the one we are looking for, parse it
 559                 pushBack();
 560                 return optionStmt();
 561             } else {
 562                 // not what we are looking for, start skipping tokens
 563                 nextToken();
 564             }
 565         }
 566         return null;
 567     }
 568 
 569     public Set<OptionFormat> parseOptions() throws ParserException, IOException {
 570         Set<OptionFormat> options = new HashSet<OptionFormat>();
 571 
 572         nextToken();
 573 
 574         while (lookahead.ttype != StreamTokenizer.TT_EOF) {
 575             // look for the start symbol
 576             if ((lookahead.ttype != StreamTokenizer.TT_WORD)
 577                     || (lookahead.sval.compareTo(START) != 0)) {
 578                 // skip tokens until a start symbol is found
 579                 nextToken();
 580                 continue;
 581             }
 582 
 583             // note: if a duplicate option statement exists, then
 584             // first one encountered is the chosen definition.
 585             OptionFormat of = optionStmt();
 586             options.add(of);
 587         }
 588         return options;
 589     }
 590 
 591     OptionFormat getOptionFormat() {
 592        return optionFormat;
 593     }
 594 
 595     private void log(boolean logging, String s) {
 596         if (logging) {
 597             System.out.println(s);
 598         }
 599     }
 600 }