aboutsummaryrefslogtreecommitdiffstats
path: root/auth/cli-editor/src/main/java/org/onap/policy/apex/auth/clieditor/CommandLineEditorLoop.java
blob: 3adea1a894885be5fb387ac8e61576c3eae1ab10 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
/*-
 * ============LICENSE_START=======================================================
 *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
 *  Modifications Copyright (C) 2019-2020 Nordix Foundation.
 * ================================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * ============LICENSE_END=========================================================
 */

package org.onap.policy.apex.auth.clieditor;

import static org.onap.policy.apex.model.utilities.TreeMapUtils.findMatchingEntries;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.onap.policy.apex.model.modelapi.ApexApiResult;
import org.onap.policy.apex.model.modelapi.ApexApiResult.Result;
import org.onap.policy.apex.model.utilities.TreeMapUtils;
import org.onap.policy.common.utils.resources.TextFileUtils;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;

/**
 * This class implements the editor loop, the loop of execution that continuously executes commands until the quit
 * command is issued or EOF is detected on input.
 *
 * @author Liam Fallon (liam.fallon@ericsson.com)
 */
public class CommandLineEditorLoop {

    // Get a reference to the logger
    private static final XLogger LOGGER = XLoggerFactory.getXLogger(CommandLineEditorLoop.class);

    // Recurring string constants
    private static final String COMMAND = "command ";
    private static final String COMMAND_LINE_ERROR = "command line error";

    // The model handler that is handling the API towards the Apex model being editied
    private final ApexModelHandler modelHandler;

    // Holds the current location in the keyword hierarchy
    private final ArrayDeque<KeywordNode> keywordNodeDeque = new ArrayDeque<>();

    // Logic block tags
    private final String logicBlockStartTag;
    private final String logicBlockEndTag;

    // File Macro tag
    private final String macroFileTag;

    /**
     * Initiate the loop with the keyword node tree.
     *
     * @param properties      The CLI editor properties defined for execution
     * @param modelHandler    the model handler that will handle commands
     * @param rootKeywordNode The root keyword node tree
     */
    public CommandLineEditorLoop(final Properties properties, final ApexModelHandler modelHandler,
        final KeywordNode rootKeywordNode) {
        this.modelHandler = modelHandler;
        keywordNodeDeque.push(rootKeywordNode);

        logicBlockStartTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_START_TAG");
        logicBlockEndTag = properties.getProperty("DEFAULT_LOGIC_BLOCK_END_TAG");
        macroFileTag = properties.getProperty("DEFAULT_MACRO_FILE_TAG");
    }

    /**
     * Run a command loop.
     *
     * @param inputStream  The stream to read commands from
     * @param outputStream The stream to write command output and messages to
     * @param parameters   The parameters for the CLI editor
     * @return the exit code from command processing
     * @throws IOException Thrown on exceptions on IO
     */
    public int runLoop(final InputStream inputStream, final OutputStream outputStream,
        final CommandLineParameters parameters) throws IOException {
        // Readers and writers for input and output
        final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));

        // The parser parses the input lines into commands and arguments
        final CommandLineParser parser = new CommandLineParser();

        // The execution status has the result of the latest command and a cumulative error count
        MutablePair<Result, Integer> executionStatus = new MutablePair<>(Result.SUCCESS, 0);

        // The main loop for command handing, it continues until EOF on the input stream or until a
        // quit command
        while (!endOfCommandExecution(executionStatus, parameters)) {
            processIncomingCommands(parameters, reader, writer, parser, executionStatus);
        }

        // Get the output model
        if (!parameters.isSuppressModelOutput()) {
            final String modelString = modelHandler.writeModelToString(writer);

            if (parameters.checkSetOutputModelFileName()) {
                TextFileUtils.putStringAsTextFile(modelString, parameters.getOutputModelFileName());
            } else {
                writer.println(modelString);
            }
        }

        reader.close();
        writer.close();

        return executionStatus.getRight();
    }

    /**
     * Check if the command processing loop has come to an end.
     *
     * @param executionStatus a pair containing the result of the last command and the accumulated error count
     * @param parameters      the input parameters for command execution
     * @return true if the command processing loop should exit
     */
    private boolean endOfCommandExecution(Pair<Result, Integer> executionStatus, CommandLineParameters parameters) {
        if (executionStatus.getLeft() == Result.FINISHED) {
            return true;
        }

        return executionStatus.getRight() > 0 && !parameters.isIgnoreCommandFailures();
    }

    /**
     * Process the incoming commands one by one.
     *
     * @param parameters      the parameters to the CLI editor
     * @param reader          the reader to read the logic block from
     * @param writer          the writer to write results and error messages on
     * @param executionStatus the status of the logic block read
     */
    private void processIncomingCommands(final CommandLineParameters parameters, final BufferedReader reader,
        final PrintWriter writer, final CommandLineParser parser, MutablePair<Result, Integer> executionStatus) {

        try {
            // Output prompt and get a line of input
            writer.print(getPrompt());
            writer.flush();
            String line = reader.readLine();
            if (line == null) {
                executionStatus.setLeft(Result.FINISHED);
                return;
            }

            // Expand any macros in the script
            while (line.contains(macroFileTag)) {
                line = expandMacroFile(parameters, line);
            }

            if (parameters.isEcho()) {
                writer.println(line);
            }

            String logicBlock = null;
            if (line.trim().endsWith(logicBlockStartTag)) {
                line = line.replace(logicBlockStartTag, "").trim();

                logicBlock = readLogicBlock(parameters, reader, writer, executionStatus);
            }

            // Parse the line into a list of commands and arguments
            final List<String> commandWords = parser.parse(line, logicBlock);

            // Find the command, if the command is null, then we are simply changing position in
            // the hierarchy
            final CommandLineCommand command = findCommand(commandWords);
            if (command != null) {
                // Check the arguments of the command
                final TreeMap<String, CommandLineArgumentValue> argumentValues = getArgumentValues(command,
                    commandWords);

                // Execute the command, a FINISHED result means a command causes the loop to
                // leave execution
                executionStatus.setLeft(executeCommand(command, argumentValues, writer));
                if (ApexApiResult.Result.isNok(executionStatus.getLeft())) {
                    executionStatus.setRight(executionStatus.getRight() + 1);
                }
            }
        } catch (final CommandLineException e) {
            // Print any error messages from command parsing and finding
            writer.println(e.getMessage());
            executionStatus.setRight(executionStatus.getRight() + 1);
            LOGGER.debug(COMMAND_LINE_ERROR, e);
        } catch (final Exception e) {
            e.printStackTrace(writer);
            LOGGER.error(COMMAND_LINE_ERROR, e);
        }
    }

    /**
     * Read a logic block, a block of program logic for a policy.
     *
     * @param parameters      the parameters to the CLI editor
     * @param reader          the reader to read the logic block from
     * @param writer          the writer to write results and error messages on
     * @param executionStatus the status of the logic block read
     * @return the result of the logic block read
     */
    private String readLogicBlock(final CommandLineParameters parameters, final BufferedReader reader,
        final PrintWriter writer, MutablePair<Result, Integer> executionStatus) {
        StringBuilder logicBlock = new StringBuilder();

        while (true) {
            try {
                String logicLine = reader.readLine();
                if (logicLine == null) {
                    return null;
                }

                while (logicLine.contains(macroFileTag)) {
                    logicLine = expandMacroFile(parameters, logicLine);
                }

                if (parameters.isEcho()) {
                    writer.println(logicLine);
                }

                if (logicLine.trim().endsWith(logicBlockEndTag)) {
                    logicBlock.append(logicLine.replace(logicBlockEndTag, "").trim() + "\n");
                    return logicBlock.toString();
                } else {
                    logicBlock.append(logicLine + "\n");
                }
            } catch (final CommandLineException e) {
                // Print any error messages from command parsing and finding
                writer.println(e.getMessage());
                executionStatus.setRight(executionStatus.getRight() + 1);
                LOGGER.debug(COMMAND_LINE_ERROR, e);
            } catch (final Exception e) {
                e.printStackTrace(writer);
                LOGGER.error(COMMAND_LINE_ERROR, e);
            }
        }
    }

    /**
     * Output a prompt that indicates where in the keyword hierarchy we are.
     *
     * @return A string with the prompt
     */
    private String getPrompt() {
        final StringBuilder builder = new StringBuilder();
        final Iterator<KeywordNode> keynodeDequeIter = keywordNodeDeque.descendingIterator();

        while (keynodeDequeIter.hasNext()) {
            builder.append('/');
            builder.append(keynodeDequeIter.next().getKeyword());
        }
        builder.append("> ");

        return builder.toString();
    }

    /**
     * Finds a command for the given input command words. Command words need only ne specified enough to uniquely
     * identify them. Therefore, "p s o c" will find the command "policy state output create"
     *
     * @param commandWords The commands and arguments parsed from the command line by the parser
     * @return The found command
     */

    private CommandLineCommand findCommand(final List<String> commandWords) {
        CommandLineCommand command = null;

        final KeywordNode startKeywordNode = keywordNodeDeque.peek();

        // Go down through the keywords searching for the command
        for (int i = 0; i < commandWords.size(); i++) {
            final KeywordNode searchKeywordNode = keywordNodeDeque.peek();

            // We have got to the arguments, time to stop looking
            if (commandWords.get(i).indexOf('=') >= 0) {
                unwindStack(startKeywordNode);
                throw new CommandLineException("command not found: " + stringAL2String(commandWords));
            }

            // If the node entries found is not equal to one, then we have either no command or more
            // than one command matching
            final List<Entry<String, KeywordNode>> foundNodeEntries = findMatchingEntries(
                searchKeywordNode.getChildren(), commandWords.get(i));
            if (foundNodeEntries.isEmpty()) {
                unwindStack(startKeywordNode);
                throw new CommandLineException("command not found: " + stringAL2String(commandWords));
            } else if (foundNodeEntries.size() > 1) {
                unwindStack(startKeywordNode);
                throw new CommandLineException(
                    "multiple commands matched: " + stringAL2String(commandWords) + " [" + nodeAL2String(
                        foundNodeEntries) + ']');
            }

            // Record the fully expanded command word
            commandWords.set(i, foundNodeEntries.get(0).getKey());

            // Check if there is a command
            final KeywordNode childKeywordNode = foundNodeEntries.get(0).getValue();
            command = childKeywordNode.getCommand();

            // If the command is null, we go into a sub mode, otherwise we unwind the stack of
            // commands and return the found command
            if (command == null) {
                keywordNodeDeque.push(childKeywordNode);
            } else {
                unwindStack(startKeywordNode);
                return command;
            }
        }

        return null;
    }

    /**
     * Unwind the stack of keyword node entries we have placed on the queue in a command search.
     *
     * @param startKeywordNode The point on the queue we want to unwind to
     */
    private void unwindStack(final KeywordNode startKeywordNode) {
        // Unwind the stack
        while (true) {
            if (keywordNodeDeque.peek().equals(startKeywordNode)) {
                return;
            }
            keywordNodeDeque.pop();
        }
    }

    /**
     * Check the arguments of the command.
     *
     * @param command      The command to check
     * @param commandWords The command words entered
     * @return the argument values
     */
    private TreeMap<String, CommandLineArgumentValue> getArgumentValues(final CommandLineCommand command,
        final List<String> commandWords) {
        final TreeMap<String, CommandLineArgumentValue> argumentValues = new TreeMap<>();
        for (final CommandLineArgument argument : command.getArgumentList()) {
            if (argument != null) {
                argumentValues.put(argument.getArgumentName(), new CommandLineArgumentValue(argument));
            }
        }

        // Set the value of the arguments
        for (final Entry<String, String> argument : getCommandArguments(commandWords)) {
            final List<Entry<String, CommandLineArgumentValue>> foundArguments = TreeMapUtils
                .findMatchingEntries(argumentValues, argument.getKey());
            if (foundArguments.isEmpty()) {
                throw new CommandLineException(
                    COMMAND + stringAL2String(commandWords) + ": " + " argument \"" + argument.getKey()
                        + "\" not allowed on command");
            } else if (foundArguments.size() > 1) {
                throw new CommandLineException(COMMAND + stringAL2String(commandWords) + ": " + " argument " + argument
                    + " matches multiple arguments [" + argumentAL2String(foundArguments) + ']');
            }

            // Set the value of the argument, stripping off any quotes
            final String argumentValue = argument.getValue().replaceAll("^\"", "").replaceAll("\"$", "");
            foundArguments.get(0).getValue().setValue(argumentValue);
        }

        // Now check all mandatory arguments are set
        for (final CommandLineArgumentValue argumentValue : argumentValues.values()) {
            // Argument values are null by default so if this argument is not nullable it is
            // mandatory
            if (!argumentValue.isSpecified() && !argumentValue.getCliArgument().isNullable()) {
                throw new CommandLineException(
                    COMMAND + stringAL2String(commandWords) + ": " + " mandatory argument \"" + argumentValue
                        .getCliArgument().getArgumentName() + "\" not specified");
            }
        }

        return argumentValues;
    }

    /**
     * Get the arguments of the command, the command words have already been conditioned into an array starting with the
     * command words and ending with the arguments as name=value tuples.
     *
     * @param commandWords The command words entered by the user
     * @return the arguments as an entry array list
     */
    private ArrayList<Entry<String, String>> getCommandArguments(final List<String> commandWords) {
        final ArrayList<Entry<String, String>> arguments = new ArrayList<>();

        // Iterate over the command words, arguments are of the format name=value
        for (final String word : commandWords) {
            final int equalsPos = word.indexOf('=');
            if (equalsPos > 0) {
                arguments
                    .add(new SimpleEntry<>(word.substring(0, equalsPos), word.substring(equalsPos + 1, word.length())));
            }
        }

        return arguments;
    }

    /**
     * Execute system and editor commands.
     *
     * @param command        The command to execute
     * @param argumentValues The arguments input on the command line to invoke the command
     * @param writer         The writer to use for any output from the command
     * @return the result of execution of the command
     */
    private Result executeCommand(final CommandLineCommand command,
        final TreeMap<String, CommandLineArgumentValue> argumentValues, final PrintWriter writer) {
        if (command.isSystemCommand()) {
            return exceuteSystemCommand(command, writer);
        } else {
            return modelHandler.executeCommand(command, argumentValues, writer);
        }
    }

    /**
     * Execute system commands.
     *
     * @param command The command to execute
     * @param writer  The writer to use for any output from the command
     * @return the result of execution of the command
     */
    private Result exceuteSystemCommand(final CommandLineCommand command, final PrintWriter writer) {
        if ("back".equals(command.getName())) {
            return executeBackCommand();
        } else if ("help".equals(command.getName())) {
            return executeHelpCommand(writer);
        } else if ("quit".equals(command.getName())) {
            return executeQuitCommand();
        } else {
            return Result.SUCCESS;
        }
    }

    /**
     * Execute the "back" command.
     *
     * @return the result of execution of the command
     */
    private Result executeBackCommand() {
        if (keywordNodeDeque.size() > 1) {
            keywordNodeDeque.pop();
        }
        return Result.SUCCESS;
    }

    /**
     * Execute the "quit" command.
     *
     * @return the result of execution of the command
     */
    private Result executeQuitCommand() {
        return Result.FINISHED;
    }

    /**
     * Execute the "help" command.
     *
     * @param writer The writer to use for output from the command
     * @return the result of execution of the command
     */
    private Result executeHelpCommand(final PrintWriter writer) {
        for (final CommandLineCommand command : keywordNodeDeque.peek().getCommands()) {
            writer.println(command.getHelp());
        }
        return Result.SUCCESS;
    }

    /**
     * Helper method to output an array list of keyword node entries to a string.
     *
     * @param nodeEntryArrayList the array list of keyword node entries
     * @return the string
     */
    private String nodeAL2String(final List<Entry<String, KeywordNode>> nodeEntryArrayList) {
        final ArrayList<String> stringArrayList = new ArrayList<>();
        for (final Entry<String, KeywordNode> node : nodeEntryArrayList) {
            stringArrayList.add(node.getValue().getKeyword());
        }

        return stringAL2String(stringArrayList);
    }

    /**
     * Helper method to output an array list of argument entries to a string.
     *
     * @param argumentArrayList the argument array list
     * @return the string
     */
    private String argumentAL2String(final List<Entry<String, CommandLineArgumentValue>> argumentArrayList) {
        final ArrayList<String> stringArrayList = new ArrayList<>();
        for (final Entry<String, CommandLineArgumentValue> argument : argumentArrayList) {
            stringArrayList.add(argument.getValue().getCliArgument().getArgumentName());
        }

        return stringAL2String(stringArrayList);
    }

    /**
     * Helper method to output an array list of strings to a string.
     *
     * @param stringArrayList the array list of strings
     * @return the string
     */
    private String stringAL2String(final List<String> stringArrayList) {
        final StringBuilder builder = new StringBuilder();
        boolean first = true;
        for (final String word : stringArrayList) {
            if (first) {
                first = false;
            } else {
                builder.append(',');
            }
            builder.append(word);
        }

        return builder.toString();
    }

    /**
     * This method reads in the file from a file macro statement, expands the macro, and replaces the Macro tag in the
     * line with the file contents.
     *
     * @param parameters The parameters for the CLI editor
     * @param line       The line with the macro keyword in it
     * @return the expanded line
     */
    private String expandMacroFile(final CommandLineParameters parameters, final String line) {
        final int macroTagPos = line.indexOf(macroFileTag);

        // Get the line before and after the macro tag
        final String lineBeforeMacroTag = line.substring(0, macroTagPos);
        final String lineAfterMacroTag = line.substring(macroTagPos + macroFileTag.length()).replaceAll("^\\s*", "");

        // Get the file name that is the argument of the Macro tag
        final String[] lineWords = lineAfterMacroTag.split("\\s+");

        if (lineWords.length == 0) {
            throw new CommandLineException("no file name specified for Macro File Tag");
        }

        // Get the macro file name and the remainder of the line after the file name
        String macroFileName = lineWords[0];
        final String lineAfterMacroFileName = lineAfterMacroTag.replaceFirst(macroFileName, "");

        if (macroFileName.length() > 2 && macroFileName.startsWith("\"") && macroFileName.endsWith("\"")) {
            macroFileName = macroFileName.substring(1, macroFileName.length() - 1);
        } else {
            throw new CommandLineException(
                "macro file name \"" + macroFileName + "\" must exist and be quoted with double quotes \"\"");
        }

        // Append the working directory to the macro file name
        macroFileName = parameters.getWorkingDirectory() + File.separatorChar + macroFileName;

        // Now, get the text file for the argument of the macro
        String macroFileContents = null;
        try {
            macroFileContents = TextFileUtils.getTextFileAsString(macroFileName);
        } catch (final IOException e) {
            throw new CommandLineException("file \"" + macroFileName + "\" specified in Macro File Tag not found", e);
        }

        return lineBeforeMacroTag + macroFileContents + lineAfterMacroFileName;
    }
}