/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.command.internal; import java.io.File; import java.util.ArrayList; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; import de.rcenvironment.core.command.api.CommandExecutionResult; import de.rcenvironment.core.command.common.CommandException; import de.rcenvironment.core.command.spi.CommandContext; import de.rcenvironment.core.command.spi.SingleCommandHandler; import de.rcenvironment.core.utils.common.textstream.TextOutputReceiver; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Handler/parser for a RCE multi-command. A multi-command may contain multiple commands which should be executed sequentially and/or in * parallel. The kind of execution is controlled by the structure of the command string. * * NOTE: Currently, only sequential execution is supported. Parallel execution will be added as needed. * * The current use cases are invocation via * <ul> * <li>the RCE command line, e.g.: <b>./rce --headless --exec "<command string>"</b></li> * <li>the OSGi console, e.g.: OSGi> <b>rce <command string></b></li> * </ul> * * The command is passed to this class as a list of tokens. The details of string tokenization are left to the caller. Note that usually, * the individual command parameters avoid the use of spaces; therefore, tokenization by spaces is usually sufficient. * * @author Robert Mischke * @author Tobias Rodehutskors (Injection of the FileLogger) */ public class MultiCommandHandler implements Callable<CommandExecutionResult> { // /** // * The command string of the 'saveto' command. // */ // public static final String SAVETO = "saveto"; // // /** // * TODO This function should be placed in a helper class FileUtils. // * // * These Strings are not allowed as filenames on the Windows platform. // */ // private static final String[] FORBIDDEN_FILENAMES = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", // "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; private static final String DOUBLE_QUOTE = "\""; private static final String ESCAPED_DOUBLE_QUOTE = "\\\""; // private static final String MIRROR = "-m"; // // private static final String AUTO = "--auto"; /** * Example of sequential execution syntax: <code>"command1 param1 ; command2 param2a param2b"</code>. Note the token separators (in this * case, spaces) around the actual separator. */ private static final String SEQUENTIAL_EXECUTION_SEPARATOR = ";"; private final List<String> rawTokens; private final Deque<String> remainingTokens; private TextOutputReceiver outputReceiver; private final SingleCommandHandler singleCommandHandler; private volatile Object initiatorInformation; // private final File profileOutputDirectory; public MultiCommandHandler(List<String> tokens, TextOutputReceiver outputReceiver, SingleCommandHandler singleCommandHandler, File profileOutput) { this.rawTokens = tokens; this.remainingTokens = new LinkedList<String>(); // empty list; filled after normalization this.outputReceiver = outputReceiver; this.singleCommandHandler = singleCommandHandler; // this.profileOutputDirectory = profileOutput; } /** * This constructor is manly intended for easing testing. As it does not specify the location of the profile output directory, this * class will return an error if the 'saveto' command is encountered during command execution. */ public MultiCommandHandler(List<String> tokens, TextOutputReceiver outputReceiver, SingleCommandHandler singleCommandHandler) { this(tokens, outputReceiver, singleCommandHandler, null); } /** * Executes the command provided via the constructor. * * @return the result of this multi-command invocation */ @TaskDescription("Text command execution") @Override public CommandExecutionResult call() { outputReceiver.onStart(); // outputReceiver.addOutput("Pre: " + rawTokens); try { List<String> normalizedTokens = normalizeTokens(rawTokens); remainingTokens.addAll(normalizedTokens); } catch (IllegalArgumentException e) { // TODO use onFatalError() instead? outputReceiver.addOutput("Syntax Error: " + e.getMessage()); outputReceiver.onFinished(); return CommandExecutionResult.ERROR; } // outputReceiver.addOutput("Post: " + remainingTokens); if (remainingTokens.isEmpty()) { // empty command string -> trigger help output CommandContext context = new CommandContext(new ArrayList<>(remainingTokens), outputReceiver, initiatorInformation); outputReceiver.onFatalError(CommandException.requestHelp(context)); return CommandExecutionResult.DEFAULT; // TODO add result key for this? } try { // // we cannot invoke the file logger earlier, since we need to check the provided commands, which needs to be normalized first // injectFileLoggerIfNecessary(); List<String> collectedTokens = new LinkedList<String>(); String token; do { token = getNextToken(); if (token == null || token.equals(SEQUENTIAL_EXECUTION_SEPARATOR)) { processSequentialPart(collectedTokens); // reset collectedTokens = new LinkedList<String>(); } else { collectedTokens.add(token); } } while (token != null); outputReceiver.onFinished(); return CommandExecutionResult.DEFAULT; } catch (CommandException e) { outputReceiver.onFatalError(e); return CommandExecutionResult.ERROR; } } // /** // * TODO This function should be placed in a helper class FileUtils. // * // * This methods checks if a filename was supplied, which does not use directory traversal. // */ // private static boolean isValidFilename(File profileOutputDirectory2, String filename) { // if (filename == null) { // return false; // } // // // check for directory traversal // if (!FilenameUtils.getName(filename).equals(filename)) { // return false; // } // // if (Arrays.asList(FORBIDDEN_FILENAMES).contains(filename)) { // return false; // } // // return true; // } // /** // * Parses the first tokens to check whether 'saveto [-m] (<filename>|--auto) <command(s)>' was specified. If this is the case, it // either // * replaces the original output receiver with a {@link FileLoggingTextOutputReceiver} or adds a {@link FileLoggingTextOutputReceiver} // to // * the original receiver depending on the presence of the mirror option. // * // * @throws CommandException // */ // private void injectFileLoggerIfNecessary() throws CommandException { // // check if 'safeto' is specified and all necessary options are given // if (SAVETO.equals(remainingTokens.peekFirst())) { // List<String> collectedTokens = new LinkedList<String>(); // collectedTokens.add(remainingTokens.pollFirst()); // // boolean mirror = false; // // // check if mirror option is set // if (MIRROR.equals(remainingTokens.peekFirst())) { // collectedTokens.add(remainingTokens.pollFirst()); // mirror = true; // } // // String filename = remainingTokens.peekFirst(); // collectedTokens.add(remainingTokens.pollFirst()); // CommandContext commandContext = new CommandContext(collectedTokens, outputReceiver, initiatorInformation); // // if (profileOutputDirectory == null) { // throw CommandException.executionError("Internal Error: The profile output directory is unkown.", commandContext); // } // // if (AUTO.equals(filename)) { // // It is unlikely that this file already exists // filename = "cmd_" + System.currentTimeMillis() + ".txt"; // } // // if (!isValidFilename(profileOutputDirectory, filename)) { // throw CommandException.syntaxError("You either need to specify the '--auto' option or supply a valid filename.", // commandContext); // } // // // construct the path to the file and create it // Path filePath = profileOutputDirectory.toPath().resolve(filename); // try { // filePath = Files.createFile(filePath); // // success // } catch (FileAlreadyExistsException e) { // throw CommandException.executionError("This file already exists. Please choose another file.", commandContext); // } catch (IOException e) { // throw CommandException.executionError( // "Encountered an IO error. Please try again with another file." + System.lineSeparator() + "IO error: " + e.toString(), // commandContext); // } // // injectFileLogger(filePath, mirror); // } // } // // private void injectFileLogger(Path file, boolean mirror) { // FileLoggingTextOutputReceiver fileLogger = new FileLoggingTextOutputReceiver(file); // fileLogger.onStart(); // // if (mirror) { // // forward the received text to the original receiver and to the file logger // MultiTextOutputReceiver multiOutputReceiver = new MultiTextOutputReceiver(); // multiOutputReceiver.addTextOutputReceiver(fileLogger); // multiOutputReceiver.addTextOutputReceiver(outputReceiver); // outputReceiver = multiOutputReceiver; // } else { // // replace the original receiver with the file logger // outputReceiver.onFinished(); // outputReceiver = fileLogger; // } // } /** * Retrieves information about the source that invoked this command; see {@link #setInitiatorInformation(Object)}. * * @return the information object */ public Object getInitiatorInformation() { return initiatorInformation; } /** * Attaches an arbitrary object to transport information about the source that invoked this command. * * @param shellAccountInformation the information object */ public void setInitiatorInformation(Object shellAccountInformation) { this.initiatorInformation = shellAccountInformation; } private List<String> normalizeTokens(List<String> input) { List<String> output = new ArrayList<>(); String quotedPartBuffer = null; for (String token : input) { if (token.isEmpty()) { continue; } if (quotedPartBuffer == null) { if (token.startsWith(DOUBLE_QUOTE) && !token.equals("\"")) { if (token.endsWith(DOUBLE_QUOTE) && !token.endsWith(ESCAPED_DOUBLE_QUOTE)) { // self-contained quoted part: unwrap and add to output output.add(unescapeQuotes(token.substring(1, token.length() - 1))); } else { // start new quoted part quotedPartBuffer = token.substring(1); } } else { // nothing special, just add to new list output.add(unescapeQuotes(token)); } } else { if (token.endsWith(DOUBLE_QUOTE) && !token.endsWith(ESCAPED_DOUBLE_QUOTE)) { // end of quoted part quotedPartBuffer += " " + token.substring(0, token.length() - 1); output.add(unescapeQuotes(quotedPartBuffer)); quotedPartBuffer = null; } else { // quoted part continued // TODO check for nested quoted parts? quotedPartBuffer += " " + token; } } } if (quotedPartBuffer != null) { throw new IllegalArgumentException("Unfinished quoted command part: " + quotedPartBuffer); } return output; } private String unescapeQuotes(String substring) { return substring.replace(ESCAPED_DOUBLE_QUOTE, DOUBLE_QUOTE); } private String getNextToken() { return remainingTokens.pollFirst(); } private void processSequentialPart(List<String> tokens) throws CommandException { if (tokens.isEmpty()) { // ignore if empty return; } // TODO parse for parallel sections executeSingleCommand(tokens); } private void executeSingleCommand(List<String> tokens) throws CommandException { CommandContext commandContext = new CommandContext(tokens, outputReceiver, initiatorInformation); singleCommandHandler.execute(commandContext); } }