/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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.
*/
package com.cinchapi.concourse.shell;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import com.google.common.base.CaseFormat;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* A collection of tools for dealing with syntax in the {@link ConcourseShell}.
*
* @author Jeff Nelson
*/
public final class SyntaxTools {
/**
* Check {@code line} to see if it is a function call that is missing any
* commas among arguments.
*
* @param line
* @param methods
* @return the line with appropriate argument commas
*/
public static String handleMissingArgCommas(String line,
List<String> methods) {
int hashCode = methods.hashCode();
Set<String> hashedMethods = CACHED_METHODS.get(hashCode);
if(hashedMethods == null) {
hashedMethods = Sets.newHashSetWithExpectedSize(methods.size());
hashedMethods.addAll(methods);
CACHED_METHODS.put(hashCode, hashedMethods);
}
char[] chars = line.toCharArray();
StringBuilder transformed = new StringBuilder();
StringBuilder gather = new StringBuilder();
boolean foundMethod = false;
boolean inSingleQuotes = false;
boolean inDoubleQuotes = false;
int parenCount = 0;
for (char c : chars) {
if(Character.isWhitespace(c) && !foundMethod) {
transformed.append(gather);
transformed.append(c);
foundMethod = hashedMethods.contains(gather.toString());
gather.setLength(0);
}
else if(Character.isWhitespace(c) && foundMethod) {
if(transformed.charAt(transformed.length() - 1) != ','
&& !inSingleQuotes && !inDoubleQuotes && c != '\n') {
transformed.append(",");
}
transformed.append(c);
}
else if(c == '(' && !foundMethod) {
parenCount++;
transformed.append(gather);
transformed.append(c);
foundMethod = hashedMethods.contains(gather.toString());
gather.setLength(0);
}
else if(c == '(' && foundMethod) {
parenCount++;
transformed.append(c);
}
else if(c == ';') {
transformed.append(c);
foundMethod = false;
parenCount = 0;
}
else if(c == ')') {
parenCount--;
transformed.append(c);
foundMethod = parenCount == 0 ? false : foundMethod;
}
else if(c == '"') {
transformed.append(c);
inSingleQuotes = !inSingleQuotes;
}
else if(c == '\'') {
transformed.append(c);
inDoubleQuotes = !inDoubleQuotes;
}
else if(foundMethod) {
transformed.append(c);
}
else {
gather.append(c);
}
}
transformed.append(gather);
return transformed.toString();
}
/**
* Check to see if {@code line} is a command that uses short syntax. Short
* syntax allows the user to call an API method without starting the command
* with {@code concourse.}. This method compares the line to the list of
* {@code options} to see if it should be "expanded" from short syntax.
* Otherwise, the original line is returned.
*
* @param line
* @param options
* @return the expanded line, if it is using short syntax, otherwise the
* original line
*/
public static String handleShortSyntax(String line, List<String> options) {
final String prepend = "concourse.";
if(line.equalsIgnoreCase("time") || line.equalsIgnoreCase("date")) {
return line + " \"now\"";
}
else if(!line.contains("(")) {
// If there are no parens in the line, then we assume that this is a
// single(e.g non-nested) function invocation.
if(line.startsWith(prepend)) {
boolean hasArgs = line.split("\\s+").length > 1;
if(!hasArgs) {
line += "()";
}
return line;
}
else {
String[] query = line.split("\\s+");
String cmd = query[0];
if(cmd.contains("_")) { // CON-457,GH-182
String replacement = CaseFormat.LOWER_UNDERSCORE.to(
CaseFormat.LOWER_CAMEL, cmd);
line = line.replaceFirst(cmd, replacement);
}
String expanded = prepend + line.trim();
Pattern pattern = Pattern.compile(expanded.split("\\s|\\(")[0]);
for (String option : options) {
if(pattern.matcher(option).matches()) {
boolean hasArgs = expanded.split("\\s+").length > 1;
if(!hasArgs) {
expanded += "()";
}
return expanded;
}
}
}
}
else {
Set<String> shortInvokedMethods = parseShortInvokedMethods(line);
for (String method : shortInvokedMethods) {
if(options.contains(prepend + method)) {
line = line.replaceAll("(?<!\\_)" + method + "\\(", prepend
+ method + "\\(");
}
}
}
return line;
}
/**
* Examine a line and parse out the names of all the methods that are being
* invoked using short syntax.
* <p>
* e.g. methodA(methodB(x), time(y), methodC(concourse.methodD())) -->
* methodA, methodB, methodC
* </p>
*
* @param line
* @return the set of all the methods which are being invoked using short
* syntax
*/
protected static Set<String> parseShortInvokedMethods(String line) { // visible
// for
// testing
Set<String> methods = Sets.newHashSet();
Set<String> blacklist = Sets.newHashSet("time", "date");
String regex = "\\b(?!" + StringUtils.join(blacklist, "|")
+ ")[\\w\\.]+\\("; // match any word followed by an paren except
// for the blacklist
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
if(!matcher.group().startsWith("concourse.")) {
methods.add(matcher.group().replace("(", ""));
}
}
return methods;
}
/**
* For the methods in this class that take a list of callable methods, this
* collection will map the hashcode of that list to a hashset with the same
* methods. The hashset can be used for more efficient O(1) lookups as
* opposed to always iterating through the list.
*/
private static Map<Integer, Set<String>> CACHED_METHODS = Maps
.newHashMapWithExpectedSize(1);
private SyntaxTools() {/* noop */}
}