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);
261 } catch (final Exception e) {
262 e.printStackTrace(writer);
263 LOGGER.error(COMMAND_LINE_ERROR, e);
269 * Output a prompt that indicates where in the keyword hierarchy we are.
271 * @return A string with the prompt
273 private String getPrompt() {
274 final StringBuilder builder = new StringBuilder();
275 final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();
277 while (keynodeDequeIter.hasNext()) {
279 builder.append(keynodeDequeIter.next().getKeyword());
281 builder.append("> ");
283 return builder.toString();
287 * Finds a command for the given input command words. Command words need only ne specified enough to uniquely
288 * identify them. Therefore, "p s o c" will find the command "policy state output create"
290 * @param commandWords The commands and arguments parsed from the command line by the parser
291 * @return The found command
294 private CommandLineCommand findCommand(final List<String> commandWords) {
295 CommandLineCommand command = null;
297 final KeywordNode startKeywordNode = keywordNodeDeque.peek();
299 // Go down through the keywords searching for the command
300 for (int i = 0; i < commandWords.size(); i++) {
301 final KeywordNode searchKeywordNode = keywordNodeDeque.peek();
303 // We have got to the arguments, time to stop looking
304 if (commandWords.get(i).indexOf('=') >= 0) {
305 unwindStack(startKeywordNode);
306 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
309 // If the node entries found is not equal to one, then we have either no command or more
310 // than one command matching
311 final List<Entry<String, KeywordNode>> foundNodeEntries = findMatchingEntries(
312 searchKeywordNode.getChildren(), commandWords.get(i));
313 if (foundNodeEntries.isEmpty()) {
314 unwindStack(startKeywordNode);
315 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
316 } else if (foundNodeEntries.size() > 1) {
317 unwindStack(startKeywordNode);
318 throw new CommandLineException(
319 "multiple commands matched: " + stringAL2String(commandWords) + " [" + nodeAL2String(
320 foundNodeEntries) + ']');
323 // Record the fully expanded command word
324 commandWords.set(i, foundNodeEntries.get(0).getKey());
326 // Check if there is a command
327 final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
328 command = childKeywordNode.getCommand();
330 // If the command is null, we go into a sub mode, otherwise we unwind the stack of
331 // commands and return the found command
332 if (command == null) {
333 keywordNodeDeque.push(childKeywordNode);
335 unwindStack(startKeywordNode);
344 * Unwind the stack of keyword node entries we have placed on the queue in a command search.
346 * @param startKeywordNode The point on the queue we want to unwind to
348 private void unwindStack(final KeywordNode startKeywordNode) {
351 if (keywordNodeDeque.peek().equals(startKeywordNode)) {
354 keywordNodeDeque.pop();
359 * Check the arguments of the command.
361 * @param command The command to check
362 * @param commandWords The command words entered
363 * @return the argument values
365 private TreeMap<String, CommandLineArgumentValue> getArgumentValues(final CommandLineCommand command,
366 final List<String> commandWords) {
367 final TreeMap<String, CommandLineArgumentValue> argumentValues = new TreeMap<>();
368 for (final CommandLineArgument argument : command.getArgumentList()) {
369 if (argument != null) {
370 argumentValues.put(argument.getArgumentName(), new CommandLineArgumentValue(argument));
374 // Set the value of the arguments
375 for (final Entry<String, String> argument : getCommandArguments(commandWords)) {
376 final List<Entry<String, CommandLineArgumentValue>> foundArguments = TreeMapUtils
377 .findMatchingEntries(argumentValues, argument.getKey());
378 if (foundArguments.isEmpty()) {
379 throw new CommandLineException(
380 COMMAND + stringAL2String(commandWords) + ": " + " argument \"" + argument.getKey()
381 + "\" not allowed on command");
382 } else if (foundArguments.size() > 1) {
383 throw new CommandLineException(COMMAND + stringAL2String(commandWords) + ": " + " argument " + argument
384 + " matches multiple arguments [" + argumentAL2String(foundArguments) + ']');
387 // Set the value of the argument, stripping off any quotes
388 final String argumentValue = argument.getValue().replaceAll("^\"", "").replaceAll("\"$", "");
389 foundArguments.get(0).getValue().setValue(argumentValue);
392 // Now check all mandatory arguments are set
393 for (final CommandLineArgumentValue argumentValue : argumentValues.values()) {
394 // Argument values are null by default so if this argument is not nullable it is
396 if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
397 throw new CommandLineException(
398 COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
399 .getCliArgument().getArgumentName() + "\" not specified");
403 return argumentValues;
407 * Get the arguments of the command, the command words have already been conditioned into an array starting with the
408 * command words and ending with the arguments as name=value tuples.
410 * @param commandWords The command words entered by the user
411 * @return the arguments as an entry array list
413 private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
414 final ArrayList<Entry<String, String>> arguments = new ArrayList<>();
416 // Iterate over the command words, arguments are of the format name=value
417 for (final String word : commandWords) {
418 final int equalsPos = word.indexOf('=');
421 .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
429 * Execute system and editor commands.
431 * @param command The command to execute
432 * @param argumentValues The arguments input on the command line to invoke the command
433 * @param writer The writer to use for any output from the command
434 * @return the result of execution of the command
436 private Result executeCommand(final CommandLineCommand command,
437 final TreeMap<String, CommandLineArgumentValue> argumentValues, final PrintWriter writer) {
438 if (command.isSystemCommand()) {
439 return exceuteSystemCommand(command, writer);
441 return modelHandler.executeCommand(command, argumentValues, writer);
446 * Execute system commands.
448 * @param command The command to execute
449 * @param writer The writer to use for any output from the command
450 * @return the result of execution of the command
452 private Result exceuteSystemCommand(final CommandLineCommand command, final PrintWriter writer) {
453 if ("back".equals(command.getName())) {
454 return executeBackCommand();
455 } else if ("help".equals(command.getName())) {
456 return executeHelpCommand(writer);
457 } else if ("quit".equals(command.getName())) {
458 return executeQuitCommand();
460 return Result.SUCCESS;
465 * Execute the "back" command.
467 * @return the result of execution of the command
469 private Result executeBackCommand() {
470 if (keywordNodeDeque.size() > 1) {
471 keywordNodeDeque.pop();
473 return Result.SUCCESS;
477 * Execute the "quit" command.
479 * @return the result of execution of the command
481 private Result executeQuitCommand() {
482 return Result.FINISHED;
486 * Execute the "help" command.
488 * @param writer The writer to use for output from the command
489 * @return the result of execution of the command
491 private Result executeHelpCommand(final PrintWriter writer) {
492 for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
493 writer.println(command.getHelp());
495 return Result.SUCCESS;
499 * Helper method to output an array list of keyword node entries to a string.
501 * @param nodeEntryArrayList the array list of keyword node entries
504 private String nodeAL2String(final List<Entry<String, KeywordNode>> nodeEntryArrayList) {
505 final ArrayList<String> stringArrayList = new ArrayList<>();
506 for (final Entry<String, KeywordNode> node : nodeEntryArrayList) {
507 stringArrayList.add(node.getValue().getKeyword());
510 return stringAL2String(stringArrayList);
514 * Helper method to output an array list of argument entries to a string.
516 * @param argumentArrayList the argument array list
519 private String argumentAL2String(final List<Entry<String, CommandLineArgumentValue>> argumentArrayList) {
520 final ArrayList<String> stringArrayList = new ArrayList<>();
521 for (final Entry<String, CommandLineArgumentValue> argument : argumentArrayList) {
522 stringArrayList.add(argument.getValue().getCliArgument().getArgumentName());
525 return stringAL2String(stringArrayList);
529 * Helper method to output an array list of strings to a string.
531 * @param stringArrayList the array list of strings
534 private String stringAL2String(final List<String> stringArrayList) {
535 final StringBuilder builder = new StringBuilder();
536 boolean first = true;
537 for (final String word : stringArrayList) {
543 builder.append(word);
546 return builder.toString();
550 * This method reads in the file from a file macro statement, expands the macro, and replaces the Macro tag in the
551 * line with the file contents.
553 * @param parameters The parameters for the CLI editor
554 * @param line The line with the macro keyword in it
555 * @return the expanded line
557 private String expandMacroFile(final CommandLineParameters parameters, final String line) {
558 final int macroTagPos = line.indexOf(macroFileTag);
560 // Get the line before and after the macro tag
561 final String lineBeforeMacroTag = line.substring(0, macroTagPos);
562 final String lineAfterMacroTag = line.substring(macroTagPos + macroFileTag.length()).replaceAll("^\\s*", "");
564 // Get the file name that is the argument of the Macro tag
565 final String[] lineWords = lineAfterMacroTag.split("\\s+");
567 if (lineWords.length == 0) {
568 throw new CommandLineException("no file name specified for Macro File Tag");
571 // Get the macro file name and the remainder of the line after the file name
572 String macroFileName = lineWords[0];
573 final String lineAfterMacroFileName = lineAfterMacroTag.replaceFirst(macroFileName, "");
575 if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
576 macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
578 throw new CommandLineException(
579 "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
582 // Append the working directory to the macro file name
583 macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;
585 // Now, get the text file for the argument of the macro
586 String macroFileContents = null;
588 macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
589 } catch (final IOException e) {
590 throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
593 return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;