Changes for checkstyle 8.32
[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 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;
50
51 /**
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.
54  *
55  * @author Liam Fallon (liam.fallon@ericsson.com)
56  */
57 public class CommandLineEditorLoop {
58
59     // Get a reference to the logger
60     private static final XLogger LOGGER = XLoggerFactory.getXLogger(CommandLineEditorLoop.class);
61
62     // Recurring string constants
63     private static final String COMMAND = "command ";
64     private static final String COMMAND_LINE_ERROR = "command line error";
65
66     // The model handler that is handling the API towards the Apex model being editied
67     private final ApexModelHandler modelHandler;
68
69     // Holds the current location in the keyword hierarchy
70     private final ArrayDeque<KeywordNode> keywordNodeDeque = new ArrayDeque<>();
71
72     // Logic block tags
73     private final String logicBlockStartTag;
74     private final String logicBlockEndTag;
75
76     // File Macro tag
77     private final String macroFileTag;
78
79     /**
80      * Initiate the loop with the keyword node tree.
81      *
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
85      */
86     public CommandLineEditorLoop(final Properties properties, final ApexModelHandler modelHandler,
87         final KeywordNode rootKeywordNode) {
88         this.modelHandler = modelHandler;
89         keywordNodeDeque.push(rootKeywordNode);
90
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");
94     }
95
96     /**
97      * Run a command loop.
98      *
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
104      */
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));
110
111         // The parser parses the input lines into commands and arguments
112         final CommandLineParser parser = new CommandLineParser();
113
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);
116
117         // The main loop for command handing, it continues until EOF on the input stream or until a
118         // quit command
119         while (!endOfCommandExecution(executionStatus, parameters)) {
120             processIncomingCommands(parameters, reader, writer, parser, executionStatus);
121         }
122
123         // Get the output model
124         if (!parameters.isSuppressModelOutput()) {
125             final String modelString = modelHandler.writeModelToString(writer);
126
127             if (parameters.checkSetOutputModelFileName()) {
128                 TextFileUtils.putStringAsTextFile(modelString, parameters.getOutputModelFileName());
129             } else {
130                 writer.println(modelString);
131             }
132         }
133
134         reader.close();
135         writer.close();
136
137         return executionStatus.getRight();
138     }
139
140     /**
141      * Check if the command processing loop has come to an end.
142      *
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
146      */
147     private boolean endOfCommandExecution(Pair<Result, Integer> executionStatus, CommandLineParameters parameters) {
148         if (executionStatus.getLeft() == Result.FINISHED) {
149             return true;
150         }
151
152         return executionStatus.getRight() > 0 && !parameters.isIgnoreCommandFailures();
153     }
154
155     /**
156      * Process the incoming commands one by one.
157      *
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
162      */
163     private void processIncomingCommands(final CommandLineParameters parameters, final BufferedReader reader,
164         final PrintWriter writer, final CommandLineParser parser, MutablePair<Result, Integer> executionStatus) {
165
166         try {
167             // Output prompt and get a line of input
168             writer.print(getPrompt());
169             writer.flush();
170             String line = reader.readLine();
171             if (line == null) {
172                 executionStatus.setLeft(Result.FINISHED);
173                 return;
174             }
175
176             // Expand any macros in the script
177             while (line.contains(macroFileTag)) {
178                 line = expandMacroFile(parameters, line);
179             }
180
181             if (parameters.isEcho()) {
182                 writer.println(line);
183             }
184
185             String logicBlock = null;
186             if (line.trim().endsWith(logicBlockStartTag)) {
187                 line = line.replace(logicBlockStartTag, "").trim();
188
189                 logicBlock = readLogicBlock(parameters, reader, writer, executionStatus);
190             }
191
192             // Parse the line into a list of commands and arguments
193             final List<String> commandWords = parser.parse(line, logicBlock);
194
195             // Find the command, if the command is null, then we are simply changing position in
196             // the hierarchy
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,
201                     commandWords);
202
203                 // Execute the command, a FINISHED result means a command causes the loop to
204                 // leave execution
205                 executionStatus.setLeft(executeCommand(command, argumentValues, writer));
206                 if (ApexApiResult.Result.isNok(executionStatus.getLeft())) {
207                     executionStatus.setRight(executionStatus.getRight() + 1);
208                 }
209             }
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);
218         }
219     }
220
221     /**
222      * Read a logic block, a block of program logic for a policy.
223      *
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
229      */
230     private String readLogicBlock(final CommandLineParameters parameters, final BufferedReader reader,
231         final PrintWriter writer, MutablePair<Result, Integer> executionStatus) {
232         StringBuilder logicBlock = new StringBuilder();
233
234         while (true) {
235             try {
236                 String logicLine = reader.readLine();
237                 if (logicLine == null) {
238                     return null;
239                 }
240
241                 while (logicLine.contains(macroFileTag)) {
242                     logicLine = expandMacroFile(parameters, logicLine);
243                 }
244
245                 if (parameters.isEcho()) {
246                     writer.println(logicLine);
247                 }
248
249                 if (logicLine.trim().endsWith(logicBlockEndTag)) {
250                     logicBlock.append(logicLine.replace(logicBlockEndTag, "").trim() + "\n");
251                     return logicBlock.toString();
252                 } else {
253                     logicBlock.append(logicLine + "\n");
254                 }
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                 continue;
261             } catch (final Exception e) {
262                 e.printStackTrace(writer);
263                 LOGGER.error(COMMAND_LINE_ERROR, e);
264             }
265         }
266     }
267
268     /**
269      * Output a prompt that indicates where in the keyword hierarchy we are.
270      *
271      * @return A string with the prompt
272      */
273     private String getPrompt() {
274         final StringBuilder builder = new StringBuilder();
275         final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();
276
277         while (keynodeDequeIter.hasNext()) {
278             builder.append('/');
279             builder.append(keynodeDequeIter.next().getKeyword());
280         }
281         builder.append("> ");
282
283         return builder.toString();
284     }
285
286     /**
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"
289      *
290      * @param commandWords The commands and arguments parsed from the command line by the parser
291      * @return The found command
292      */
293
294     private CommandLineCommand findCommand(final List<String> commandWords) {
295         CommandLineCommand command = null;
296
297         final KeywordNode startKeywordNode = keywordNodeDeque.peek();
298
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();
302
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));
307             }
308
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) + ']');
321             }
322
323             // Record the fully expanded command word
324             commandWords.set(i, foundNodeEntries.get(0).getKey());
325
326             // Check if there is a command
327             final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
328             command = childKeywordNode.getCommand();
329
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);
334             } else {
335                 unwindStack(startKeywordNode);
336                 return command;
337             }
338         }
339
340         return null;
341     }
342
343     /**
344      * Unwind the stack of keyword node entries we have placed on the queue in a command search.
345      *
346      * @param startKeywordNode The point on the queue we want to unwind to
347      */
348     private void unwindStack(final KeywordNode startKeywordNode) {
349         // Unwind the stack
350         while (true) {
351             if (keywordNodeDeque.peek().equals(startKeywordNode)) {
352                 return;
353             }
354             keywordNodeDeque.pop();
355         }
356     }
357
358     /**
359      * Check the arguments of the command.
360      *
361      * @param command      The command to check
362      * @param commandWords The command words entered
363      * @return the argument values
364      */
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));
371             }
372         }
373
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) + ']');
385             }
386
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);
390         }
391
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
395             // mandatory
396             if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
397                 throw new CommandLineException(
398                     COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
399                         .getCliArgument().getArgumentName() + "\" not specified");
400             }
401         }
402
403         return argumentValues;
404     }
405
406     /**
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.
409      *
410      * @param commandWords The command words entered by the user
411      * @return the arguments as an entry array list
412      */
413     private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
414         final ArrayList<Entry<String, String>> arguments = new ArrayList<>();
415
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('=');
419             if (equalsPos > 0) {
420                 arguments
421                     .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
422             }
423         }
424
425         return arguments;
426     }
427
428     /**
429      * Execute system and editor commands.
430      *
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
435      */
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);
440         } else {
441             return modelHandler.executeCommand(command, argumentValues, writer);
442         }
443     }
444
445     /**
446      * Execute system commands.
447      *
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
451      */
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();
459         } else {
460             return Result.SUCCESS;
461         }
462     }
463
464     /**
465      * Execute the "back" command.
466      *
467      * @return the result of execution of the command
468      */
469     private Result executeBackCommand() {
470         if (keywordNodeDeque.size() > 1) {
471             keywordNodeDeque.pop();
472         }
473         return Result.SUCCESS;
474     }
475
476     /**
477      * Execute the "quit" command.
478      *
479      * @return the result of execution of the command
480      */
481     private Result executeQuitCommand() {
482         return Result.FINISHED;
483     }
484
485     /**
486      * Execute the "help" command.
487      *
488      * @param writer The writer to use for output from the command
489      * @return the result of execution of the command
490      */
491     private Result executeHelpCommand(final PrintWriter writer) {
492         for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
493             writer.println(command.getHelp());
494         }
495         return Result.SUCCESS;
496     }
497
498     /**
499      * Helper method to output an array list of keyword node entries to a string.
500      *
501      * @param nodeEntryArrayList the array list of keyword node entries
502      * @return the string
503      */
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());
508         }
509
510         return stringAL2String(stringArrayList);
511     }
512
513     /**
514      * Helper method to output an array list of argument entries to a string.
515      *
516      * @param argumentArrayList the argument array list
517      * @return the string
518      */
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());
523         }
524
525         return stringAL2String(stringArrayList);
526     }
527
528     /**
529      * Helper method to output an array list of strings to a string.
530      *
531      * @param stringArrayList the array list of strings
532      * @return the string
533      */
534     private String stringAL2String(final List<String> stringArrayList) {
535         final StringBuilder builder = new StringBuilder();
536         boolean first = true;
537         for (final String word : stringArrayList) {
538             if (first) {
539                 first = false;
540             } else {
541                 builder.append(',');
542             }
543             builder.append(word);
544         }
545
546         return builder.toString();
547     }
548
549     /**
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.
552      *
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
556      */
557     private String expandMacroFile(final CommandLineParameters parameters, final String line) {
558         final int macroTagPos = line.indexOf(macroFileTag);
559
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*", "");
563
564         // Get the file name that is the argument of the Macro tag
565         final String[] lineWords = lineAfterMacroTag.split("\\s+");
566
567         if (lineWords.length == 0) {
568             throw new CommandLineException("no file name specified for Macro File Tag");
569         }
570
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, "");
574
575         if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
576             macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
577         } else {
578             throw new CommandLineException(
579                 "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
580         }
581
582         // Append the working directory to the macro file name
583         macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;
584
585         // Now, get the text file for the argument of the macro
586         String macroFileContents = null;
587         try {
588             macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
589         } catch (final IOException e) {
590             throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
591         }
592
593         return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;
594     }
595 }