org.wildfly.common.expression.Expression Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including
all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and
JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.common.expression;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import org.wildfly.common.Assert;
import org.wildfly.common._private.CommonMessages;
import org.wildfly.common.function.ExceptionBiConsumer;
/**
* A compiled property-expansion expression string. An expression string is a mix of plain strings and expression
* segments, which are wrapped by the sequence "{@code ${ ... }}".
*
* @author David M. Lloyd
*/
public final class Expression {
private final Node content;
private final Set referencedStrings;
Expression(Node content) {
this.content = content;
HashSet strings = new HashSet<>();
content.catalog(strings);
referencedStrings = strings.isEmpty() ? Collections.emptySet() : strings.size() == 1 ? Collections.singleton(strings.iterator().next()) : Collections.unmodifiableSet(strings);
}
/**
* Get the immutable set of string keys that are referenced by expressions in this compiled expression. If there
* are no expansions in this expression, the set is empty. Note that this will not include any string keys
* that themselves contain expressions, in the case that {@link Flag#NO_RECURSE_KEY} was not specified.
*
* @return the immutable set of strings (not {@code null})
*/
public Set getReferencedStrings() {
return referencedStrings;
}
/**
* Evaluate the expression with the given expansion function, which may throw a checked exception. The given "function"
* is a predicate which returns {@code true} if the expansion succeeded or {@code false} if it failed (in which case
* a default value may be used). If expansion succeeds, the expansion function should append the result to the
* given {@link StringBuilder}.
*
* @param expandFunction the expansion function to apply (must not be {@code null})
* @param the exception type thrown by the expansion function
* @return the expanded string
* @throws E if the expansion function throws an exception
*/
public String evaluateException(final ExceptionBiConsumer, StringBuilder, E> expandFunction) throws E {
Assert.checkNotNullParam("expandFunction", expandFunction);
final StringBuilder b = new StringBuilder();
content.emit(new ResolveContext(expandFunction, b), expandFunction);
return b.toString();
}
/**
* Evaluate the expression with the given expansion function. The given "function"
* is a predicate which returns {@code true} if the expansion succeeded or {@code false} if it failed (in which case
* a default value may be used). If expansion succeeds, the expansion function should append the result to the
* given {@link StringBuilder}.
*
* @param expandFunction the expansion function to apply (must not be {@code null})
* @return the expanded string
*/
public String evaluate(BiConsumer, StringBuilder> expandFunction) {
return evaluateException(expandFunction::accept);
}
/**
* Evaluate the expression using a default expansion function that evaluates system and environment properties
* in the JBoss style (i.e. using the prefix {@code "env."} to designate an environment property).
* The caller must have all required security manager permissions.
*
* @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no
* default value; {@code false} to expand such keys to an empty string
* @return the expanded string
*/
public String evaluateWithPropertiesAndEnvironment(boolean failOnNoDefault) {
return evaluate((c, b) -> {
final String key = c.getKey();
if (key.startsWith("env.")) {
final String env = key.substring(4);
final String val = System.getenv(env);
if (val == null) {
if (failOnNoDefault && ! c.hasDefault()) {
throw CommonMessages.msg.unresolvedEnvironmentProperty(env);
}
c.expandDefault();
} else {
b.append(val);
}
} else {
final String val = System.getProperty(key);
if (val == null) {
if (failOnNoDefault && ! c.hasDefault()) {
throw CommonMessages.msg.unresolvedSystemProperty(key);
}
c.expandDefault();
} else {
b.append(val);
}
}
});
}
/**
* Evaluate the expression using a default expansion function that evaluates system properties.
* The caller must have all required security manager permissions.
*
* @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no
* default value; {@code false} to expand such keys to an empty string
* @return the expanded string
*/
public String evaluateWithProperties(boolean failOnNoDefault) {
return evaluate((c, b) -> {
final String key = c.getKey();
final String val = System.getProperty(key);
if (val == null) {
if (failOnNoDefault && ! c.hasDefault()) {
throw CommonMessages.msg.unresolvedSystemProperty(key);
}
c.expandDefault();
} else {
b.append(val);
}
});
}
/**
* Evaluate the expression using a default expansion function that evaluates environment properties.
* The caller must have all required security manager permissions.
*
* @param failOnNoDefault {@code true} to throw an {@link IllegalArgumentException} if an unresolvable key has no
* default value; {@code false} to expand such keys to an empty string
* @return the expanded string
*/
public String evaluateWithEnvironment(boolean failOnNoDefault) {
return evaluate((c, b) -> {
final String key = c.getKey();
final String val = System.getenv(key);
if (val == null) {
if (failOnNoDefault && ! c.hasDefault()) {
throw CommonMessages.msg.unresolvedEnvironmentProperty(key);
}
c.expandDefault();
} else {
b.append(val);
}
});
}
/**
* Compile an expression string.
*
* @param string the expression string (must not be {@code null})
* @param flags optional flags to apply which affect the compilation
* @return the compiled expression (not {@code null})
*/
public static Expression compile(String string, Flag... flags) {
return compile(string, flags == null || flags.length == 0 ? NO_FLAGS : EnumSet.of(flags[0], flags));
}
/**
* Compile an expression string.
*
* @param string the expression string (must not be {@code null})
* @param flags optional flags to apply which affect the compilation (must not be {@code null})
* @return the compiled expression (not {@code null})
*/
public static Expression compile(String string, EnumSet flags) {
Assert.checkNotNullParam("string", string);
Assert.checkNotNullParam("flags", flags);
final Node content;
final Itr itr;
if (flags.contains(Flag.NO_TRIM)) {
itr = new Itr(string);
} else {
itr = new Itr(string.trim());
}
content = parseString(itr, true, false, false, flags);
return content == Node.NULL ? EMPTY : new Expression(content);
}
private static final Expression EMPTY = new Expression(Node.NULL);
static final class Itr {
private final String str;
private int idx;
Itr(final String str) {
this.str = str;
}
boolean hasNext() {
return idx < str.length();
}
int next() {
final int idx = this.idx;
try {
return str.codePointAt(idx);
} finally {
this.idx = str.offsetByCodePoints(idx, 1);
}
}
int prev() {
final int idx = this.idx;
try {
return str.codePointBefore(idx);
} finally {
this.idx = str.offsetByCodePoints(idx, -1);
}
}
int getNextIdx() {
return idx;
}
int getPrevIdx() {
return str.offsetByCodePoints(idx, -1);
}
String getStr() {
return str;
}
int peekNext() {
return str.codePointAt(idx);
}
int peekPrev() {
return str.codePointBefore(idx);
}
void rewind(final int newNext) {
idx = newNext;
}
}
private static Node parseString(Itr itr, final boolean allowExpr, final boolean endOnBrace, final boolean endOnColon, final EnumSet flags) {
int ignoreBraceLevel = 0;
final List list = new ArrayList<>();
int start = itr.getNextIdx();
while (itr.hasNext()) {
// index of this character
int idx = itr.getNextIdx();
int ch = itr.next();
switch (ch) {
case '$': {
if (! allowExpr) {
// TP 1
// treat as plain content
continue;
}
// check to see if it's a dangling $
if (! itr.hasNext()) {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 2
throw invalidExpressionSyntax(itr.getStr(), idx);
}
// TP 3
list.add(new LiteralNode(itr.getStr(), start, itr.getNextIdx()));
start = itr.getNextIdx();
continue;
}
// enqueue what we have acquired so far
if (idx > start) {
// TP 4
list.add(new LiteralNode(itr.getStr(), start, idx));
}
// next char should be an expression starter of some sort
idx = itr.getNextIdx();
ch = itr.next();
switch (ch) {
case '{': {
// ${
boolean general = flags.contains(Flag.GENERAL_EXPANSION) && itr.hasNext() && itr.peekNext() == '{';
// consume double-{
if (general) itr.next();
// set start to the beginning of the key for later
start = itr.getNextIdx();
// the expression name starts in the next position
Node keyNode = parseString(itr, ! flags.contains(Flag.NO_RECURSE_KEY), true, true, flags);
if (! itr.hasNext()) {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 5
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// TP 6
// otherwise treat it as a properly terminated expression
list.add(new ExpressionNode(general, keyNode, Node.NULL));
start = itr.getNextIdx();
continue;
} else if (itr.peekNext() == ':') {
if (flags.contains(Flag.DOUBLE_COLON) && itr.hasNext() && itr.peekNext() == ':') {
// TP 7
// OK actually the whole thing is really going to be part of the key
// Best approach is, rewind and do it over again, but without end-on-colon
itr.rewind(start);
keyNode = parseString(itr, ! flags.contains(Flag.NO_RECURSE_KEY), true, false, flags);
list.add(new ExpressionNode(general, keyNode, Node.NULL));
} else {
// TP 8
itr.next(); // consume it
final Node defaultValueNode = parseString(itr, ! flags.contains(Flag.NO_RECURSE_DEFAULT), true, false, flags);
list.add(new ExpressionNode(general, keyNode, defaultValueNode));
}
// now expect }
if (! itr.hasNext()) {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 9
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// TP 10
// otherwise treat it as a properly terminated expression
start = itr.getNextIdx();
continue;
} else {
// TP 11
assert itr.peekNext() == '}';
itr.next(); // consume
if (general) {
if (! itr.hasNext()) {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 11_1
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// TP 11_2
// otherwise treat it as a properly terminated expression
start = itr.getNextIdx();
continue;
} else {
if (itr.peekNext() == '}') {
itr.next(); // consume it
// TP 11_3
start = itr.getNextIdx();
continue;
} else {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 11_4
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// otherwise treat it as a properly terminated expression
start = itr.getNextIdx();
continue;
}
}
} else {
start = itr.getNextIdx();
continue;
}
//throw Assert.unreachableCode();
}
} else {
// TP 12
assert itr.peekNext() == '}';
itr.next(); // consume
list.add(new ExpressionNode(general, keyNode, Node.NULL));
if (general) {
if (! itr.hasNext()) {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 12_1
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// TP 12_2
// otherwise treat it as a properly terminated expression
start = itr.getNextIdx();
continue;
} else {
if (itr.peekNext() == '}') {
itr.next(); // consume it
// TP 12_3
start = itr.getNextIdx();
continue;
} else {
if (! flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 12_4
throw invalidExpressionSyntax(itr.getStr(), itr.getNextIdx());
}
// otherwise treat it as a properly terminated expression
start = itr.getNextIdx();
continue;
}
}
}
start = itr.getNextIdx();
continue;
}
//throw Assert.unreachableCode();
}
case '$': {
// $$
if (flags.contains(Flag.MINI_EXPRS)) {
// TP 13
list.add(new ExpressionNode(false, LiteralNode.DOLLAR, Node.NULL));
} else {
// just resolve $$ to $
// TP 14
list.add(LiteralNode.DOLLAR);
}
start = itr.getNextIdx();
continue;
}
case '}': {
// $}
if (flags.contains(Flag.MINI_EXPRS)) {
// TP 15
list.add(new ExpressionNode(false, LiteralNode.CLOSE_BRACE, Node.NULL));
start = itr.getNextIdx();
continue;
} else if (endOnBrace) {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 16
// just treat the $ that we got like plain text, and return
list.add(LiteralNode.DOLLAR);
itr.prev(); // back up to point at } again
return Node.fromList(list);
} else {
// TP 17
throw invalidExpressionSyntax(itr.getStr(), idx);
}
} else {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 18
// just treat $} like plain text
list.add(LiteralNode.DOLLAR);
list.add(LiteralNode.CLOSE_BRACE);
start = itr.getNextIdx();
continue;
} else {
// TP 19
throw invalidExpressionSyntax(itr.getStr(), idx);
}
}
//throw Assert.unreachableCode();
}
case ':': {
// $:
if (flags.contains(Flag.MINI_EXPRS)) {
// $: is an expression
// TP 20
list.add(new ExpressionNode(false, LiteralNode.COLON, Node.NULL));
start = itr.getNextIdx();
continue;
} else if (endOnColon) {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 21
// just treat the $ that we got like plain text, and return
itr.prev(); // back up to point at : again
list.add(LiteralNode.DOLLAR);
return Node.fromList(list);
} else {
// TP 22
throw invalidExpressionSyntax(itr.getStr(), idx);
}
} else {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 23
// just treat $: like plain text
list.add(LiteralNode.DOLLAR);
list.add(LiteralNode.COLON);
start = itr.getNextIdx();
continue;
} else {
// TP 24
throw invalidExpressionSyntax(itr.getStr(), idx);
}
}
//throw Assert.unreachableCode();
}
default: {
// $ followed by anything else
if (flags.contains(Flag.MINI_EXPRS)) {
// TP 25
list.add(new ExpressionNode(false, new LiteralNode(itr.getStr(), idx, itr.getNextIdx()), Node.NULL));
start = itr.getNextIdx();
continue;
} else if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 26
// just treat it as literal
start = itr.getPrevIdx() - 1; // we can use 1 here because unicode '$' is one char in size
continue;
} else {
// TP 27
throw invalidExpressionSyntax(itr.getStr(), idx);
}
//throw Assert.unreachableCode();
}
}
//throw Assert.unreachableCode();
}
case ':': {
if (endOnColon) {
// TP 28
itr.prev(); // back up to point at : again
if (idx > start) {
list.add(new LiteralNode(itr.getStr(), start, idx));
}
return Node.fromList(list);
} else {
// TP 29
// plain content always
continue;
}
//throw Assert.unreachableCode();
}
case '{': {
if (! flags.contains(Flag.NO_SMART_BRACES)) {
// TP 1.2
ignoreBraceLevel++;
}
// TP 1.3
continue;
}
case '}': {
if (! flags.contains(Flag.NO_SMART_BRACES) && ignoreBraceLevel > 0) {
// TP 1.1
ignoreBraceLevel--;
continue;
} else if (endOnBrace) {
// TP 30
itr.prev(); // back up to point at } again
// TP 46 // allow an empty default value
if (idx >= start) {
list.add(new LiteralNode(itr.getStr(), start, idx));
}
return Node.fromList(list);
} else {
// TP 31
// treat as plain content
continue;
}
//throw Assert.unreachableCode();
}
case '\\': {
if (flags.contains(Flag.ESCAPES)) {
if (idx > start) {
list.add(new LiteralNode(itr.getStr(), start, idx));
start = idx;
}
if (! itr.hasNext()) {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// just treat it like plain content
// TP 33
continue;
} else {
// TP 34
throw invalidExpressionSyntax(itr.getStr(), idx);
}
} else {
ch = itr.next();
final LiteralNode node;
switch (ch) {
case 'n': {
// TP 35
node = LiteralNode.NEWLINE;
break;
}
case 'r': {
// TP 36
node = LiteralNode.CARRIAGE_RETURN;
break;
}
case 't': {
// TP 37
node = LiteralNode.TAB;
break;
}
case 'b': {
// TP 38
node = LiteralNode.BACKSPACE;
break;
}
case 'f': {
// TP 39
node = LiteralNode.FORM_FEED;
break;
}
case '\\': {
// TP 45
node = LiteralNode.BACKSLASH;
break;
}
default: {
if (flags.contains(Flag.LENIENT_SYNTAX)) {
// TP 40
// just append the literal character after the \, whatever it was
start = itr.getPrevIdx();
continue;
}
// TP 41
throw invalidExpressionSyntax(itr.getStr(), idx);
}
}
list.add(node);
start = itr.getNextIdx();
continue;
}
}
// TP 42
// otherwise, just...
continue;
}
default: {
// TP 43
// treat as plain content
//noinspection UnnecessaryContinue
continue;
}
}
//throw Assert.unreachableCode();
}
final int length = itr.getStr().length();
if (length > start) {
// TP 44
list.add(new LiteralNode(itr.getStr(), start, length));
}
return Node.fromList(list);
}
private static IllegalArgumentException invalidExpressionSyntax(final String string, final int index) {
String msg = CommonMessages.msg.invalidExpressionSyntax(index);
StringBuilder b = new StringBuilder(msg.length() + string.length() + string.length() + 5);
b.append(msg);
b.append('\n').append('\t').append(string);
b.append('\n').append('\t');
for (int i = 0; i < index; i = string.offsetByCodePoints(i, 1)) {
final int cp = string.codePointAt(i);
if (Character.isWhitespace(cp)) {
b.append(cp);
} else if (Character.isValidCodePoint(cp) && ! Character.isISOControl(cp)) {
b.append(' ');
}
}
b.append('^');
return new IllegalArgumentException(b.toString());
}
private static final EnumSet NO_FLAGS = EnumSet.noneOf(Flag.class);
/**
* Flags that can apply to a property expression compilation
*/
public enum Flag {
/**
* Do not trim leading and trailing whitespace off of the expression string before parsing it.
*/
NO_TRIM,
/**
* Ignore syntax problems instead of throwing an exception.
*/
LENIENT_SYNTAX,
/**
* Support single-character expressions that can be interpreted without wrapping in curly braces.
*/
MINI_EXPRS,
/**
* Do not support recursive expression expansion in the key part of the expression.
*/
NO_RECURSE_KEY,
/**
* Do not support recursion in default values.
*/
NO_RECURSE_DEFAULT,
/**
* Do not support smart braces.
*/
NO_SMART_BRACES,
/**
* Support {@code Policy} file style "general" expansion alternate expression syntax. "Smart" braces
* will only work if the opening brace is not the first character in the expression key.
*/
GENERAL_EXPANSION,
/**
* Support standard escape sequences in plain text and default value fields, which begin with a backslash ("{@code \}") character.
*/
ESCAPES,
/**
* Treat expressions containing a double-colon delimiter as special, encoding the entire content into the key.
*/
DOUBLE_COLON,
}
}