/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.controller;
import java.util.Stack;
import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;
import org.jboss.dmr.ValueExpression;
/**
* Basic {@link ExpressionResolver} implementation.
*
* @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
*/
public class ExpressionResolverImpl implements ExpressionResolver {
private static final int INITIAL = 0;
private static final int GOT_DOLLAR = 1;
private static final int GOT_OPEN_BRACE = 2;
private final boolean lenient;
/**
* Creates a new {@code ExpressionResolverImpl} configured to throw an OFE
* when it encounters an unresolvable expression.
*/
protected ExpressionResolverImpl() {
this(false);
}
/**
* Creates a new {@code ExpressionResolverImpl} with configurable behavior as to whether it throws an OFE
* when it encounters an unresolvable expression.
*
* @param lenient {@code false} if an OFE should be thrown if an unresolvable expression is found; {@code true}
* if the node should be left as an unresolved expression
*/
protected ExpressionResolverImpl(boolean lenient) {
this.lenient = lenient;
}
@Override
public final ModelNode resolveExpressions(final ModelNode node) throws OperationFailedException {
return resolveExpressionsRecursively(node);
}
/**
* Examine the given model node, resolving any expressions found within, including within child nodes.
*
* @param node the node
* @return a node with all expressions resolved
* @throws OperationFailedException if an expression cannot be resolved
*/
private ModelNode resolveExpressionsRecursively(final ModelNode node) throws OperationFailedException {
if (!node.isDefined()) {
return node;
}
ModelType type = node.getType();
ModelNode resolved;
if (type == ModelType.EXPRESSION) {
resolved = resolveExpressionStringRecursively(node.asExpression().getExpressionString(), lenient, true);
} else if (type == ModelType.OBJECT) {
resolved = node.clone();
for (Property prop : resolved.asPropertyList()) {
resolved.get(prop.getName()).set(resolveExpressionsRecursively(prop.getValue()));
}
} else if (type == ModelType.LIST) {
resolved = node.clone();
ModelNode list = new ModelNode();
list.setEmptyList();
for (ModelNode current : resolved.asList()) {
list.add(resolveExpressionsRecursively(current));
}
resolved = list;
} else if (type == ModelType.PROPERTY) {
resolved = node.clone();
resolved.set(resolved.asProperty().getName(), resolveExpressionsRecursively(resolved.asProperty().getValue()));
} else {
resolved = node;
}
return resolved;
}
/**
* Attempt to resolve the expression {@link org.jboss.dmr.ModelNode#asString() encapsulated in the given node},
* setting the value of {@code node} to the resolved string if successful, or leaving {@code node} unaltered
* if the expression is not of a form resolvable by this method. When this method returns, the type of {@code node}
* should either be {@link ModelType#STRING} if this method was able to resolve, or {@link ModelType#EXPRESSION} if
* not.
* <p>
* The default implementation does nothing.
* </p>
*
* @param node a node of type {@link ModelType#EXPRESSION}
*
* @throws OperationFailedException if the expression in {@code node} is of a form that should be resolvable by this
* method but some resolution failure occurs
*/
protected void resolvePluggableExpression(ModelNode node) throws OperationFailedException {
}
/**
* Attempt to resolve the given expression string, recursing if resolution of one string produces
* another expression.
*
* @param expressionString the expression string from a node of {@link ModelType#EXPRESSION}
* @param ignoreDMRResolutionFailure {@code false} if {@link org.jboss.dmr.ModelNode#resolve() basic DMR resolution}
* failures should be ignored, and {@code new ModelNode(expressionType.asString())} returned
* @param initial {@code true} if this call originated outside this method; {@code false} if it is a recursive call
*
* @return a node of {@link ModelType#STRING} where the encapsulated string is the resolved expression, or a node
* of {@link ModelType#EXPRESSION} if {@code ignoreDMRResolutionFailure} and {@code initial} are
* {@code true} and the string could not be resolved.
*
* @throws OperationFailedException if the expression cannot be resolved
*/
private ModelNode resolveExpressionStringRecursively(final String expressionString, final boolean ignoreDMRResolutionFailure,
final boolean initial) throws OperationFailedException {
ParseAndResolveResult resolved = parseAndResolve(expressionString, ignoreDMRResolutionFailure);
if (resolved.recursive) {
// Some part of expressionString resolved into a different expression.
// So, start over, ignoring failures. Ignore failures because we don't require
// that expressions must not resolve to something that *looks like* an expression but isn't
return resolveExpressionStringRecursively(resolved.result, true, false);
} else if (resolved.modified) {
// Typical case
return new ModelNode(resolved.result);
} else if (initial && EXPRESSION_PATTERN.matcher(expressionString).matches()) {
// We should only get an unmodified expression string back if there was a resolution
// failure that we ignored.
assert ignoreDMRResolutionFailure;
// expressionString came from a node of type expression, so since we did nothing send it back in the same type
return new ModelNode(new ValueExpression(expressionString));
} else {
// The string wasn't really an expression. Two possible cases:
// 1) if initial == true, someone created a expression node with a non-expression string, which is legal
// 2) if initial == false, we resolved from an ModelType.EXPRESSION to a string that looked like an
// expression but can't be resolved. We don't require that expressions must not resolve to something that
// *looks like* an expression but isn't, so we'll just treat this as a string
return new ModelNode(expressionString);
}
}
private ParseAndResolveResult parseAndResolve(final String initialValue, boolean lenient) throws OperationFailedException {
final StringBuilder builder = new StringBuilder();
final int len = initialValue.length();
int state = INITIAL;
int ignoreBraceLevel = 0;
boolean modified = false;
Stack<OpenExpression> stack = null;
for (int i = 0; i < len; i = initialValue.offsetByCodePoints(i, 1)) {
final int ch = initialValue.codePointAt(i);
switch (state) {
case INITIAL: {
switch (ch) {
case '$': {
stack = addToStack(stack, i);
state = GOT_DOLLAR;
continue;
}
default: {
builder.appendCodePoint(ch);
continue;
}
}
// not reachable
}
case GOT_DOLLAR: {
switch (ch) {
case '{': {
state = GOT_OPEN_BRACE;
continue;
}
default: {
// Previous $ was not the start of an expression
if (stack.size() == 1) {
// not in an outer expression; store to the builder and resume
stack.clear(); // looks faster than pop()
if (ch != '$') {
// Preceding $ wasn't an escape, so restore it
builder.append('$');
} else {
modified = true; // since we discarded the '$'
}
builder.appendCodePoint(ch);
state = INITIAL;
} else {
// We're in an outer expression, so just discard the top stack element
// created when we saw the '$' and resume tracking the outer expression
stack.pop();
if(ch == '$') {
modified = true; // since we discarded the '$'
} else if (ch == '}') {//this may be the end of the outer expression
i--;
}
state = GOT_OPEN_BRACE;
}
continue;
}
}
// not reachable
}
case GOT_OPEN_BRACE: {
switch (ch) {
case '$': {
stack.push(new OpenExpression(i));
state = GOT_DOLLAR;
continue;
}
case '{': {
ignoreBraceLevel++;
continue;
}
case '}': {
if (ignoreBraceLevel > 0) {
ignoreBraceLevel--;
continue;
}
String toResolve = getStringToResolve(initialValue, stack, i);
final String resolved = resolveExpressionString(toResolve);
// We only successfully resolved if toResolve != resolved
if (!toResolve.equals(resolved)) {
if (EXPRESSION_PATTERN.matcher(resolved).matches()) {
// The resolved value is itself an expression, so
// there will need to be another pass.
// We need to discard any changes made from initialValue
// prior to this expression, because if there were any
// escaped $ sequences in there, we can't lose the escape char
return createRecursiveResult(initialValue, resolved, stack, i);
}
// Non-recursive case
// Update the stack
recordResolutionInStack(resolved, stack);
if (stack.size() == 0) {
// All expressions so far are resolved; record for output
builder.append(resolved);
state = INITIAL;
} else {
// We resolved a nested expression; keep going with the outer one
state = GOT_OPEN_BRACE;
}
modified = true;
continue;
} else if (stack.size() > 1) {
// We don't fail the overall resolution due to not resolving a nested expression,
// as the nested part may be irrelevant to the final resolution.
// For example '${bar}' is irrelevant to resolving '${foo:${bar}}'
// if system property 'foo' is set.
// Clean up stack and store toResolve so we don't have to build it again
// when we get to the end of the outer expression
recordResolutionInStack(toResolve, stack);
state = GOT_OPEN_BRACE;
continue;
} else if (lenient) {
// just respond with the initial value
return new ParseAndResolveResult(initialValue, false, false);
} else {
throw ControllerLogger.ROOT_LOGGER.cannotResolveExpression(initialValue);
}
}
default: {
continue;
}
}
// not reachable
}
default:
// If we reach this, there's a programming error in this class
throw new IllegalStateException();
}
}
if (stack != null && stack.size() > 0) {
if (state == GOT_DOLLAR) {
stack.pop();
}
if (stack.size() > 0) {
throw ControllerLogger.ROOT_LOGGER.incompleteExpression(initialValue);
}
// Stack was a single item due to GOT_DOLLAR. Need to restore the lost $
builder.append('$');
}
return new ParseAndResolveResult(builder.toString(), modified, false);
}
private static Stack<OpenExpression> addToStack(Stack<OpenExpression> stack, int startIndex) {
Stack<OpenExpression> result = stack == null ? new Stack<OpenExpression>() : stack;
result.push(new OpenExpression(startIndex));
return result;
}
/** Resolve the given string using any plugin and the DMR resolve method */
private String resolveExpressionString(final String unresolvedString) throws OperationFailedException {
// parseAndResolve should only be providing expressions with no leading or trailing chars
assert unresolvedString.startsWith("${") && unresolvedString.endsWith("}");
// Default result is no change from input
String result = unresolvedString;
ModelNode resolveNode = new ModelNode(new ValueExpression(unresolvedString));
// Try plug-in resolution; i.e. vault
resolvePluggableExpression(resolveNode);
if (resolveNode.getType() == ModelType.EXPRESSION ) {
// resolvePluggableExpression did nothing. Try standard resolution
String resolvedString = resolveStandardExpression(resolveNode);
if (!unresolvedString.equals(resolvedString)) {
// resolveStandardExpression made progress
result = resolvedString;
} // else there is nothing more we can do with this string
} else {
// resolvePluggableExpression made progress
result = resolveNode.asString();
}
return result;
}
/**
* Perform a standard {@link org.jboss.dmr.ModelNode#resolve()} on the given {@code unresolved} node.
* @param unresolved the unresolved node, which should be of type {@link org.jboss.dmr.ModelType#EXPRESSION}
* @return a node of type {@link ModelType#STRING}
*
* @throws OperationFailedException if {@code ignoreFailures} is {@code false} and the expression cannot be resolved
*/
private static String resolveStandardExpression(final ModelNode unresolved) throws OperationFailedException {
try {
return unresolved.resolve().asString();
} catch (SecurityException e) {
// A security exception should propagate no matter what the value of ignoreUnresolvable is. The first call to
// this method for any expression will have ignoreUnresolvable set to 'false' which means a basic test of
// ability to read system properties will have already passed. So a failure with ignoreUnresolvable set to
// true means a specific property caused the failure, and that should not be ignored
throw new OperationFailedException(ControllerLogger.ROOT_LOGGER.noPermissionToResolveExpression(unresolved, e));
} catch (IllegalStateException e) {
return unresolved.asString();
}
}
private static String getStringToResolve(String initialValue, Stack<OpenExpression> stack, int expressionEndIndex) {
int stackSize = stack.size();
int expressionElement = -1;
OpenExpression firstUnresolved = null;
for (int i = stackSize - 1; i >= 0; i--) {
OpenExpression oe = stack.get(i);
if (oe.resolvedValue == null) {
expressionElement = i;
firstUnresolved = oe;
break;
}
}
assert expressionElement > -1;
// Now we know how long this expression is
firstUnresolved.endIndex = expressionEndIndex;
if (expressionElement == stackSize - 1) {
// Simple case; no already resolved nested stuff to patch in
return initialValue.substring(firstUnresolved.startIndex, expressionEndIndex + 1);
} else {
// Compose the new expression from the original and resolved nested elements
StringBuilder sb = new StringBuilder();
int nextStart = firstUnresolved.startIndex;
for (int i = expressionElement + 1; i < stackSize; i++) {
OpenExpression oe = stack.get(i);
sb.append(initialValue.substring(nextStart, oe.startIndex));
sb.append(oe.resolvedValue);
nextStart = oe.endIndex + 1;
}
// Add the last bits, which will at least be the trailing '}' at expressionEndIndex
sb.append(initialValue.substring(nextStart, expressionEndIndex + 1));
return sb.toString();
}
}
private static ParseAndResolveResult createRecursiveResult(String initialValue, String val,
Stack<OpenExpression> stack, int expressionEndIndex) {
int initialLength = initialValue.length();
int expressionIndex = -1;
while (expressionIndex == -1) {
OpenExpression oe = stack.pop();
if (oe.resolvedValue == null) {
expressionIndex = oe.startIndex;
}
}
String result;
if (expressionIndex == 0 && expressionEndIndex == initialLength -1) {
// basic case
result = val;
} else if (expressionIndex == 0) {
result = val + initialValue.substring(expressionEndIndex + 1);
} else {
StringBuilder sb = new StringBuilder(initialValue.substring(0, expressionIndex));
sb.append(val);
if (expressionEndIndex < initialLength - 1) {
sb.append(initialValue.substring(expressionEndIndex + 1));
}
result = sb.toString();
}
return new ParseAndResolveResult(result, true, true);
}
private static void recordResolutionInStack(String val, Stack<OpenExpression> stack) {
for (int i = stack.size() -1; i >= 0; i--) {
OpenExpression oe = i == 0 ? stack.pop() : stack.peek();
if (oe.resolvedValue == null) {
oe.resolvedValue = val;
break;
} else {
assert i > 0;
// Don't need the nested data any more
stack.pop();
}
}
}
private static class ParseAndResolveResult {
private final String result;
private final boolean modified;
private final boolean recursive;
private ParseAndResolveResult(String result, boolean modified, boolean recursive) {
this.result = result;
this.modified = modified;
this.recursive = recursive;
}
}
private static class OpenExpression {
private final int startIndex;
private int endIndex = -1;
private String resolvedValue;
private OpenExpression(int startIndex) {
this.startIndex = startIndex;
}
}
}