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