/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*
*/
/*
* Created on Jul 25, 2003
*/
package org.apache.jmeter.engine.util;
import java.io.IOException;
import java.io.StringReader;
import java.util.LinkedList;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.functions.Function;
import org.apache.jmeter.functions.InvalidVariableException;
import org.apache.jmeter.testelement.TestStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses function / variable references of the form
* ${functionName[([var[,var...]])]}
* and
* ${variableName}
*/
class FunctionParser {
private static final Logger log = LoggerFactory.getLogger(FunctionParser.class);
/**
* Compile a general string into a list of elements for a CompoundVariable.
*
* Calls {@link #makeFunction(StringReader)} if it detects an unescaped "${".
*
* Removes escapes from '$', ',' and '\'.
*
* @param value string containing the function / variable references (if any)
*
* @return list of Strings or Objects representing functions
* @throws InvalidVariableException when evaluation of variables fail
*/
LinkedList<Object> compileString(String value) throws InvalidVariableException {
StringReader reader = new StringReader(value);
LinkedList<Object> result = new LinkedList<>();
StringBuilder buffer = new StringBuilder();
char previous = ' '; // TODO - why use space?
char[] current = new char[1];
try {
while (reader.read(current) == 1) {
if (current[0] == '\\') { // Handle escapes
previous = current[0];
if (reader.read(current) == 0) {
break;
}
// Keep '\' unless it is one of the escapable chars '$' ',' or '\'
// N.B. This method is used to parse function parameters, so must treat ',' as special
if (current[0] != '$' && current[0] != ',' && current[0] != '\\') {
buffer.append(previous); // i.e. '\\'
}
previous = ' ';
buffer.append(current[0]);
} else if (current[0] == '{' && previous == '$') {// found "${"
buffer.deleteCharAt(buffer.length() - 1);
if (buffer.length() > 0) {// save leading text
result.add(buffer.toString());
buffer.setLength(0);
}
result.add(makeFunction(reader));
previous = ' ';
} else {
buffer.append(current[0]);
previous = current[0];
}
}
if (buffer.length() > 0) {
result.add(buffer.toString());
}
} catch (IOException e) {
log.error("Error parsing function: {}", value, e);
result.clear();
result.add(value);
}
if (result.size() == 0) {
result.add("");
}
return result;
}
/**
* Compile a string into a function or SimpleVariable.
*
* Called by {@link #compileString(String)} when that has detected "${".
*
* Calls {@link CompoundVariable#getNamedFunction(String)} if it detects:
* '(' - start of parameter list
* '}' - end of function call
*
* @param reader points to input after the "${"
* @return the function or variable object (or a String)
* @throws InvalidVariableException when evaluation of variables fail
*/
Object makeFunction(StringReader reader) throws InvalidVariableException {
char[] current = new char[1];
char previous = ' '; // TODO - why use space?
StringBuilder buffer = new StringBuilder();
Object function;
try {
while (reader.read(current) == 1) {
if (current[0] == '\\') {
if (reader.read(current) == 0) {
break;
}
previous = ' ';
buffer.append(current[0]);
} else if (current[0] == '(' && previous != ' ') {
String funcName = buffer.toString();
function = CompoundVariable.getNamedFunction(funcName);
if (function instanceof Function) {
((Function) function).setParameters(parseParams(reader));
if (reader.read(current) == 0 || current[0] != '}') {
reader.reset();// set to start of string
char []cb = new char[100];
int nbRead = reader.read(cb);
throw new InvalidVariableException
("Expected } after "+funcName+" function call in "+new String(cb, 0, nbRead));
}
if (function instanceof TestStateListener) {
StandardJMeterEngine.register((TestStateListener) function);
}
return function;
} else { // Function does not exist, so treat as per missing variable
buffer.append(current[0]);
}
} else if (current[0] == '}') {// variable, or function with no parameter list
function = CompoundVariable.getNamedFunction(buffer.toString());
if (function instanceof Function){// ensure that setParameters() is called.
((Function) function).setParameters(new LinkedList<CompoundVariable>());
}
buffer.setLength(0);
return function;
} else {
buffer.append(current[0]);
previous = current[0];
}
}
} catch (IOException e) {
log.error("Error parsing function: {}", buffer, e);
return null;
}
log.warn("Probably an invalid function string: {}", buffer);
return buffer.toString();
}
/**
* Compile a String into a list of parameters, each made into a
* CompoundVariable.
*
* Parses strings of the following form:
* <ul>
* <li>text)</li>
* <li>text,text)</li>
* <li></li>
* </ul>
* @param reader a StringReader pointing to the current input location, just after "("
* @return a list of CompoundVariable elements
* @throws InvalidVariableException when evaluation of variables fail
*/
LinkedList<CompoundVariable> parseParams(StringReader reader) throws InvalidVariableException {
LinkedList<CompoundVariable> result = new LinkedList<>();
StringBuilder buffer = new StringBuilder();
char[] current = new char[1];
char previous = ' ';
int functionRecursion = 0;
int parenRecursion = 0;
try {
while (reader.read(current) == 1) {
if (current[0] == '\\') { // Process escaped characters
buffer.append(current[0]); // Store the \
if (reader.read(current) == 0) {
break; // end of buffer
}
previous = ' ';
buffer.append(current[0]); // store the following character
} else if (current[0] == ',' && functionRecursion == 0) {
CompoundVariable param = new CompoundVariable();
param.setParameters(buffer.toString());
buffer.setLength(0);
result.add(param);
} else if (current[0] == ')' && functionRecursion == 0 && parenRecursion == 0) {
// Detect functionName() so this does not generate empty string as the parameter
if (buffer.length() == 0 && result.isEmpty()){
return result;
}
// Normal exit occurs here
CompoundVariable param = new CompoundVariable();
param.setParameters(buffer.toString());
buffer.setLength(0);
result.add(param);
return result;
} else if (current[0] == '{' && previous == '$') {
buffer.append(current[0]);
previous = current[0];
functionRecursion++;
} else if (current[0] == '}' && functionRecursion > 0) {
buffer.append(current[0]);
previous = current[0];
functionRecursion--;
} else if (current[0] == ')' && functionRecursion == 0 && parenRecursion > 0) {
buffer.append(current[0]);
previous = current[0];
parenRecursion--;
} else if (current[0] == '(' && functionRecursion == 0) {
buffer.append(current[0]);
previous = current[0];
parenRecursion++;
} else {
buffer.append(current[0]);
previous = current[0];
}
}
} catch (IOException e) {// Should not happen with StringReader
log.error("Error parsing function: {}", buffer, e);
}
// Dropped out, i.e. did not find closing ')'
log.warn("Probably an invalid function string: {}", buffer);
CompoundVariable var = new CompoundVariable();
var.setParameters(buffer.toString());
result.add(var);
return result;
}
}