/*******************************************************************************
* Copyright (c) 2013 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.foundation.core.expressions;
import java.io.File;
import java.util.Map;
import java.util.Properties;
/**
* A resolver for expressions.
*
* This class requires a IVariableResolver to be in charge of
* resolving variables. Clients may choose to use a variety
* of backing datastores from which they can resolve their variables,
* such as System Properties, a Properties object, a Map, or a private
* model such as the eclipse variables plugin.
*
* Some IVariableResolver may choose to treat the argument as a
* default value, while others may choose to treat it as an important
* parameter for resolving the variable, depending on their specific
* usecases.
*
* Syntax:
* Given an IVariableResolver who's backing store will return "bar" for "foo"
* and treats the argument as a default:
* $(empty) => ExpressionResolutionException
* ${foo} => bar
* ${empty:argument} => argument
* ${empty,foo:argument} => bar
*
* Given an IVariableResolver who's backing store treats the argument
* as a critical part of the query, look at these examples of a
* 4-wheel, 5-seat vehicle variable:
*
* ${numWheels} => ExpressionResolutionException // argument is required but not provided
* ${numWheels:myCar} => 4 // correct usage
* ${numSeats, numWheels:myCar} => 4 // comma syntax leads to inappropriate result
*
* The last example may seem counter-intuitive. Some may feel
* that the myCar argument should distributed over both
* the numSeats and the numWheels variables.
*
* It will not be. The argument will ONLY be used for the resolution
* of the FINAL variable in the string. In the above example,
* numSeats failed to resolve, because it did not have an associated argument.
*
* Be aware of this restriction when using the argument as a critical part of the query!
* Clients are in charge of ensuring that the syntax used in their strings fit their usecase.
* Clients who require the argument to be treated as a critical component of the variable
* resolution should not allow the use of comma syntax, and should instead use only a subset
* of the features supported by this ExpressionResolver.
*
*
* Originally based on ValueExpressionResolver from jboss-dmr.
*
* This class has been modified substantially to fit it into the
* eclipse workflow for variable resolution
*
* The state DEFAULT has been renamed to ARGUMENT for clarity and consistancy.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
* @author <a href="mailto:rob.stryker@redhat.com">Rob Stryker</a>
* @since 1.1
*/
public class ExpressionResolver {
private static final int INITIAL = 0;
private static final int GOT_DOLLAR = 1;
private static final int GOT_OPEN_BRACE = 2;
private static final int RESOLVED = 3;
private static final int ARGUMENT = 4;
/**
* The private instance of the resolver we'll be using to resolve variables
*/
private IVariableResolver resolver;
/**
* Construct a new instance using system properties
* and environment variable maps to resolve variables.
*/
public ExpressionResolver() {
this(new SystemPropertiesVariableResolver());
}
/**
* Construct a new instance using a map as the backing
* variable resolver. If the map's value objects
* are not String objects, the value of the toString
* method is what will be used.
*/
public ExpressionResolver(Map<String, ? extends Object> map) {
this(new MapVariableResolver(map));
}
/**
* Construct a new instance using a Properties object as the backing
* variable resolver. If the Properties value objects
* are not String objects, the value of the toString
* method is what will be used.
*/
public ExpressionResolver(Properties props) {
this(new PropertiesVariableResolver(props));
}
/**
* Construct a new instance with an arbitrary {@link IVariableResolver}
*
* @param resolver A variable resolver
*/
public ExpressionResolver(IVariableResolver resolver) {
this.resolver = resolver;
}
/**
* Perform expression resolution.
* In the event of any errors, simply return the original string instead.
*
* @param expression the expression to resolve
* @return the resolved string, or the original string if there were errors resolving
*/
public String resolveIgnoreErrors(final String value) {
try {
return resolve(value);
} catch(ExpressionResolutionException ise) {
return value; // Just return the string unchanged
}
}
/**
* Perform expression resolution.
*
* @param expression the expression to resolve
* @return the resolved string
*/
public String resolve(final String value) throws ExpressionResolutionException {
if(value==null) return null;
final StringBuilder builder = new StringBuilder();
final int len = value.length();
int state = INITIAL;
int start = -1;
int nest = 0;
int nameStart = -1;
int nameEnd = -1;
String resolvedValue = null;
for (int i = 0; i < len; i = value.offsetByCodePoints(i, 1)) {
final int ch = value.codePointAt(i);
switch (state) {
case INITIAL: {
switch (ch) {
case '$': {
state = GOT_DOLLAR;
continue;
}
default: {
builder.appendCodePoint(ch);
continue;
}
}
// not reachable
}
case GOT_DOLLAR: {
switch (ch) {
case '$': {
builder.appendCodePoint(ch);
state = INITIAL;
continue;
}
case '{': {
start = i + 1;
nameStart = start;
nameEnd = start;
state = GOT_OPEN_BRACE;
continue;
}
default: {
// invalid; emit and resume
builder.append('$').appendCodePoint(ch);
state = INITIAL;
continue;
}
}
// not reachable
}
case GOT_OPEN_BRACE: {
switch (ch) {
case '{': {
nest++;
continue;
}
case ':':
case '}':
case ',': {
if (nest > 0) {
if (ch == '}') nest--;
continue;
}
if (ch == ',') {
// The next char is a comma. This variable should
// attempt to be resolved with no argument
final String val2 = resolveVariable(value.substring(nameStart, i).trim(), null);
if (val2 != null) {
builder.append(val2);
resolvedValue = val2;
state = ch == '}' ? INITIAL : RESOLVED;
continue;
}
// Resolution has failed, but, we can move on to the next variable after the comma
nameStart = i + 1;
continue;
} else if (ch == ':') {
// We are now looking to harvest the argument
nameEnd = i;
start = i + 1;
state = ARGUMENT;
continue;
} else {
final String val2 = resolveVariable(value.substring(nameStart, i).trim(), null);
if (val2 != null) {
builder.append(val2);
resolvedValue = val2;
state = ch == '}' ? INITIAL : RESOLVED;
continue;
}
throw new ExpressionResolutionException("Failed to resolve expression: "+ value.substring(start - 2, i + 1));
}
}
default: {
continue;
}
}
// not reachable
}
case RESOLVED: {
if (ch == '{') {
nest ++;
} else if (ch == '}') {
if (nest > 0) {
nest--;
} else {
state = INITIAL;
}
}
continue;
}
case ARGUMENT: {
if (ch == '{') {
nest ++;
} else if (ch == '}') {
if (nest > 0) {
nest --;
} else {
state = INITIAL;
String s1 = value.substring(nameStart, nameEnd);
String s2 = value.substring(start, i);
final String val2 = resolveVariable(s1.trim(), s2);
if (val2 != null) {
builder.append(val2);
resolvedValue = val2;
state = ch == '}' ? INITIAL : RESOLVED;
continue;
} else {
throw new ExpressionResolutionException("Failed to resolve expression: "+ s1 + " with argument: " + s2);
}
}
}
continue;
}
default:
throw new ExpressionResolutionException("Unexpected char seen: "+ch);
}
}
switch (state) {
case GOT_DOLLAR: {
builder.append('$');
break;
}
case ARGUMENT: {
builder.append(value.substring(start - 2));
break;
}
case GOT_OPEN_BRACE: {
// We had a reference that was not resolved, throw ISE
if (resolvedValue == null)
throw new ExpressionResolutionException("Incomplete expression: "+builder.toString());
break;
}
}
return builder.toString();
}
private String resolveVariable(String name, String argument) {
return resolver.resolve(name, argument);
}
/**
* This variable resolver will check a given map
* to discover the value of a variable.
*
* This resolver will treat the argument as a default value.
*/
public static class MapVariableResolver implements IVariableResolver {
private Map<String, ? extends Object> map;
public MapVariableResolver(Map<String, ? extends Object> map) {
this.map = map;
}
@Override
public String resolve(String variable, String argument) {
Object ret = map.get(variable);
return ret == null ? argument : ret.toString();
}
}
/**
* This variable resolver will check a given properties object
* to discover the value of a variable.
*
* This resolver will treat the argument as a default value.
*/
public static class PropertiesVariableResolver implements IVariableResolver {
private Properties props;
public PropertiesVariableResolver(Properties props) {
this.props = props;
}
@Override
public String resolve(String variable, String argument) {
String ret = props.getProperty(variable);
return ret == null ? argument : ret;
}
}
/**
* This variable resolver will check system properties
* and environment variables to discover the value of a variable
*
* This resolver will treat the argument as a default value.
*/
public static class SystemPropertiesVariableResolver implements IVariableResolver {
public SystemPropertiesVariableResolver() {
}
@Override
public String resolve(String variable, String argument) {
String ret = resolvePart(variable);
return ret == null ? argument : ret;
}
/*
* Resolve a single name in the expression. Return {@code null} if no resolution is possible. The default
* implementation (which may be delegated to) checks system properties, environment variables, and a small set of
* special strings.
*
* @param name the name to resolve
* @return the resolved value, or {@code null} for none
*/
private String resolvePart(String name) {
if ("/".equals(name)) {
return File.separator;
} else if (":".equals(name)) {
return File.pathSeparator;
}
// First check for a key in the provided properties, otherwise env vars.
String val = System.getProperty(name);
if (val == null && name.startsWith("env."))
val = System.getenv(name.substring(4));
return val;
}
}
}