2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2016-2018 Ericsson. All rights reserved.
4 * Modifications Copyright (C) 2019-2020 Nordix Foundation.
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.apex.auth.clieditor;
24 import static org.onap.policy.apex.model.utilities.TreeMapUtils.findMatchingEntries;
26 import java.io.BufferedReader;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStream;
32 import java.io.OutputStreamWriter;
33 import java.io.PrintWriter;
34 import java.util.AbstractMap.SimpleEntry;
35 import java.util.ArrayDeque;
36 import java.util.ArrayList;
37 import java.util.Iterator;
38 import java.util.List;
39 import java.util.Map.Entry;
40 import java.util.Properties;
41 import java.util.TreeMap;
42 import org.apache.commons.lang3.tuple.MutablePair;
43 import org.apache.commons.lang3.tuple.Pair;
44 import org.onap.policy.apex.model.modelapi.ApexApiResult;
45 import org.onap.policy.apex.model.modelapi.ApexApiResult.Result;
46 import org.onap.policy.apex.model.utilities.TreeMapUtils;
47 import org.onap.policy.common.utils.resources.TextFileUtils;
48 import org.slf4j.ext.XLogger;
49 import org.slf4j.ext.XLoggerFactory;
52 * This class implements the editor loop, the loop of execution that continuously executes commands until the quit
53 * command is issued or EOF is detected on input.
55 * @author Liam Fallon (liam.fallon@ericsson.com)
57 public class CommandLineEditorLoop {
59 // Get a reference to the logger
60 private static final XLogger LOGGER = XLoggerFactory.getXLogger(CommandLineEditorLoop.class);
62 // Recurring string constants
63 private static final String COMMAND = "command ";
64 private static final String COMMAND_LINE_ERROR = "command line error";
66 // The model handler that is handling the API towards the Apex model being editied
67 private final ApexModelHandler modelHandler;
69 // Holds the current location in the keyword hierarchy
70 private final ArrayDeque<KeywordNode> keywordNodeDeque = new ArrayDeque<>();
73 private final String logicBlockStartTag;
74 private final String logicBlockEndTag;
77 private final String macroFileTag;
80 * Initiate the loop with the keyword node tree.
82 * @param properties The CLI editor properties defined for execution
83 * @param modelHandler the model handler that will handle commands
84 * @param rootKeywordNode The root keyword node tree
86 public CommandLineEditorLoop(final Properties properties, final ApexModelHandler modelHandler,
87 final KeywordNode rootKeywordNode) {
88 this.modelHandler = modelHandler;
89 keywordNodeDeque.push(rootKeywordNode);
91 logicBlockStartTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_START_TAG");
92 logicBlockEndTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_END_TAG");
93 macroFileTag = properties.getProperty("DEFAULT_MACRO_FILE_TAG");
99 * @param inputStream The stream to read commands from
100 * @param outputStream The stream to write command output and messages to
101 * @param parameters The parameters for the CLI editor
102 * @return the exit code from command processing
103 * @throws IOException Thrown on exceptions on IO
105 public int runLoop(final InputStream inputStream, final OutputStream outputStream,
106 final CommandLineParameters parameters) throws IOException {
107 // Readers and writers for input and output
108 final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
109 final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));
111 // The parser parses the input lines into commands and arguments
112 final CommandLineParser parser = new CommandLineParser();
114 // The execution status has the result of the latest command and a cumulative error count
115 MutablePair<Result, Integer> executionStatus = new MutablePair<>(Result.SUCCESS, 0);
117 // The main loop for command handing, it continues until EOF on the input stream or until a
119 while (!endOfCommandExecution(executionStatus, parameters)) {
120 processIncomingCommands(parameters, reader, writer, parser, executionStatus);
123 // Get the output model
124 if (!parameters.isSuppressModelOutput()) {
125 final String modelString = modelHandler.writeModelToString(writer);
127 if (parameters.checkSetOutputModelFileName()) {
128 TextFileUtils.putStringAsTextFile(modelString, parameters.getOutputModelFileName());
130 writer.println(modelString);
137 return executionStatus.getRight();
141 * Check if the command processing loop has come to an end.
143 * @param executionStatus a pair containing the result of the last command and the accumulated error count
144 * @param parameters the input parameters for command execution
145 * @return true if the command processing loop should exit
147 private boolean endOfCommandExecution(Pair<Result, Integer> executionStatus, CommandLineParameters parameters) {
148 if (executionStatus.getLeft() == Result.FINISHED) {
152 return executionStatus.getRight() > 0 && !parameters.isIgnoreCommandFailures();
156 * Process the incoming commands one by one.
158 * @param parameters the parameters to the CLI editor
159 * @param reader the reader to read the logic block from
160 * @param writer the writer to write results and error messages on
161 * @param executionStatus the status of the logic block read
163 private void processIncomingCommands(final CommandLineParameters parameters, final BufferedReader reader,
164 final PrintWriter writer, final CommandLineParser parser, MutablePair<Result, Integer> executionStatus) {
167 // Output prompt and get a line of input
168 writer.print(getPrompt());
170 String line = reader.readLine();
172 executionStatus.setLeft(Result.FINISHED);
176 // Expand any macros in the script
177 while (line.contains(macroFileTag)) {
178 line = expandMacroFile(parameters, line);
181 if (parameters.isEcho()) {
182 writer.println(line);
185 String logicBlock = null;
186 if (line.trim().endsWith(logicBlockStartTag)) {
187 line = line.replace(logicBlockStartTag, "").trim();
189 logicBlock = readLogicBlock(parameters, reader, writer, executionStatus);
192 // Parse the line into a list of commands and arguments
193 final List<String> commandWords = parser.parse(line, logicBlock);
195 // Find the command, if the command is null, then we are simply changing position in
197 final CommandLineCommand command = findCommand(commandWords);
198 if (command != null) {
199 // Check the arguments of the command
200 final TreeMap<String, CommandLineArgumentValue> argumentValues = getArgumentValues(command,
203 // Execute the command, a FINISHED result means a command causes the loop to
205 executionStatus.setLeft(executeCommand(command, argumentValues, writer));
206 if (ApexApiResult.Result.isNok(executionStatus.getLeft())) {
207 executionStatus.setRight(executionStatus.getRight() + 1);
210 } catch (final CommandLineException e) {
211 // Print any error messages from command parsing and finding
212 writer.println(e.getMessage());
213 executionStatus.setRight(executionStatus.getRight() + 1);
214 LOGGER.debug(COMMAND_LINE_ERROR, e);
215 } catch (final Exception e) {
216 e.printStackTrace(writer);
217 LOGGER.error(COMMAND_LINE_ERROR, e);
222 * Read a logic block, a block of program logic for a policy.
224 * @param parameters the parameters to the CLI editor
225 * @param reader the reader to read the logic block from
226 * @param writer the writer to write results and error messages on
227 * @param executionStatus the status of the logic block read
228 * @return the result of the logic block read
230 private String readLogicBlock(final CommandLineParameters parameters, final BufferedReader reader,
231 final PrintWriter writer, MutablePair<Result, Integer> executionStatus) {
232 StringBuilder logicBlock = new StringBuilder();
236 String logicLine = reader.readLine();
237 if (logicLine == null) {
241 while (logicLine.contains(macroFileTag)) {
242 logicLine = expandMacroFile(parameters, logicLine);
245 if (parameters.isEcho()) {
246 writer.println(logicLine);
249 if (logicLine.trim().endsWith(logicBlockEndTag)) {
250 logicBlock.append(logicLine.replace(logicBlockEndTag, "").trim() + "\n");
251 return logicBlock.toString();
253 logicBlock.append(logicLine + "\n");
255 } catch (final CommandLineException e) {
256 // Print any error messages from command parsing and finding
257 writer.println(e.getMessage());
258 executionStatus.setRight(executionStatus.getRight() + 1);
259 LOGGER.debug(COMMAND_LINE_ERROR, e);
260 } catch (final Exception e) {
261 e.printStackTrace(writer);
262 LOGGER.error(COMMAND_LINE_ERROR, e);
268 * Output a prompt that indicates where in the keyword hierarchy we are.
270 * @return A string with the prompt
272 private String getPrompt() {
273 final StringBuilder builder = new StringBuilder();
274 final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();
276 while (keynodeDequeIter.hasNext()) {
278 builder.append(keynodeDequeIter.next().getKeyword());
280 builder.append("> ");
282 return builder.toString();
286 * Finds a command for the given input command words. Command words need only ne specified enough to uniquely
287 * identify them. Therefore, "p s o c" will find the command "policy state output create"
289 * @param commandWords The commands and arguments parsed from the command line by the parser
290 * @return The found command
293 private CommandLineCommand findCommand(final List<String> commandWords) {
294 CommandLineCommand command = null;
296 final KeywordNode startKeywordNode = keywordNodeDeque.peek();
298 // Go down through the keywords searching for the command
299 for (int i = 0; i < commandWords.size(); i++) {
300 final KeywordNode searchKeywordNode = keywordNodeDeque.peek();
302 // We have got to the arguments, time to stop looking
303 if (commandWords.get(i).indexOf('=') >= 0) {
304 unwindStack(startKeywordNode);
305 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
308 // If the node entries found is not equal to one, then we have either no command or more
309 // than one command matching
310 final List<Entry<String, KeywordNode>> foundNodeEntries = findMatchingEntries(
311 searchKeywordNode.getChildren(), commandWords.get(i));
312 if (foundNodeEntries.isEmpty()) {
313 unwindStack(startKeywordNode);
314 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
315 } else if (foundNodeEntries.size() > 1) {
316 unwindStack(startKeywordNode);
317 throw new CommandLineException(
318 "multiple commands matched: " + stringAL2String(commandWords) + " [" + nodeAL2String(
319 foundNodeEntries) + ']');
322 // Record the fully expanded command word
323 commandWords.set(i, foundNodeEntries.get(0).getKey());
325 // Check if there is a command
326 final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
327 command = childKeywordNode.getCommand();
329 // If the command is null, we go into a sub mode, otherwise we unwind the stack of
330 // commands and return the found command
331 if (command == null) {
332 keywordNodeDeque.push(childKeywordNode);
334 unwindStack(startKeywordNode);
343 * Unwind the stack of keyword node entries we have placed on the queue in a command search.
345 * @param startKeywordNode The point on the queue we want to unwind to
347 private void unwindStack(final KeywordNode startKeywordNode) {
350 if (keywordNodeDeque.peek().equals(startKeywordNode)) {
353 keywordNodeDeque.pop();
358 * Check the arguments of the command.
360 * @param command The command to check
361 * @param commandWords The command words entered
362 * @return the argument values
364 private TreeMap<String, CommandLineArgumentValue> getArgumentValues(final CommandLineCommand command,
365 final List<String> commandWords) {
366 final TreeMap<String, CommandLineArgumentValue> argumentValues = new TreeMap<>();
367 for (final CommandLineArgument argument : command.getArgumentList()) {
368 if (argument != null) {
369 argumentValues.put(argument.getArgumentName(), new CommandLineArgumentValue(argument));
373 // Set the value of the arguments
374 for (final Entry<String, String> argument : getCommandArguments(commandWords)) {
375 final List<Entry<String, CommandLineArgumentValue>> foundArguments = TreeMapUtils
376 .findMatchingEntries(argumentValues, argument.getKey());
377 if (foundArguments.isEmpty()) {
378 throw new CommandLineException(
379 COMMAND + stringAL2String(commandWords) + ": " + " argument \"" + argument.getKey()
380 + "\" not allowed on command");
381 } else if (foundArguments.size() > 1) {
382 throw new CommandLineException(COMMAND + stringAL2String(commandWords) + ": " + " argument " + argument
383 + " matches multiple arguments [" + argumentAL2String(foundArguments) + ']');
386 // Set the value of the argument, stripping off any quotes
387 final String argumentValue = argument.getValue().replaceAll("^\"", "").replaceAll("\"$", "");
388 foundArguments.get(0).getValue().setValue(argumentValue);
391 // Now check all mandatory arguments are set
392 for (final CommandLineArgumentValue argumentValue : argumentValues.values()) {
393 // Argument values are null by default so if this argument is not nullable it is
395 if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
396 throw new CommandLineException(
397 COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
398 .getCliArgument().getArgumentName() + "\" not specified");
402 return argumentValues;
406 * Get the arguments of the command, the command words have already been conditioned into an array starting with the
407 * command words and ending with the arguments as name=value tuples.
409 * @param commandWords The command words entered by the user
410 * @return the arguments as an entry array list
412 private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
413 final ArrayList<Entry<String, String>> arguments = new ArrayList<>();
415 // Iterate over the command words, arguments are of the format name=value
416 for (final String word : commandWords) {
417 final int equalsPos = word.indexOf('=');
420 .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
428 * Execute system and editor commands.
430 * @param command The command to execute
431 * @param argumentValues The arguments input on the command line to invoke the command
432 * @param writer The writer to use for any output from the command
433 * @return the result of execution of the command
435 private Result executeCommand(final CommandLineCommand command,
436 final TreeMap<String, CommandLineArgumentValue> argumentValues, final PrintWriter writer) {
437 if (command.isSystemCommand()) {
438 return exceuteSystemCommand(command, writer);
440 return modelHandler.executeCommand(command, argumentValues, writer);
445 * Execute system commands.
447 * @param command The command to execute
448 * @param writer The writer to use for any output from the command
449 * @return the result of execution of the command
451 private Result exceuteSystemCommand(final CommandLineCommand command, final PrintWriter writer) {
452 if ("back".equals(command.getName())) {
453 return executeBackCommand();
454 } else if ("help".equals(command.getName())) {
455 return executeHelpCommand(writer);
456 } else if ("quit".equals(command.getName())) {
457 return executeQuitCommand();
459 return Result.SUCCESS;
464 * Execute the "back" command.
466 * @return the result of execution of the command
468 private Result executeBackCommand() {
469 if (keywordNodeDeque.size() > 1) {
470 keywordNodeDeque.pop();
472 return Result.SUCCESS;
476 * Execute the "quit" command.
478 * @return the result of execution of the command
480 private Result executeQuitCommand() {
481 return Result.FINISHED;
485 * Execute the "help" command.
487 * @param writer The writer to use for output from the command
488 * @return the result of execution of the command
490 private Result executeHelpCommand(final PrintWriter writer) {
491 for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
492 writer.println(command.getHelp());
494 return Result.SUCCESS;
498 * Helper method to output an array list of keyword node entries to a string.
500 * @param nodeEntryArrayList the array list of keyword node entries
503 private String nodeAL2String(final List<Entry<String, KeywordNode>> nodeEntryArrayList) {
504 final ArrayList<String> stringArrayList = new ArrayList<>();
505 for (final Entry<String, KeywordNode> node : nodeEntryArrayList) {
506 stringArrayList.add(node.getValue().getKeyword());
509 return stringAL2String(stringArrayList);
513 * Helper method to output an array list of argument entries to a string.
515 * @param argumentArrayList the argument array list
518 private String argumentAL2String(final List<Entry<String, CommandLineArgumentValue>> argumentArrayList) {
519 final ArrayList<String> stringArrayList = new ArrayList<>();
520 for (final Entry<String, CommandLineArgumentValue> argument : argumentArrayList) {
521 stringArrayList.add(argument.getValue().getCliArgument().getArgumentName());
524 return stringAL2String(stringArrayList);
528 * Helper method to output an array list of strings to a string.
530 * @param stringArrayList the array list of strings
533 private String stringAL2String(final List<String> stringArrayList) {
534 final StringBuilder builder = new StringBuilder();
535 boolean first = true;
536 for (final String word : stringArrayList) {
542 builder.append(word);
545 return builder.toString();
549 * This method reads in the file from a file macro statement, expands the macro, and replaces the Macro tag in the
550 * line with the file contents.
552 * @param parameters The parameters for the CLI editor
553 * @param line The line with the macro keyword in it
554 * @return the expanded line
556 private String expandMacroFile(final CommandLineParameters parameters, final String line) {
557 final int macroTagPos = line.indexOf(macroFileTag);
559 // Get the line before and after the macro tag
560 final String lineBeforeMacroTag = line.substring(0, macroTagPos);
561 final String lineAfterMacroTag = line.substring(macroTagPos + macroFileTag.length()).replaceAll("^\\s*", "");
563 // Get the file name that is the argument of the Macro tag
564 final String[] lineWords = lineAfterMacroTag.split("\\s+");
566 if (lineWords.length == 0) {
567 throw new CommandLineException("no file name specified for Macro File Tag");
570 // Get the macro file name and the remainder of the line after the file name
571 String macroFileName = lineWords[0];
572 final String lineAfterMacroFileName = lineAfterMacroTag.replaceFirst(macroFileName, "");
574 if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
575 macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
577 throw new CommandLineException(
578 "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
581 // Append the working directory to the macro file name
582 macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;
584 // Now, get the text file for the argument of the macro
585 String macroFileContents = null;
587 macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
588 } catch (final IOException e) {
589 throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
592 return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;