d882b1a21ddd36c1398a044de31cd06046018c92
[policy/apex-pdp.git] / auth / cli-editor / src / main / java / org / onap / policy / apex / auth / clieditor / CommandLineEditorLoop.java
1 /*-
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
9  *
10  *      http://www.apache.org/licenses/LICENSE-2.0
11  *
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.
17  *
18  * SPDX-License-Identifier: Apache-2.0
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.onap.policy.apex.auth.clieditor;
23
24 import static org.onap.policy.apex.model.utilities.TreeMapUtils.findMatchingEntries;
25
26 import java.io.BufferedReader;
27 import java.io.File;
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
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;
51
52 /**
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.
55  *
56  * @author Liam Fallon (liam.fallon@ericsson.com)
57  */
58 public class CommandLineEditorLoop {
59
60     // Get a reference to the logger
61     private static final XLogger LOGGER = XLoggerFactory.getXLogger(CommandLineEditorLoop.class);
62
63     // Recurring string constants
64     private static final String COMMAND = "command ";
65     private static final String COMMAND_LINE_ERROR = "command line error";
66
67     // The model handler that is handling the API towards the Apex model being editied
68     private final ApexModelHandler modelHandler;
69
70     // Holds the current location in the keyword hierarchy
71     private final ArrayDeque<KeywordNode> keywordNodeDeque = new ArrayDeque<>();
72
73     // Logic block tags
74     private final String logicBlockStartTag;
75     private final String logicBlockEndTag;
76
77     // File Macro tag
78     private final String macroFileTag;
79
80     /**
81      * Initiate the loop with the keyword node tree.
82      *
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
86      */
87     public CommandLineEditorLoop(final Properties properties, final ApexModelHandler modelHandler,
88         final KeywordNode rootKeywordNode) {
89         this.modelHandler = modelHandler;
90         keywordNodeDeque.push(rootKeywordNode);
91
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");
95     }
96
97     /**
98      * Run a command loop.
99      *
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
105      */
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));
111
112         // The parser parses the input lines into commands and arguments
113         final CommandLineParser parser = new CommandLineParser();
114
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);
117
118         // The main loop for command handing, it continues until EOF on the input stream or until a
119         // quit command
120         while (!endOfCommandExecution(executionStatus, parameters)) {
121             processIncomingCommands(parameters, reader, writer, parser, executionStatus);
122         }
123
124         // Get the output model
125         if (!parameters.isSuppressModelOutput()) {
126             final String modelString = modelHandler.writeModelToString(writer);
127
128             if (parameters.checkSetOutputModelFileName()) {
129                 TextFileUtils.putStringAsTextFile(modelString, parameters.getOutputModelFileName());
130             } else {
131                 writer.println(modelString);
132             }
133         }
134
135         reader.close();
136         writer.close();
137
138         return executionStatus.getRight();
139     }
140
141     /**
142      * Check if the command processing loop has come to an end.
143      *
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
147      */
148     private boolean endOfCommandExecution(Pair<Result, Integer> executionStatus, CommandLineParameters parameters) {
149         if (executionStatus.getLeft() == Result.FINISHED) {
150             return true;
151         }
152
153         return executionStatus.getRight() > 0 && !parameters.isIgnoreCommandFailures();
154     }
155
156     /**
157      * Process the incoming commands one by one.
158      *
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
163      */
164     private void processIncomingCommands(final CommandLineParameters parameters, final BufferedReader reader,
165         final PrintWriter writer, final CommandLineParser parser, MutablePair<Result, Integer> executionStatus) {
166
167         try {
168             // Output prompt and get a line of input
169             writer.print(getPrompt());
170             writer.flush();
171             String line = reader.readLine();
172             if (line == null) {
173                 executionStatus.setLeft(Result.FINISHED);
174                 return;
175             }
176
177             // Expand any macros in the script
178             while (line.contains(macroFileTag)) {
179                 line = expandMacroFile(parameters, line);
180             }
181
182             if (parameters.isEcho()) {
183                 writer.println(line);
184             }
185
186             String logicBlock = null;
187             if (line.trim().endsWith(logicBlockStartTag)) {
188                 line = line.replace(logicBlockStartTag, "").trim();
189
190                 logicBlock = readLogicBlock(parameters, reader, writer, executionStatus);
191             }
192
193             // Parse the line into a list of commands and arguments
194             final List<String> commandWords = parser.parse(line, logicBlock);
195
196             // Find the command, if the command is null, then we are simply changing position in
197             // the hierarchy
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,
202                     commandWords);
203
204                 // Execute the command, a FINISHED result means a command causes the loop to
205                 // leave execution
206                 executionStatus.setLeft(executeCommand(command, argumentValues, writer));
207                 if (ApexApiResult.Result.isNok(executionStatus.getLeft())) {
208                     executionStatus.setRight(executionStatus.getRight() + 1);
209                 }
210             }
211         }
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);
220         }
221     }
222
223     /**
224      * Read a logic block, a block of program logic for a policy.
225      *
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
231      */
232     private String readLogicBlock(final CommandLineParameters parameters, final BufferedReader reader,
233         final PrintWriter writer, MutablePair<Result, Integer> executionStatus) {
234         StringBuilder logicBlock = new StringBuilder();
235
236         while (true) {
237             try {
238                 String logicLine = reader.readLine();
239                 if (logicLine == null) {
240                     return null;
241                 }
242
243                 while (logicLine.contains(macroFileTag)) {
244                     logicLine = expandMacroFile(parameters, logicLine);
245                 }
246
247                 if (parameters.isEcho()) {
248                     writer.println(logicLine);
249                 }
250
251                 if (logicLine.trim().endsWith(logicBlockEndTag)) {
252                     logicBlock.append(logicLine.replace(logicBlockEndTag, "").trim() + "\n");
253                     return logicBlock.toString();
254                 } else {
255                     logicBlock.append(logicLine + "\n");
256                 }
257             }
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);
263                 continue;
264             } catch (final Exception e) {
265                 e.printStackTrace(writer);
266                 LOGGER.error(COMMAND_LINE_ERROR, e);
267             }
268         }
269     }
270
271     /**
272      * Output a prompt that indicates where in the keyword hierarchy we are.
273      *
274      * @return A string with the prompt
275      */
276     private String getPrompt() {
277         final StringBuilder builder = new StringBuilder();
278         final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();
279
280         while (keynodeDequeIter.hasNext()) {
281             builder.append('/');
282             builder.append(keynodeDequeIter.next().getKeyword());
283         }
284         builder.append("> ");
285
286         return builder.toString();
287     }
288
289     /**
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"
292      *
293      * @param commandWords The commands and arguments parsed from the command line by the parser
294      * @return The found command
295      */
296
297     private CommandLineCommand findCommand(final List<String> commandWords) {
298         CommandLineCommand command = null;
299
300         final KeywordNode startKeywordNode = keywordNodeDeque.peek();
301
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();
305
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));
310             }
311
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) + ']');
324             }
325
326             // Record the fully expanded command word
327             commandWords.set(i, foundNodeEntries.get(0).getKey());
328
329             // Check if there is a command
330             final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
331             command = childKeywordNode.getCommand();
332
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);
337             } else {
338                 unwindStack(startKeywordNode);
339                 return command;
340             }
341         }
342
343         return null;
344     }
345
346     /**
347      * Unwind the stack of keyword node entries we have placed on the queue in a command search.
348      *
349      * @param startKeywordNode The point on the queue we want to unwind to
350      */
351     private void unwindStack(final KeywordNode startKeywordNode) {
352         // Unwind the stack
353         while (true) {
354             if (keywordNodeDeque.peek().equals(startKeywordNode)) {
355                 return;
356             }
357             keywordNodeDeque.pop();
358         }
359     }
360
361     /**
362      * Check the arguments of the command.
363      *
364      * @param command      The command to check
365      * @param commandWords The command words entered
366      * @return the argument values
367      */
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));
374             }
375         }
376
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) + ']');
388             }
389
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);
393         }
394
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
398             // mandatory
399             if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
400                 throw new CommandLineException(
401                     COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
402                         .getCliArgument().getArgumentName() + "\" not specified");
403             }
404         }
405
406         return argumentValues;
407     }
408
409     /**
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.
412      *
413      * @param commandWords The command words entered by the user
414      * @return the arguments as an entry array list
415      */
416     private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
417         final ArrayList<Entry<String, String>> arguments = new ArrayList<>();
418
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('=');
422             if (equalsPos > 0) {
423                 arguments
424                     .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
425             }
426         }
427
428         return arguments;
429     }
430
431     /**
432      * Execute system and editor commands.
433      *
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
438      */
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);
443         } else {
444             return modelHandler.executeCommand(command, argumentValues, writer);
445         }
446     }
447
448     /**
449      * Execute system commands.
450      *
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
454      */
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();
462         } else {
463             return Result.SUCCESS;
464         }
465     }
466
467     /**
468      * Execute the "back" command.
469      *
470      * @return the result of execution of the command
471      */
472     private Result executeBackCommand() {
473         if (keywordNodeDeque.size() > 1) {
474             keywordNodeDeque.pop();
475         }
476         return Result.SUCCESS;
477     }
478
479     /**
480      * Execute the "quit" command.
481      *
482      * @return the result of execution of the command
483      */
484     private Result executeQuitCommand() {
485         return Result.FINISHED;
486     }
487
488     /**
489      * Execute the "help" command.
490      *
491      * @param writer The writer to use for output from the command
492      * @return the result of execution of the command
493      */
494     private Result executeHelpCommand(final PrintWriter writer) {
495         for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
496             writer.println(command.getHelp());
497         }
498         return Result.SUCCESS;
499     }
500
501     /**
502      * Helper method to output an array list of keyword node entries to a string.
503      *
504      * @param nodeEntryArrayList the array list of keyword node entries
505      * @return the string
506      */
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());
511         }
512
513         return stringAL2String(stringArrayList);
514     }
515
516     /**
517      * Helper method to output an array list of argument entries to a string.
518      *
519      * @param argumentArrayList the argument array list
520      * @return the string
521      */
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());
526         }
527
528         return stringAL2String(stringArrayList);
529     }
530
531     /**
532      * Helper method to output an array list of strings to a string.
533      *
534      * @param stringArrayList the array list of strings
535      * @return the string
536      */
537     private String stringAL2String(final List<String> stringArrayList) {
538         final StringBuilder builder = new StringBuilder();
539         boolean first = true;
540         for (final String word : stringArrayList) {
541             if (first) {
542                 first = false;
543             } else {
544                 builder.append(',');
545             }
546             builder.append(word);
547         }
548
549         return builder.toString();
550     }
551
552     /**
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.
555      *
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
559      */
560     private String expandMacroFile(final CommandLineParameters parameters, final String line) {
561         final int macroTagPos = line.indexOf(macroFileTag);
562
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*", "");
566
567         // Get the file name that is the argument of the Macro tag
568         final String[] lineWords = lineAfterMacroTag.split("\\s+");
569
570         if (lineWords.length == 0) {
571             throw new CommandLineException("no file name specified for Macro File Tag");
572         }
573
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, "");
577
578         if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
579             macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
580         } else {
581             throw new CommandLineException(
582                 "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
583         }
584
585         // Append the working directory to the macro file name
586         macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;
587
588         // Now, get the text file for the argument of the macro
589         String macroFileContents = null;
590         try {
591             macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
592         } catch (final IOException e) {
593             throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
594         }
595
596         return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;
597     }
598 }