/*- * ============LICENSE_START======================================================= * Copyright (C) 2016-2018 Ericsson. All rights reserved. * Modifications Copyright (C) 2020-2021 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 java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; /** * This class chops a command line up into commands, parameters and arguments. * * @author Liam Fallon (liam.fallon@ericsson.com) */ public class CommandLineParser { /** * This method breaks a line of input up into commands, parameters, and arguments. Commands are standalone words at * the beginning of the line, of which there may be multiple Parameters are single words followed by an '=' * character Arguments are single words or a block of quoted text following an '=' character. * *

Format: command [command....] parameter=argument [parameter = argument] * *

Examples entity create name=hello description="description of hello" help entity list * * @param line The line to parse * @param logicBlock A block of logic code to be taken literally * @return the string array list */ public List parse(final String line, final String logicBlock) { return checkFormat( mergeArguments(mergeEquals(splitOnEquals( stripAndSplitWords(mergeQuotes(splitOnChar(stripComments(line), '\"')))))), logicBlock); } /** * Strip comments from lines, comments start with a # character. * * @param line the line * @return the line without comments */ private String stripComments(final String line) { final int commentPos = line.indexOf('#'); if (commentPos == -1) { return line; } else { return line.substring(0, commentPos); } } /** * This method merges an array with separate quotes into an array with quotes delimiting the start and end of quoted * words Example [Humpty ],["],[Dumpty sat on the wall],["],[, Humpty Dumpty had ],["],["],a ["],[great],["],[ fall] * becomes [Humpty ],["Dumpty sat on the wall"],[, Humpty Dumpty had ],[""],[a],["great"],[ fall]. * * @param wordsSplitOnQuotes the words split on quotes * @return the merged array list */ private ArrayList mergeQuotes(final ArrayList wordsSplitOnQuotes) { final ArrayList wordsWithQuotesMerged = new ArrayList<>(); var wordIndex = 0; while (wordIndex < wordsSplitOnQuotes.size()) { wordIndex = mergeQuote(wordsSplitOnQuotes, wordsWithQuotesMerged, wordIndex); } return wordsWithQuotesMerged; } /** * This method merges the next set of quotes. * * @param wordsSplitOnQuotes the words split on quotes * @param wordsWithQuotesMerged the merged words * @param wordIndex the current word index * @return the next word index */ private int mergeQuote(final ArrayList wordsSplitOnQuotes, final ArrayList wordsWithQuotesMerged, int wordIndex) { if ("\"".equals(wordsSplitOnQuotes.get(wordIndex))) { var quotedWord = new StringBuilder(wordsSplitOnQuotes.get(wordIndex++)); for (; wordIndex < wordsSplitOnQuotes.size(); wordIndex++) { quotedWord.append(wordsSplitOnQuotes.get(wordIndex)); if ("\"".equals(wordsSplitOnQuotes.get(wordIndex))) { wordIndex++; break; } } var quotedWordToString = quotedWord.toString(); if (quotedWordToString.matches("^\".*\"$")) { wordsWithQuotesMerged.add(quotedWordToString); } else { throw new CommandLineException("trailing quote found in input " + wordsSplitOnQuotes); } } else { wordsWithQuotesMerged.add(wordsSplitOnQuotes.get(wordIndex++)); } return wordIndex; } /** * This method splits the words on an array list into an array list where each portion of the line is split into * words by '=', quoted words are ignored Example: aaa = bbb = ccc=ddd=eee = becomes [aaa ],[=],[bbb * ],[=],[ccc],[=],[ddd],[=],[eee ],[=]. * * @param words the words * @return the merged array list */ private ArrayList splitOnEquals(final ArrayList words) { final ArrayList wordsSplitOnEquals = new ArrayList<>(); for (final String word : words) { // Is this a quoted word ? if (word.startsWith("\"")) { wordsSplitOnEquals.add(word); continue; } // Split on equals character final ArrayList splitWords = splitOnChar(word, '='); wordsSplitOnEquals.addAll(splitWords); } return wordsSplitOnEquals; } /** * This method merges an array with separate equals into an array with equals delimiting the start of words Example: * [aaa ],[=],[bbb ],[=],[ccc],[=],[ddd],[=],[eee ],[=] becomes [aaa ],[= bbb ],[= ccc],[=ddd],[=eee ],[=]. * * @param wordsSplitOnEquals the words split on equals * @return the merged array list */ private ArrayList mergeEquals(final ArrayList wordsSplitOnEquals) { final ArrayList wordsWithEqualsMerged = new ArrayList<>(); var wordIndex = 0; while (wordIndex < wordsSplitOnEquals.size()) { // Is this a quoted word ? if (wordsSplitOnEquals.get(wordIndex).startsWith("\"")) { wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex)); continue; } if ("=".equals(wordsSplitOnEquals.get(wordIndex))) { if (wordIndex < wordsSplitOnEquals.size() - 1 && !wordsSplitOnEquals.get(wordIndex + 1).startsWith("=")) { wordsWithEqualsMerged.add( wordsSplitOnEquals.get(wordIndex) + wordsSplitOnEquals.get(wordIndex + 1)); wordIndex += 2; } else { wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex++)); } } else { wordsWithEqualsMerged.add(wordsSplitOnEquals.get(wordIndex++)); } } return wordsWithEqualsMerged; } /** * This method merges words that start with an '=' character with the previous word if that word does not start with * an '='. * * @param words the words * @return the merged array list */ private ArrayList mergeArguments(final ArrayList words) { final ArrayList mergedArguments = new ArrayList<>(); for (var i = 0; i < words.size(); i++) { // Is this a quoted word ? if (words.get(i).startsWith("\"")) { mergedArguments.add(words.get(i)); continue; } if (words.get(i).startsWith("=")) { if (i > 0 && !words.get(i - 1).startsWith("=")) { mergedArguments.remove(mergedArguments.size() - 1); mergedArguments.add(words.get(i - 1) + words.get(i)); } else { mergedArguments.add(words.get(i)); } } else { mergedArguments.add(words.get(i)); } } return mergedArguments; } /** * This method strips all non quoted white space down to single spaces and splits non-quoted words into separate * words. * * @param words the words * @return the array list with white space stripped and words split */ private ArrayList stripAndSplitWords(final ArrayList words) { final ArrayList strippedAndSplitWords = new ArrayList<>(); for (String word : words) { // Is this a quoted word if (word.startsWith("\"")) { strippedAndSplitWords.add(word); } else { // Split the word on blanks strippedAndSplitWords.addAll(stripAndSplitWord(word)); } } return strippedAndSplitWords; } /** * Strip and split a word on blanks into an array of words split on blanks. * * @param word the word to split * @return the array of split words */ private Collection stripAndSplitWord(final String word) { final ArrayList strippedAndSplitWords = new ArrayList<>(); // Strip white space by replacing all white space with blanks and then removing leading // and trailing blanks String singleSpaceWord = word.replaceAll("\\s+", " ").trim(); if (singleSpaceWord.length() == 0) { return strippedAndSplitWords; } // Split on space characters final String[] splitWords = singleSpaceWord.split(" "); Collections.addAll(strippedAndSplitWords, splitWords); return strippedAndSplitWords; } /** * This method splits a line of text into an array list where each portion of the line is split into words by a * character, with the characters themselves as separate words Example: Humpty "Dumpty sat on the wall", Humpty * Dumpty had ""a "great" fall becomes [Humpty ],["],[Dumpty sat on the wall],["],[, Humpty Dumpty had ],["],["],a * ["],[great],["],[ fall]. * * @param line the input line * @param splitChar the split char * @return the split array list */ private ArrayList splitOnChar(final String line, final char splitChar) { final ArrayList wordsSplitOnQuotes = new ArrayList<>(); var currentPos = 0; while (currentPos != -1) { final int quotePos = line.indexOf(splitChar, currentPos); if (quotePos != -1) { if (currentPos < quotePos) { wordsSplitOnQuotes.add(line.substring(currentPos, quotePos)); } wordsSplitOnQuotes.add("" + splitChar); currentPos = quotePos + 1; if (currentPos == line.length()) { currentPos = -1; } } else { wordsSplitOnQuotes.add(line.substring(currentPos)); currentPos = quotePos; } } return wordsSplitOnQuotes; } /** * This method checks that an array list containing a command is in the correct format. * * @param commandWords the command words * @param logicBlock A block of logic code to be taken literally * @return the checked array list */ private ArrayList checkFormat(final ArrayList commandWords, final String logicBlock) { // There should be at least one word if (commandWords.isEmpty()) { return commandWords; } // The first word must be alphanumeric, that is a command if (!commandWords.get(0).matches("^[a-zA-Z0-9]*$")) { throw new CommandLineException( "first command word is not alphanumeric or is not a command: " + commandWords.get(0)); } // Now check that we have a sequence of commands at the beginning var currentWordPos = 0; for (; currentWordPos < commandWords.size(); currentWordPos++) { if (!commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]*$")) { break; } } for (; currentWordPos < commandWords.size(); ++currentWordPos) { if (currentWordPos == commandWords.size() - 1 && logicBlock != null) { // for the last command, if the command ends with = and there is a Logic block if (commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]+=")) { commandWords.set(currentWordPos, commandWords.get(currentWordPos) + logicBlock); } else { throw new CommandLineException( "command argument is not properly formed: " + commandWords.get(currentWordPos)); } } else if (!commandWords.get(currentWordPos).matches("^[a-zA-Z0-9]+=[a-zA-Z0-9/\"].*$")) { // Not a last command, or the last command, but there is no logic block - wrong pattern throw new CommandLineException( "command argument is not properly formed: " + commandWords.get(currentWordPos)); } } return commandWords; } }