Merge "Fix Sonar Issues on Apex-pdp"
[policy/apex-pdp.git] / auth / cli-editor / src / main / java / org / onap / policy / apex / auth / clieditor / CommandLineParser.java
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
4  *  Modifications Copyright (C) 2020-2021 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 java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28
29 /**
30  * This class chops a command line up into commands, parameters and arguments.
31  *
32  * @author Liam Fallon (liam.fallon@ericsson.com)
33  */
34 public class CommandLineParser {
35
36     /**
37      * This method breaks a line of input up into commands, parameters, and arguments. Commands are standalone words at
38      * the beginning of the line, of which there may be multiple Parameters are single words followed by an '='
39      * character Arguments are single words or a block of quoted text following an '=' character.
40      *
41      * <p>Format: command [command....] parameter=argument [parameter = argument]
42      *
43      * <p>Examples entity create name=hello description="description of hello" help entity list
44      *
45      * @param line The line to parse
46      * @param logicBlock A block of logic code to be taken literally
47      * @return the string array list
48      */
49     public List<String> parse(final String line, final String logicBlock) {
50         return checkFormat(
51                         mergeArguments(mergeEquals(splitOnEquals(
52                                         stripAndSplitWords(mergeQuotes(splitOnChar(stripComments(line), '\"')))))),
53                         logicBlock);
54     }
55
56     /**
57      * Strip comments from lines, comments start with a # character.
58      *
59      * @param line the line
60      * @return the line without comments
61      */
62     private String stripComments(final String line) {
63         final int commentPos = line.indexOf('#');
64         if (commentPos == -1) {
65             return line;
66         } else {
67             return line.substring(0, commentPos);
68         }
69     }
70
71     /**
72      * This method merges an array with separate quotes into an array with quotes delimiting the start and end of quoted
73      * words Example [Humpty ],["],[Dumpty sat on the wall],["],[, Humpty Dumpty had ],["],["],a ["],[great],["],[ fall]
74      * becomes [Humpty ],["Dumpty sat on the wall"],[, Humpty Dumpty had ],[""],[a],["great"],[ fall].
75      *
76      * @param wordsSplitOnQuotes the words split on quotes
77      * @return the merged array list
78      */
79     private ArrayList<String> mergeQuotes(final ArrayList<String> wordsSplitOnQuotes) {
80         final ArrayList<String> wordsWithQuotesMerged = new ArrayList<>();
81
82         var wordIndex = 0;
83         while (wordIndex < wordsSplitOnQuotes.size()) {
84             wordIndex = mergeQuote(wordsSplitOnQuotes, wordsWithQuotesMerged, wordIndex);
85         }
86
87         return wordsWithQuotesMerged;
88     }
89
90     /**
91      * This method merges the next set of quotes.
92      *
93      * @param wordsSplitOnQuotes the words split on quotes
94      * @param wordsWithQuotesMerged the merged words
95      * @param wordIndex the current word index
96      * @return the next word index
97      */
98     private int mergeQuote(final ArrayList<String> wordsSplitOnQuotes, final ArrayList<String> wordsWithQuotesMerged,
99                     int wordIndex) {
100
101         if ("\"".equals(wordsSplitOnQuotes.get(wordIndex))) {
102             var quotedWord = new StringBuilder(wordsSplitOnQuotes.get(wordIndex++));
103
104             for (; wordIndex < wordsSplitOnQuotes.size(); wordIndex++) {
105                 quotedWord.append(wordsSplitOnQuotes.get(wordIndex));
106                 if ("\"".equals(wordsSplitOnQuotes.get(wordIndex))) {
107                     wordIndex++;
108                     break;
109                 }
110             }
111             var quotedWordToString = quotedWord.toString();
112             if (quotedWordToString.matches("^\".*\"$")) {
113                 wordsWithQuotesMerged.add(quotedWordToString);
114             } else {
115                 throw new CommandLineException("trailing quote found in input " + wordsSplitOnQuotes);
116             }
117         } else {
118             wordsWithQuotesMerged.add(wordsSplitOnQuotes.get(wordIndex++));
119         }
120         return wordIndex;
121     }
122
123     /**
124      * This method splits the words on an array list into an array list where each portion of the line is split into
125      * words by '=', quoted words are ignored Example: aaa = bbb = ccc=ddd=eee = becomes [aaa ],[=],[bbb
126      * ],[=],[ccc],[=],[ddd],[=],[eee ],[=].
127      *
128      * @param words the words
129      * @return the merged array list
130      */
131     private ArrayList<String> splitOnEquals(final ArrayList<String> words) {
132         final ArrayList<String> wordsSplitOnEquals = new ArrayList<>();
133
134         for (final String word : words) {
135             // Is this a quoted word ?
136             if (word.startsWith("\"")) {
137                 wordsSplitOnEquals.add(word);
138                 continue;
139             }
140
141             // Split on equals character
142             final ArrayList<String> splitWords = splitOnChar(word, '=');
143             wordsSplitOnEquals.addAll(splitWords);
144         }
145
146         return wordsSplitOnEquals;
147     }
148
149     /**
150      * This method merges an array with separate equals into an array with equals delimiting the start of words Example:
151      * [aaa ],[=],[bbb ],[=],[ccc],[=],[ddd],[=],[eee ],[=] becomes [aaa ],[= bbb ],[= ccc],[=ddd],[=eee ],[=].
152      *
153      * @param wordsSplitOnEquals the words split on equals
154      * @return the merged array list
155      */
156     private ArrayList<String> mergeEquals(final ArrayList<String> wordsSplitOnEquals) {
157         final ArrayList<String> wordsWithEqualsMerged = new ArrayList<>();
158
159         var wordIndex = 0;
160         while (wordIndex < wordsSplitOnEquals.size()) {
161
162             // Is this a quoted word ?
163             if (wordsSplitOnEquals.get(wordIndex).startsWith("\"")) {
164                 wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex));
165                 continue;
166             }
167
168             if ("=".equals(wordsSplitOnEquals.get(wordIndex))) {
169                 if (wordIndex < wordsSplitOnEquals.size() - 1
170                                 && !wordsSplitOnEquals.get(wordIndex + 1).startsWith("=")) {
171                     wordsWithEqualsMerged.add(
172                                     wordsSplitOnEquals.get(wordIndex) + wordsSplitOnEquals.get(wordIndex + 1));
173                     wordIndex += 2;
174                 } else {
175                     wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex++));
176                 }
177             } else {
178                 wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex++));
179             }
180         }
181
182         return wordsWithEqualsMerged;
183     }
184
185     /**
186      * This method merges words that start with an '=' character with the previous word if that word does not start with
187      * an '='.
188      *
189      * @param words the words
190      * @return the merged array list
191      */
192     private ArrayList<String> mergeArguments(final ArrayList<String> words) {
193         final ArrayList<String> mergedArguments = new ArrayList<>();
194
195         for (var i = 0; i < words.size(); i++) {
196             // Is this a quoted word ?
197             if (words.get(i).startsWith("\"")) {
198                 mergedArguments.add(words.get(i));
199                 continue;
200             }
201
202             if (words.get(i).startsWith("=")) {
203                 if (i > 0 && !words.get(i - 1).startsWith("=")) {
204                     mergedArguments.remove(mergedArguments.size() - 1);
205                     mergedArguments.add(words.get(i - 1) + words.get(i));
206                 } else {
207                     mergedArguments.add(words.get(i));
208                 }
209             } else {
210                 mergedArguments.add(words.get(i));
211             }
212         }
213
214         return mergedArguments;
215     }
216
217     /**
218      * This method strips all non quoted white space down to single spaces and splits non-quoted words into separate
219      * words.
220      *
221      * @param words the words
222      * @return the array list with white space stripped and words split
223      */
224     private ArrayList<String> stripAndSplitWords(final ArrayList<String> words) {
225         final ArrayList<String> strippedAndSplitWords = new ArrayList<>();
226
227         for (String word : words) {
228             // Is this a quoted word
229             if (word.startsWith("\"")) {
230                 strippedAndSplitWords.add(word);
231             } else {
232                 // Split the word on blanks
233                 strippedAndSplitWords.addAll(stripAndSplitWord(word));
234             }
235         }
236
237         return strippedAndSplitWords;
238     }
239
240     /**
241      * Strip and split a word on blanks into an array of words split on blanks.
242      *
243      * @param word the word to split
244      * @return the array of split words
245      */
246     private Collection<? extends String> stripAndSplitWord(final String word) {
247         final ArrayList<String> strippedAndSplitWords = new ArrayList<>();
248
249         // Strip white space by replacing all white space with blanks and then removing leading
250         // and trailing blanks
251         String singleSpaceWord = word.replaceAll("\\s+", " ").trim();
252
253         if (singleSpaceWord.length() == 0) {
254             return strippedAndSplitWords;
255         }
256
257         // Split on space characters
258         final String[] splitWords = singleSpaceWord.split(" ");
259         Collections.addAll(strippedAndSplitWords, splitWords);
260
261         return strippedAndSplitWords;
262     }
263
264     /**
265      * This method splits a line of text into an array list where each portion of the line is split into words by a
266      * character, with the characters themselves as separate words Example: Humpty "Dumpty sat on the wall", Humpty
267      * Dumpty had ""a "great" fall becomes [Humpty ],["],[Dumpty sat on the wall],["],[, Humpty Dumpty had ],["],["],a
268      * ["],[great],["],[ fall].
269      *
270      * @param line the input line
271      * @param splitChar the split char
272      * @return the split array list
273      */
274     private ArrayList<String> splitOnChar(final String line, final char splitChar) {
275         final ArrayList<String> wordsSplitOnQuotes = new ArrayList<>();
276
277         var currentPos = 0;
278         while (currentPos != -1) {
279             final int quotePos = line.indexOf(splitChar, currentPos);
280             if (quotePos != -1) {
281                 if (currentPos < quotePos) {
282                     wordsSplitOnQuotes.add(line.substring(currentPos, quotePos));
283                 }
284                 wordsSplitOnQuotes.add("" + splitChar);
285                 currentPos = quotePos + 1;
286
287                 if (currentPos == line.length()) {
288                     currentPos = -1;
289                 }
290             } else {
291                 wordsSplitOnQuotes.add(line.substring(currentPos));
292                 currentPos = quotePos;
293             }
294         }
295
296         return wordsSplitOnQuotes;
297     }
298
299     /**
300      * This method checks that an array list containing a command is in the correct format.
301      *
302      * @param commandWords the command words
303      * @param logicBlock A block of logic code to be taken literally
304      * @return the checked array list
305      */
306     private ArrayList<String> checkFormat(final ArrayList<String> commandWords, final String logicBlock) {
307         // There should be at least one word
308         if (commandWords.isEmpty()) {
309             return commandWords;
310         }
311
312         // The first word must be alphanumeric, that is a command
313         if (!commandWords.get(0).matches("^[a-zA-Z0-9]*$")) {
314             throw new CommandLineException(
315                     "first command word is not alphanumeric or is not a command: " + commandWords.get(0));
316         }
317
318         // Now check that we have a sequence of commands at the beginning
319         var currentWordPos = 0;
320         for (; currentWordPos < commandWords.size(); currentWordPos++) {
321             if (!commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]*$")) {
322                 break;
323             }
324         }
325
326         for (; currentWordPos < commandWords.size(); ++currentWordPos) {
327             if (currentWordPos == commandWords.size() - 1 && logicBlock != null) {
328                 // for the last command, if the command ends with = and there is a Logic block
329                 if (commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]+=")) {
330                     commandWords.set(currentWordPos, commandWords.get(currentWordPos) + logicBlock);
331                 } else {
332                     throw new CommandLineException(
333                             "command argument is not properly formed: " + commandWords.get(currentWordPos));
334                 }
335             } else if (!commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]+=[a-zA-Z0-9/\"].*$")) {
336                 // Not a last command, or the last command, but there is no logic block - wrong pattern
337                 throw new CommandLineException(
338                         "command argument is not properly formed: " + commandWords.get(currentWordPos));
339             }
340         }
341
342         return commandWords;
343     }
344 }