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;
43 import org.apache.commons.lang3.tuple.MutablePair;
44 import org.apache.commons.lang3.tuple.Pair;
45 import org.onap.policy.apex.model.modelapi.ApexApiResult;
46 import org.onap.policy.apex.model.modelapi.ApexApiResult.Result;
47 import org.onap.policy.apex.model.utilities.TreeMapUtils;
48 import org.onap.policy.common.utils.resources.TextFileUtils;
49 import org.slf4j.ext.XLogger;
50 import org.slf4j.ext.XLoggerFactory;
53 * This class implements the editor loop, the loop of execution that continuously executes commands until the quit
54 * command is issued or EOF is detected on input.
56 * @author Liam Fallon (liam.fallon@ericsson.com)
58 public class CommandLineEditorLoop {
60 // Get a reference to the logger
61 private static final XLogger LOGGER = XLoggerFactory.getXLogger(CommandLineEditorLoop.class);
63 // Recurring string constants
64 private static final String COMMAND = "command ";
65 private static final String COMMAND_LINE_ERROR = "command line error";
67 // The model handler that is handling the API towards the Apex model being editied
68 private final ApexModelHandler modelHandler;
70 // Holds the current location in the keyword hierarchy
71 private final ArrayDeque<KeywordNode> keywordNodeDeque = new ArrayDeque<>();
74 private final String logicBlockStartTag;
75 private final String logicBlockEndTag;
78 private final String macroFileTag;
81 * Initiate the loop with the keyword node tree.
83 * @param properties The CLI editor properties defined for execution
84 * @param modelHandler the model handler that will handle commands
85 * @param rootKeywordNode The root keyword node tree
87 public CommandLineEditorLoop(final Properties properties, final ApexModelHandler modelHandler,
88 final KeywordNode rootKeywordNode) {
89 this.modelHandler = modelHandler;
90 keywordNodeDeque.push(rootKeywordNode);
92 logicBlockStartTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_START_TAG");
93 logicBlockEndTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_END_TAG");
94 macroFileTag = properties.getProperty("DEFAULT_MACRO_FILE_TAG");
100 * @param inputStream The stream to read commands from
101 * @param outputStream The stream to write command output and messages to
102 * @param parameters The parameters for the CLI editor
103 * @return the exit code from command processing
104 * @throws IOException Thrown on exceptions on IO
106 public int runLoop(final InputStream inputStream, final OutputStream outputStream,
107 final CommandLineParameters parameters) throws IOException {
108 // Readers and writers for input and output
109 final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
110 final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));
112 // The parser parses the input lines into commands and arguments
113 final CommandLineParser parser = new CommandLineParser();
115 // The execution status has the result of the latest command and a cumulative error count
116 MutablePair<Result, Integer> executionStatus = new MutablePair<>(Result.SUCCESS, 0);
118 // The main loop for command handing, it continues until EOF on the input stream or until a
120 while (!endOfCommandExecution(executionStatus, parameters)) {
121 processIncomingCommands(parameters, reader, writer, parser, executionStatus);
124 // Get the output model
125 if (!parameters.isSuppressModelOutput()) {
126 final String modelString = modelHandler.writeModelToString(writer);
128 if (parameters.checkSetOutputModelFileName()) {
129 TextFileUtils.putStringAsTextFile(modelString, parameters.getOutputModelFileName());
131 writer.println(modelString);
138 return executionStatus.getRight();
142 * Check if the command processing loop has come to an end.
144 * @param executionStatus a pair containing the result of the last command and the accumulated error count
145 * @param parameters the input parameters for command execution
146 * @return true if the command processing loop should exit
148 private boolean endOfCommandExecution(Pair<Result, Integer> executionStatus, CommandLineParameters parameters) {
149 if (executionStatus.getLeft() == Result.FINISHED) {
153 return executionStatus.getRight() > 0 && !parameters.isIgnoreCommandFailures();
157 * Process the incoming commands one by one.
159 * @param parameters the parameters to the CLI editor
160 * @param reader the reader to read the logic block from
161 * @param writer the writer to write results and error messages on
162 * @param executionStatus the status of the logic block read
164 private void processIncomingCommands(final CommandLineParameters parameters, final BufferedReader reader,
165 final PrintWriter writer, final CommandLineParser parser, MutablePair<Result, Integer> executionStatus) {
168 // Output prompt and get a line of input
169 writer.print(getPrompt());
171 String line = reader.readLine();
173 executionStatus.setLeft(Result.FINISHED);
177 // Expand any macros in the script
178 while (line.contains(macroFileTag)) {
179 line = expandMacroFile(parameters, line);
182 if (parameters.isEcho()) {
183 writer.println(line);
186 String logicBlock = null;
187 if (line.trim().endsWith(logicBlockStartTag)) {
188 line = line.replace(logicBlockStartTag, "").trim();
190 logicBlock = readLogicBlock(parameters, reader, writer, executionStatus);
193 // Parse the line into a list of commands and arguments
194 final List<String> commandWords = parser.parse(line, logicBlock);
196 // Find the command, if the command is null, then we are simply changing position in
198 final CommandLineCommand command = findCommand(commandWords);
199 if (command != null) {
200 // Check the arguments of the command
201 final TreeMap<String, CommandLineArgumentValue> argumentValues = getArgumentValues(command,
204 // Execute the command, a FINISHED result means a command causes the loop to
206 executionStatus.setLeft(executeCommand(command, argumentValues, writer));
207 if (ApexApiResult.Result.isNok(executionStatus.getLeft())) {
208 executionStatus.setRight(executionStatus.getRight() + 1);
212 // Print any error messages from command parsing and finding
213 catch (final CommandLineException e) {
214 writer.println(e.getMessage());
215 executionStatus.setRight(executionStatus.getRight() + 1);
216 LOGGER.debug(COMMAND_LINE_ERROR, e);
217 } catch (final Exception e) {
218 e.printStackTrace(writer);
219 LOGGER.error(COMMAND_LINE_ERROR, e);
224 * Read a logic block, a block of program logic for a policy.
226 * @param parameters the parameters to the CLI editor
227 * @param reader the reader to read the logic block from
228 * @param writer the writer to write results and error messages on
229 * @param executionStatus the status of the logic block read
230 * @return the result of the logic block read
232 private String readLogicBlock(final CommandLineParameters parameters, final BufferedReader reader,
233 final PrintWriter writer, MutablePair<Result, Integer> executionStatus) {
234 StringBuilder logicBlock = new StringBuilder();
238 String logicLine = reader.readLine();
239 if (logicLine == null) {
243 while (logicLine.contains(macroFileTag)) {
244 logicLine = expandMacroFile(parameters, logicLine);
247 if (parameters.isEcho()) {
248 writer.println(logicLine);
251 if (logicLine.trim().endsWith(logicBlockEndTag)) {
252 logicBlock.append(logicLine.replace(logicBlockEndTag, "").trim() + "\n");
253 return logicBlock.toString();
255 logicBlock.append(logicLine + "\n");
258 // Print any error messages from command parsing and finding
259 catch (final CommandLineException e) {
260 writer.println(e.getMessage());
261 executionStatus.setRight(executionStatus.getRight() + 1);
262 LOGGER.debug(COMMAND_LINE_ERROR, e);
264 } catch (final Exception e) {
265 e.printStackTrace(writer);
266 LOGGER.error(COMMAND_LINE_ERROR, e);
272 * Output a prompt that indicates where in the keyword hierarchy we are.
274 * @return A string with the prompt
276 private String getPrompt() {
277 final StringBuilder builder = new StringBuilder();
278 final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();
280 while (keynodeDequeIter.hasNext()) {
282 builder.append(keynodeDequeIter.next().getKeyword());
284 builder.append("> ");
286 return builder.toString();
290 * Finds a command for the given input command words. Command words need only ne specified enough to uniquely
291 * identify them. Therefore, "p s o c" will find the command "policy state output create"
293 * @param commandWords The commands and arguments parsed from the command line by the parser
294 * @return The found command
297 private CommandLineCommand findCommand(final List<String> commandWords) {
298 CommandLineCommand command = null;
300 final KeywordNode startKeywordNode = keywordNodeDeque.peek();
302 // Go down through the keywords searching for the command
303 for (int i = 0; i < commandWords.size(); i++) {
304 final KeywordNode searchKeywordNode = keywordNodeDeque.peek();
306 // We have got to the arguments, time to stop looking
307 if (commandWords.get(i).indexOf('=') >= 0) {
308 unwindStack(startKeywordNode);
309 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
312 // If the node entries found is not equal to one, then we have either no command or more
313 // than one command matching
314 final List<Entry<String, KeywordNode>> foundNodeEntries = findMatchingEntries(
315 searchKeywordNode.getChildren(), commandWords.get(i));
316 if (foundNodeEntries.isEmpty()) {
317 unwindStack(startKeywordNode);
318 throw new CommandLineException("command not found: " + stringAL2String(commandWords));
319 } else if (foundNodeEntries.size() > 1) {
320 unwindStack(startKeywordNode);
321 throw new CommandLineException(
322 "multiple commands matched: " + stringAL2String(commandWords) + " [" + nodeAL2String(
323 foundNodeEntries) + ']');
326 // Record the fully expanded command word
327 commandWords.set(i, foundNodeEntries.get(0).getKey());
329 // Check if there is a command
330 final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
331 command = childKeywordNode.getCommand();
333 // If the command is null, we go into a sub mode, otherwise we unwind the stack of
334 // commands and return the found command
335 if (command == null) {
336 keywordNodeDeque.push(childKeywordNode);
338 unwindStack(startKeywordNode);
347 * Unwind the stack of keyword node entries we have placed on the queue in a command search.
349 * @param startKeywordNode The point on the queue we want to unwind to
351 private void unwindStack(final KeywordNode startKeywordNode) {
354 if (keywordNodeDeque.peek().equals(startKeywordNode)) {
357 keywordNodeDeque.pop();
362 * Check the arguments of the command.
364 * @param command The command to check
365 * @param commandWords The command words entered
366 * @return the argument values
368 private TreeMap<String, CommandLineArgumentValue> getArgumentValues(final CommandLineCommand command,
369 final List<String> commandWords) {
370 final TreeMap<String, CommandLineArgumentValue> argumentValues = new TreeMap<>();
371 for (final CommandLineArgument argument : command.getArgumentList()) {
372 if (argument != null) {
373 argumentValues.put(argument.getArgumentName(), new CommandLineArgumentValue(argument));
377 // Set the value of the arguments
378 for (final Entry<String, String> argument : getCommandArguments(commandWords)) {
379 final List<Entry<String, CommandLineArgumentValue>> foundArguments = TreeMapUtils
380 .findMatchingEntries(argumentValues, argument.getKey());
381 if (foundArguments.isEmpty()) {
382 throw new CommandLineException(
383 COMMAND + stringAL2String(commandWords) + ": " + " argument \"" + argument.getKey()
384 + "\" not allowed on command");
385 } else if (foundArguments.size() > 1) {
386 throw new CommandLineException(COMMAND + stringAL2String(commandWords) + ": " + " argument " + argument
387 + " matches multiple arguments [" + argumentAL2String(foundArguments) + ']');
390 // Set the value of the argument, stripping off any quotes
391 final String argumentValue = argument.getValue().replaceAll("^\"", "").replaceAll("\"$", "");
392 foundArguments.get(0).getValue().setValue(argumentValue);
395 // Now check all mandatory arguments are set
396 for (final CommandLineArgumentValue argumentValue : argumentValues.values()) {
397 // Argument values are null by default so if this argument is not nullable it is
399 if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
400 throw new CommandLineException(
401 COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
402 .getCliArgument().getArgumentName() + "\" not specified");
406 return argumentValues;
410 * Get the arguments of the command, the command words have already been conditioned into an array starting with the
411 * command words and ending with the arguments as name=value tuples.
413 * @param commandWords The command words entered by the user
414 * @return the arguments as an entry array list
416 private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
417 final ArrayList<Entry<String, String>> arguments = new ArrayList<>();
419 // Iterate over the command words, arguments are of the format name=value
420 for (final String word : commandWords) {
421 final int equalsPos = word.indexOf('=');
424 .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
432 * Execute system and editor commands.
434 * @param command The command to execute
435 * @param argumentValues The arguments input on the command line to invoke the command
436 * @param writer The writer to use for any output from the command
437 * @return the result of execution of the command
439 private Result executeCommand(final CommandLineCommand command,
440 final TreeMap<String, CommandLineArgumentValue> argumentValues, final PrintWriter writer) {
441 if (command.isSystemCommand()) {
442 return exceuteSystemCommand(command, writer);
444 return modelHandler.executeCommand(command, argumentValues, writer);
449 * Execute system commands.
451 * @param command The command to execute
452 * @param writer The writer to use for any output from the command
453 * @return the result of execution of the command
455 private Result exceuteSystemCommand(final CommandLineCommand command, final PrintWriter writer) {
456 if ("back".equals(command.getName())) {
457 return executeBackCommand();
458 } else if ("help".equals(command.getName())) {
459 return executeHelpCommand(writer);
460 } else if ("quit".equals(command.getName())) {
461 return executeQuitCommand();
463 return Result.SUCCESS;
468 * Execute the "back" command.
470 * @return the result of execution of the command
472 private Result executeBackCommand() {
473 if (keywordNodeDeque.size() > 1) {
474 keywordNodeDeque.pop();
476 return Result.SUCCESS;
480 * Execute the "quit" command.
482 * @return the result of execution of the command
484 private Result executeQuitCommand() {
485 return Result.FINISHED;
489 * Execute the "help" command.
491 * @param writer The writer to use for output from the command
492 * @return the result of execution of the command
494 private Result executeHelpCommand(final PrintWriter writer) {
495 for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
496 writer.println(command.getHelp());
498 return Result.SUCCESS;
502 * Helper method to output an array list of keyword node entries to a string.
504 * @param nodeEntryArrayList the array list of keyword node entries
507 private String nodeAL2String(final List<Entry<String, KeywordNode>> nodeEntryArrayList) {
508 final ArrayList<String> stringArrayList = new ArrayList<>();
509 for (final Entry<String, KeywordNode> node : nodeEntryArrayList) {
510 stringArrayList.add(node.getValue().getKeyword());
513 return stringAL2String(stringArrayList);
517 * Helper method to output an array list of argument entries to a string.
519 * @param argumentArrayList the argument array list
522 private String argumentAL2String(final List<Entry<String, CommandLineArgumentValue>> argumentArrayList) {
523 final ArrayList<String> stringArrayList = new ArrayList<>();
524 for (final Entry<String, CommandLineArgumentValue> argument : argumentArrayList) {
525 stringArrayList.add(argument.getValue().getCliArgument().getArgumentName());
528 return stringAL2String(stringArrayList);
532 * Helper method to output an array list of strings to a string.
534 * @param stringArrayList the array list of strings
537 private String stringAL2String(final List<String> stringArrayList) {
538 final StringBuilder builder = new StringBuilder();
539 boolean first = true;
540 for (final String word : stringArrayList) {
546 builder.append(word);
549 return builder.toString();
553 * This method reads in the file from a file macro statement, expands the macro, and replaces the Macro tag in the
554 * line with the file contents.
556 * @param parameters The parameters for the CLI editor
557 * @param line The line with the macro keyword in it
558 * @return the expanded line
560 private String expandMacroFile(final CommandLineParameters parameters, final String line) {
561 final int macroTagPos = line.indexOf(macroFileTag);
563 // Get the line before and after the macro tag
564 final String lineBeforeMacroTag = line.substring(0, macroTagPos);
565 final String lineAfterMacroTag = line.substring(macroTagPos + macroFileTag.length()).replaceAll("^\\s*", "");
567 // Get the file name that is the argument of the Macro tag
568 final String[] lineWords = lineAfterMacroTag.split("\\s+");
570 if (lineWords.length == 0) {
571 throw new CommandLineException("no file name specified for Macro File Tag");
574 // Get the macro file name and the remainder of the line after the file name
575 String macroFileName = lineWords[0];
576 final String lineAfterMacroFileName = lineAfterMacroTag.replaceFirst(macroFileName, "");
578 if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
579 macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
581 throw new CommandLineException(
582 "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
585 // Append the working directory to the macro file name
586 macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;
588 // Now, get the text file for the argument of the macro
589 String macroFileContents = null;
591 macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
592 } catch (final IOException e) {
593 throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
596 return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;