1 /*
   2  * Copyright (c) 2016, 2019, 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 jdk.jfr.internal.tool;
  27 
  28 import java.io.File;
  29 import java.io.FileNotFoundException;
  30 import java.io.IOError;
  31 import java.io.IOException;
  32 import java.io.PrintStream;
  33 import java.io.RandomAccessFile;
  34 import java.nio.file.Files;
  35 import java.nio.file.InvalidPathException;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.util.ArrayList;
  39 import java.util.Collections;
  40 import java.util.Deque;
  41 import java.util.List;
  42 
  43 abstract class Command {
  44     public final static String title = "Tool for working with Flight Recorder files (.jfr)";
  45     private final static Command HELP = new Help();
  46     private final static List<Command> COMMANDS = createCommands();
  47 
  48     private static List<Command> createCommands() {
  49         List<Command> commands = new ArrayList<>();
  50         commands.add(new Print());
  51         commands.add(new Metadata());
  52         commands.add(new Summary());
  53         commands.add(new Assemble());
  54         commands.add(new Disassemble());
  55         commands.add(new Version());
  56         commands.add(HELP);
  57         return Collections.unmodifiableList(commands);
  58     }
  59 
  60     static void displayHelp() {
  61         System.out.println(title);
  62         System.out.println();
  63         displayAvailableCommands(System.out);
  64     }
  65 
  66     abstract public String getName();
  67 
  68     abstract public String getDescription();
  69 
  70     abstract public void execute(Deque<String> argList) throws UserSyntaxException, UserDataException;
  71 
  72     protected String getTitle() {
  73         return getDescription();
  74     }
  75 
  76     static void displayAvailableCommands(PrintStream stream) {
  77         boolean first = true;
  78         for (Command c : Command.COMMANDS) {
  79             if (!first) {
  80                 System.out.println();
  81             }
  82             displayCommand(stream, c);
  83             stream.println("     " + c.getDescription());
  84             first = false;
  85         }
  86     }
  87 
  88     protected static void displayCommand(PrintStream stream, Command c) {
  89         boolean firstSyntax = true;
  90         String alias = buildAlias(c);
  91         String initial = " jfr " + c.getName();
  92         for (String syntaxLine : c.getOptionSyntax()) {
  93             if (firstSyntax) {
  94                 if (syntaxLine.length() != 0) {
  95                    stream.println(initial + " " + syntaxLine + alias);
  96                 } else {
  97                    stream.println(initial + alias);
  98                 }
  99             } else {
 100                 for (int i = 0; i < initial.length(); i++) {
 101                     stream.print(" ");
 102                 }
 103                 stream.println(" " + syntaxLine);
 104             }
 105             firstSyntax = false;
 106         }
 107     }
 108 
 109     private static String buildAlias(Command c) {
 110         List<String> aliases = c.getAliases();
 111         if (aliases.isEmpty()) {
 112             return "";
 113         }
 114         StringBuilder sb = new StringBuilder();
 115         if (aliases.size() == 1) {
 116             sb.append(" (alias ");
 117             sb.append(aliases.get(0));
 118             sb.append(")");
 119             return sb.toString();
 120          }
 121          sb.append(" (aliases ");
 122          for (int i = 0; i< aliases.size(); i ++ ) {
 123              sb.append(aliases.get(i));
 124              if (i < aliases.size() -1) {
 125                  sb.append(", ");
 126              }
 127          }
 128          sb.append(")");
 129          return sb.toString();
 130     }
 131 
 132     public static List<Command> getCommands() {
 133         return COMMANDS;
 134     }
 135 
 136     public static Command valueOf(String commandName) {
 137         for (Command command : COMMANDS) {
 138             if (command.getName().equals(commandName)) {
 139                 return command;
 140             }
 141         }
 142         return null;
 143     }
 144 
 145     public List<String> getOptionSyntax() {
 146         return Collections.singletonList("");
 147     }
 148 
 149     public void displayOptionUsage(PrintStream stream) {
 150     }
 151 
 152     protected boolean acceptOption(Deque<String> options, String expected) throws UserSyntaxException {
 153         if (expected.equals(options.peek())) {
 154             if (options.size() < 2) {
 155                 throw new UserSyntaxException("missing value for " + options.peek());
 156             }
 157             options.remove();
 158             return true;
 159         }
 160         return false;
 161     }
 162 
 163     protected void warnForWildcardExpansion(String option, String filter) throws UserDataException {
 164         // Users should quote their wildcards to avoid expansion by the shell
 165         try {
 166             if (!filter.contains(File.pathSeparator)) {
 167                 Path p = Path.of(".", filter);
 168                 if (!Files.exists(p)) {
 169                     return;
 170                 }
 171             }
 172             throw new UserDataException("wildcards should be quoted, for example " + option + " \"Foo*\"");
 173         } catch (InvalidPathException ipe) {
 174             // ignore
 175         }
 176     }
 177 
 178     protected boolean acceptFilterOption(Deque<String> options, String expected) throws UserSyntaxException {
 179         if (!acceptOption(options, expected)) {
 180             return false;
 181         }
 182         if (options.isEmpty()) {
 183             throw new UserSyntaxException("missing filter after " + expected);
 184         }
 185         String filter = options.peek();
 186         if (filter.startsWith("--")) {
 187             throw new UserSyntaxException("missing filter after " + expected);
 188         }
 189         return true;
 190     }
 191 
 192     final protected void ensureMaxArgumentCount(Deque<String> options, int maxCount) throws UserSyntaxException {
 193         if (options.size() > maxCount) {
 194             throw new UserSyntaxException("too many arguments");
 195         }
 196     }
 197 
 198     final protected void ensureMinArgumentCount(Deque<String> options, int minCount) throws UserSyntaxException {
 199         if (options.size() < minCount) {
 200             throw new UserSyntaxException("too few arguments");
 201         }
 202     }
 203 
 204     final protected Path getDirectory(String pathText) throws UserDataException {
 205         try {
 206             Path path = Paths.get(pathText).toAbsolutePath();
 207             if (!Files.exists((path))) {
 208                 throw new UserDataException("directory does not exist, " + pathText);
 209             }
 210             if (!Files.isDirectory(path)) {
 211                 throw new UserDataException("path must be directory, " + pathText);
 212             }
 213             return path;
 214         } catch (InvalidPathException ipe) {
 215             throw new UserDataException("invalid path '" + pathText + "'");
 216         }
 217     }
 218 
 219     final protected Path getJFRInputFile(Deque<String> options) throws UserSyntaxException, UserDataException {
 220         if (options.isEmpty()) {
 221             throw new UserSyntaxException("missing file");
 222         }
 223         String file = options.removeLast();
 224         if (file.startsWith("--")) {
 225             throw new UserSyntaxException("missing file");
 226         }
 227         try {
 228             Path path = Paths.get(file).toAbsolutePath();
 229             ensureAccess(path);
 230             ensureJFRFile(path);
 231             return path;
 232         } catch (IOError ioe) {
 233             throw new UserDataException("i/o error reading file '" + file + "', " + ioe.getMessage());
 234         } catch (InvalidPathException ipe) {
 235             throw new UserDataException("invalid path '" + file + "'");
 236         }
 237     }
 238 
 239     private void ensureAccess(Path path) throws UserDataException {
 240         try (RandomAccessFile rad = new RandomAccessFile(path.toFile(), "r")) {
 241             if (rad.length() == 0) {
 242                 throw new UserDataException("file is empty '" + path + "'");
 243             }
 244             rad.read(); // try to read 1 byte
 245         } catch (FileNotFoundException e) {
 246             throw new UserDataException("could not open file " + e.getMessage());
 247         } catch (IOException e) {
 248             throw new UserDataException("i/o error reading file '" + path + "', " + e.getMessage());
 249         }
 250     }
 251 
 252     final protected void couldNotReadError(Path p, IOException e) throws UserDataException {
 253         throw new UserDataException("could not read recording at " + p.toAbsolutePath() + ". " + e.getMessage());
 254     }
 255 
 256     final protected Path ensureFileDoesNotExist(Path file) throws UserDataException {
 257         if (Files.exists(file)) {
 258             throw new UserDataException("file '" + file + "' already exists");
 259         }
 260         return file;
 261     }
 262 
 263     final protected void ensureJFRFile(Path path) throws UserDataException {
 264         if (!path.toString().endsWith(".jfr")) {
 265             throw new UserDataException("filename must end with '.jfr'");
 266         }
 267     }
 268 
 269     protected void displayUsage(PrintStream stream) {
 270         displayCommand(stream, this);
 271         stream.println();
 272         displayOptionUsage(stream);
 273     }
 274 
 275     final protected void println() {
 276         System.out.println();
 277     }
 278 
 279     final protected void print(String text) {
 280         System.out.print(text);
 281     }
 282 
 283     final protected void println(String text) {
 284         System.out.println(text);
 285     }
 286 
 287     final protected boolean matches(String command) {
 288         for (String s : getNames()) {
 289             if (s.equals(command)) {
 290                 return true;
 291             }
 292         }
 293         return false;
 294     }
 295 
 296     protected List<String> getAliases() {
 297         return Collections.emptyList();
 298     }
 299 
 300     public List<String> getNames() {
 301         List<String> names = new ArrayList<>();
 302         names.add(getName());
 303         names.addAll(getAliases());
 304         return names;
 305     }
 306 }