/******************************************************************************* * 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; } } }