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 
Parser(String filename)92     public Parser(String filename) throws FileNotFoundException {
93         this.filename = filename;
94         Reader r = new BufferedReader(new FileReader(filename));
95     }
96 
Parser(Reader r)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      */
pushBack()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      */
nextToken()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      */
matchOne(Set<String> keyWords)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      */
match(int ttype, String token)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      */
match(int ttype)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      */
match(char ttype)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      */
matchQuotedString()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      */
matchNumber()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      */
matchID()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      */
match(String token)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      */
isReservedWord(String word)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      */
isInfixOperator(char op)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      */
scaleStmt(ColumnFormat cf)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      */
alignStmt(ColumnFormat cf)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      */
headerStmt(ColumnFormat cf)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      */
widthStmt(ColumnFormat cf)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      */
formatStmt(ColumnFormat cf)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      */
primary()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      */
unary()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      */
multExpression()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      */
addExpression()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      */
expression()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      */
dataStmt(ColumnFormat cf)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      */
requiredStmt(ColumnFormat cf)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      */
statementList(ColumnFormat cf)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      */
optionList(OptionFormat of)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      */
optionStmt()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      */
parse(String option)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 
parseOptions()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 
getOptionFormat()591     OptionFormat getOptionFormat() {
592        return optionFormat;
593     }
594 
log(boolean logging, String s)595     private void log(boolean logging, String s) {
596         if (logging) {
597             System.out.println(s);
598         }
599     }
600 }
601