http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/NumberLiteralEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/NumberLiteralEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/NumberLiteralEvaluator.java
new file mode 100644
index 0000000..d7569e0
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/NumberLiteralEvaluator.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.literals;
+
+import java.util.Map;
+
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.NumberEvaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.NumberQueryResult;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+
+public class NumberLiteralEvaluator extends NumberEvaluator {
+
+    private final long literal;
+
+    public NumberLiteralEvaluator(final String value) {
+        this.literal = Long.parseLong(value);
+    }
+
+    @Override
+    public QueryResult<Long> evaluate(final Map<String, String> attributes) {
+        return new NumberQueryResult(literal);
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java
new file mode 100644
index 0000000..d739ac7
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.literals;
+
+import java.util.Map;
+
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+public class StringLiteralEvaluator extends StringEvaluator {
+
+    private final String value;
+
+    public StringLiteralEvaluator(final String value) {
+        // need to escape characters after backslashes
+        final StringBuilder sb = new StringBuilder();
+        boolean lastCharIsBackslash = false;
+        for (int i = 0; i < value.length(); i++) {
+            final char c = value.charAt(i);
+
+            if (lastCharIsBackslash) {
+                switch (c) {
+                    case 'n':
+                        sb.append("\n");
+                        break;
+                    case 'r':
+                        sb.append("\r");
+                        break;
+                    case '\\':
+                        sb.append("\\");
+                        break;
+                    case 't':
+                        sb.append("\\t");
+                        break;
+                    default:
+                        sb.append("\\").append(c);
+                        break;
+                }
+
+                lastCharIsBackslash = false;
+            } else if (c == '\\') {
+                lastCharIsBackslash = true;
+            } else {
+                sb.append(c);
+            }
+        }
+
+        this.value = sb.toString();
+    }
+
+    @Override
+    public QueryResult<String> evaluate(final Map<String, String> attributes) {
+        return new StringQueryResult(value);
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AllAttributesEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AllAttributesEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AllAttributesEvaluator.java
new file mode 100644
index 0000000..d9dd4d3
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AllAttributesEvaluator.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import java.util.Map;
+
+import 
org.apache.nifi.attribute.expression.language.evaluation.BooleanEvaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.BooleanQueryResult;
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+
+public class AllAttributesEvaluator extends BooleanEvaluator {
+
+    private final BooleanEvaluator booleanEvaluator;
+    private final MultiAttributeEvaluator multiAttributeEvaluator;
+
+    public AllAttributesEvaluator(final BooleanEvaluator booleanEvaluator, 
final MultiAttributeEvaluator multiAttributeEvaluator) {
+        this.booleanEvaluator = booleanEvaluator;
+        this.multiAttributeEvaluator = multiAttributeEvaluator;
+    }
+
+    @Override
+    public QueryResult<Boolean> evaluate(final Map<String, String> attributes) 
{
+        QueryResult<Boolean> attributeValueQuery = 
booleanEvaluator.evaluate(attributes);
+        Boolean result = attributeValueQuery.getValue();
+        if (result == null) {
+            return new BooleanQueryResult(false);
+        }
+
+        if (!result) {
+            return new BooleanQueryResult(false);
+        }
+
+        while (multiAttributeEvaluator.getEvaluationsRemaining() > 0) {
+            attributeValueQuery = booleanEvaluator.evaluate(attributes);
+            result = attributeValueQuery.getValue();
+            if (result != null && !result) {
+                return attributeValueQuery;
+            }
+        }
+
+        return new BooleanQueryResult(true);
+    }
+
+    @Override
+    public int getEvaluationsRemaining() {
+        return 0;
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyAttributeEvaluator.java
new file mode 100644
index 0000000..9192958
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyAttributeEvaluator.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import java.util.Map;
+
+import 
org.apache.nifi.attribute.expression.language.evaluation.BooleanEvaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.BooleanQueryResult;
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+
+public class AnyAttributeEvaluator extends BooleanEvaluator {
+
+    private final BooleanEvaluator booleanEvaluator;
+    private final MultiAttributeEvaluator multiAttributeEvaluator;
+
+    public AnyAttributeEvaluator(final BooleanEvaluator booleanEvaluator, 
final MultiAttributeEvaluator multiAttributeEvaluator) {
+        this.booleanEvaluator = booleanEvaluator;
+        this.multiAttributeEvaluator = multiAttributeEvaluator;
+    }
+
+    @Override
+    public QueryResult<Boolean> evaluate(final Map<String, String> attributes) 
{
+        QueryResult<Boolean> attributeValueQuery = 
booleanEvaluator.evaluate(attributes);
+        Boolean result = attributeValueQuery.getValue();
+        if (result == null) {
+            return new BooleanQueryResult(false);
+        }
+
+        if (result) {
+            return new BooleanQueryResult(true);
+        }
+
+        while (multiAttributeEvaluator.getEvaluationsRemaining() > 0) {
+            attributeValueQuery = booleanEvaluator.evaluate(attributes);
+            result = attributeValueQuery.getValue();
+            if (result != null && result) {
+                return attributeValueQuery;
+            }
+        }
+
+        return new BooleanQueryResult(false);
+    }
+
+    @Override
+    public int getEvaluationsRemaining() {
+        return 0;
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyMatchingAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyMatchingAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyMatchingAttributeEvaluator.java
new file mode 100644
index 0000000..8c07278
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/AnyMatchingAttributeEvaluator.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+public class AnyMatchingAttributeEvaluator {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/DelineatedAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/DelineatedAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/DelineatedAttributeEvaluator.java
new file mode 100644
index 0000000..209c86f
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/DelineatedAttributeEvaluator.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import java.util.Map;
+
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+public class DelineatedAttributeEvaluator extends MultiAttributeEvaluator {
+
+    private final StringEvaluator subjectEvaluator;
+    private final StringEvaluator delimiterEvaluator;
+    private final int evaluationType;
+    private String[] delineatedValues;
+    private int evaluationCount = 0;
+    private int evaluationsLeft = 1;
+
+    public DelineatedAttributeEvaluator(final StringEvaluator 
subjectEvaluator, final StringEvaluator delimiterEvaluator, final int 
evaluationType) {
+        this.subjectEvaluator = subjectEvaluator;
+        this.delimiterEvaluator = delimiterEvaluator;
+        this.evaluationType = evaluationType;
+    }
+
+    @Override
+    public QueryResult<String> evaluate(final Map<String, String> attributes) {
+        if (delineatedValues == null) {
+            final QueryResult<String> subjectValue = 
subjectEvaluator.evaluate(attributes);
+            if (subjectValue.getValue() == null) {
+                evaluationsLeft = 0;
+                return new StringQueryResult(null);
+            }
+
+            final QueryResult<String> delimiterValue = 
delimiterEvaluator.evaluate(attributes);
+            if (subjectValue.getValue() == null) {
+                evaluationsLeft = 0;
+                return new StringQueryResult(null);
+            }
+
+            delineatedValues = 
subjectValue.getValue().split(delimiterValue.getValue());
+        }
+
+        if (evaluationCount > delineatedValues.length) {
+            evaluationsLeft = 0;
+            return new StringQueryResult(null);
+        }
+
+        evaluationsLeft = delineatedValues.length - evaluationCount - 1;
+
+        return new StringQueryResult(delineatedValues[evaluationCount++]);
+    }
+
+    @Override
+    public int getEvaluationsRemaining() {
+        return evaluationsLeft;
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+
+    @Override
+    public int getEvaluationType() {
+        return evaluationType;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiAttributeEvaluator.java
new file mode 100644
index 0000000..f80ed97
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiAttributeEvaluator.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
+
+public abstract class MultiAttributeEvaluator extends StringEvaluator {
+
+    public abstract int getEvaluationType();
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiMatchAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiMatchAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiMatchAttributeEvaluator.java
new file mode 100644
index 0000000..9a441ce
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiMatchAttributeEvaluator.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+public class MultiMatchAttributeEvaluator extends MultiAttributeEvaluator {
+
+    private final List<Pattern> attributePatterns;
+    private final int evaluationType;
+    private final List<String> attributeNames = new ArrayList<>();
+    private int evaluationCount = 0;
+
+    public MultiMatchAttributeEvaluator(final List<String> attributeRegexes, 
final int evaluationType) {
+        this.attributePatterns = new ArrayList<>();
+        for (final String regex : attributeRegexes) {
+            attributePatterns.add(Pattern.compile(regex));
+        }
+
+        this.evaluationType = evaluationType;
+    }
+
+    /**
+     * Can be called only after the first call to evaluate
+     *
+     * @return
+     */
+    @Override
+    public int getEvaluationsRemaining() {
+        return attributeNames.size() - evaluationCount;
+    }
+
+    @Override
+    public QueryResult<String> evaluate(final Map<String, String> attributes) {
+        if (evaluationCount == 0) {
+            for (final Pattern pattern : attributePatterns) {
+                for (final String attrName : attributes.keySet()) {
+                    if (pattern.matcher(attrName).matches()) {
+                        attributeNames.add(attrName);
+                    }
+                }
+            }
+        }
+
+        if (evaluationCount >= attributeNames.size()) {
+            return new StringQueryResult(null);
+        }
+
+        return new 
StringQueryResult(attributes.get(attributeNames.get(evaluationCount++)));
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+
+    @Override
+    public int getEvaluationType() {
+        return evaluationType;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiNamedAttributeEvaluator.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiNamedAttributeEvaluator.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiNamedAttributeEvaluator.java
new file mode 100644
index 0000000..6dabc0a
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/selection/MultiNamedAttributeEvaluator.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.selection;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import 
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+public class MultiNamedAttributeEvaluator extends MultiAttributeEvaluator {
+
+    private final List<String> attributeNames;
+    private final int evaluationType;
+    private int evaluationCount = 0;
+    private List<String> matchingAttributeNames = null;
+
+    public MultiNamedAttributeEvaluator(final List<String> attributeNames, 
final int evaluationType) {
+        this.attributeNames = attributeNames;
+        this.evaluationType = evaluationType;
+    }
+
+    @Override
+    public QueryResult<String> evaluate(final Map<String, String> attributes) {
+        matchingAttributeNames = new ArrayList<>(attributeNames);
+
+        if (matchingAttributeNames.size() <= evaluationCount) {
+            return new StringQueryResult(null);
+        }
+
+        return new 
StringQueryResult(attributes.get(matchingAttributeNames.get(evaluationCount++)));
+    }
+
+    @Override
+    public int getEvaluationsRemaining() {
+        return matchingAttributeNames.size() - evaluationCount;
+    }
+
+    @Override
+    public Evaluator<?> getSubjectEvaluator() {
+        return null;
+    }
+
+    @Override
+    public int getEvaluationType() {
+        return evaluationType;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageException.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageException.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageException.java
new file mode 100644
index 0000000..47d42cb
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.exception;
+
+public class AttributeExpressionLanguageException extends RuntimeException {
+
+    private static final long serialVersionUID = -5637284498692447901L;
+
+    public AttributeExpressionLanguageException(final String explanation) {
+        super(explanation);
+    }
+
+    public AttributeExpressionLanguageException(final String explanation, 
final Throwable t) {
+        super(explanation, t);
+    }
+
+    public AttributeExpressionLanguageException(final Throwable t) {
+        super(t);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageParsingException.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageParsingException.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageParsingException.java
new file mode 100644
index 0000000..f8531cb
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/AttributeExpressionLanguageParsingException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.exception;
+
+public class AttributeExpressionLanguageParsingException extends 
AttributeExpressionLanguageException {
+
+    private static final long serialVersionUID = 7422163230677064726L;
+
+    public AttributeExpressionLanguageParsingException(final String 
explanation) {
+        super(explanation);
+    }
+
+    public AttributeExpressionLanguageParsingException(final String 
explanation, final Throwable t) {
+        super(explanation, t);
+    }
+
+    public AttributeExpressionLanguageParsingException(final Throwable t) {
+        super(t);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/IllegalAttributeException.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/IllegalAttributeException.java
 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/IllegalAttributeException.java
new file mode 100644
index 0000000..4a9a9c5
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/exception/IllegalAttributeException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language.exception;
+
+public class IllegalAttributeException extends RuntimeException {
+
+    public IllegalAttributeException() {
+        super();
+    }
+
+    public IllegalAttributeException(final String explanation) {
+        super(explanation);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
 
b/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
new file mode 100644
index 0000000..a2b7214
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
@@ -0,0 +1,1068 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.nifi.attribute.expression.language.Query.Range;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import 
org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageParsingException;
+import org.apache.nifi.expression.AttributeExpression.ResultType;
+import org.apache.nifi.flowfile.FlowFile;
+
+import org.antlr.runtime.tree.Tree;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class TestQuery {
+
+    @Test
+    public void testCompilation() {
+        assertInvalid("${attr:uuid()}");
+        assertInvalid("${attr:indexOf(length())}");
+        assertValid("${UUID()}");
+        assertInvalid("${UUID():nextInt()}");
+        assertValid("${nextInt()}");
+        assertValid("${now():format('yyyy/MM/dd')}");
+        assertInvalid("${attr:times(3)}");
+        assertValid("${attr:toNumber():multiply(3)}");
+        // left here because it's convenient for looking at the output
+        //System.out.println(Query.compile("").evaluate(null));
+    }
+    
+    private void assertValid(final String query) {
+        try {
+            Query.compile(query);
+        } catch (final Exception e) {
+            e.printStackTrace();
+            Assert.fail("Expected query to be valid, but it failed to compile 
due to " + e);
+        }
+    }
+    
+    private void assertInvalid(final String query) {
+        try {
+            Query.compile(query);
+            Assert.fail("Expected query to be invalid, but it did compile");
+        } catch (final Exception e) {
+        }
+    }
+    
+    @Test
+    public void testIsValidExpression() {
+        Query.validateExpression("${abc:substring(${xyz:length()})}", false);
+        Query.isValidExpression("${now():format('yyyy-MM-dd')}");
+        
+        
+        try {
+            Query.validateExpression("$${attr}", false);
+            Assert.fail("invalid query validated");
+        } catch (final AttributeExpressionLanguageParsingException e) {
+        }
+        
+        Query.validateExpression("$${attr}", true);
+        
+        Query.validateExpression("${filename:startsWith('T8MTXBC')\n" 
+            + ":or( ${filename:startsWith('C4QXABC')} )\n"
+            + ":or( ${filename:startsWith('U6CXEBC')} )"
+            + ":or( ${filename:startsWith('KYM3ABC')} )}", false);
+    }
+
+    
+    @Test
+    public void testCompileEmbedded() {
+        final String expression = "${x:equals( ${y} )}";
+        final Query query = Query.compile(expression);
+        final Tree tree = query.getTree();
+        System.out.println( printTree(tree) );
+        
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("x", "x");
+        attributes.put("y", "x");
+        final String result = Query.evaluateExpressions(expression, 
attributes, null);
+        assertEquals("true", result);
+        
+        Query.validateExpression(expression, false);
+    }
+    
+    private String printTree(final Tree tree) {
+        final StringBuilder sb = new StringBuilder();
+        printTree(tree, 0, sb);
+        
+        return sb.toString();
+    }
+    
+    private void printTree(final Tree tree, final int spaces, final 
StringBuilder sb) {
+        for (int i=0; i < spaces; i++) {
+            sb.append(" ");
+        }
+        
+        if ( tree.getText().trim().isEmpty() ) {
+            sb.append(tree.toString()).append("\n");
+        } else {
+            sb.append(tree.getText()).append("\n");
+        }
+        
+        for (int i=0; i < tree.getChildCount(); i++) {
+            printTree(tree.getChild(i), spaces + 2, sb);
+        }
+    }
+
+    @Test
+    public void testEscape() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "My Value");
+        attributes.put("${xx}", "hello");
+        
+        assertEquals("My Value", evaluateQueryForEscape("${attr}", 
attributes));
+        assertEquals("${attr}", evaluateQueryForEscape("$${attr}", 
attributes));
+        assertEquals("$My Value", evaluateQueryForEscape("$$${attr}", 
attributes));
+        assertEquals("$${attr}", evaluateQueryForEscape("$$$${attr}", 
attributes));
+        assertEquals("$$My Value", evaluateQueryForEscape("$$$$${attr}", 
attributes));
+    }
+
+    @Test
+    public void testWithBackSlashes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("x", "C:\\test\\1.txt");
+        attributes.put("y", "y\ny");
+        
+        final String query = "${x:substringAfterLast( '/' 
):substringAfterLast( '\\\\' )}";
+        verifyEquals(query, attributes, "1.txt");
+        attributes.put("x", "C:/test/1.txt");
+        verifyEquals(query, attributes, "1.txt");
+        
+        verifyEquals("${y:equals('y\\ny')}", attributes, Boolean.TRUE);
+    }
+    
+    @Test
+    public void testWithTicksOutside() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "My Value");
+
+        assertEquals(1, Query.extractExpressionRanges("\"${attr}").size());
+        assertEquals(1, Query.extractExpressionRanges("'${attr}").size());
+        assertEquals(1, Query.extractExpressionRanges("'${attr}'").size());
+        assertEquals(1, Query.extractExpressionRanges("${attr}").size());
+
+        assertEquals("'My Value'", Query.evaluateExpressions("'${attr}'", 
attributes, null));
+        assertEquals("'My Value", Query.evaluateExpressions("'${attr}", 
attributes, null));
+    }
+
+    
+    @Test
+    @Ignore("Depends on TimeZone")
+    public void testDateToNumber() {
+        final Query query = Query.compile("${dateTime:toDate('yyyy/MM/dd 
HH:mm:ss.SSS'):toNumber()}");
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("dateTime", "2013/11/18 10:22:27.678");
+        
+        final QueryResult<?> result = query.evaluate(attributes);
+        assertEquals(ResultType.NUMBER, result.getResultType());
+        assertEquals(1384788147678L, result.getValue());
+    }
+
+    @Test
+    public void testAddOneDayToDate() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("dateTime", "2013/11/18 10:22:27.678");
+
+        verifyEquals("${dateTime:toDate('yyyy/MM/dd 
HH:mm:ss.SSS'):toNumber():plus(86400000):toDate():format('yyyy/MM/dd 
HH:mm:ss.SSS')}", attributes, "2013/11/19 10:22:27.678");
+    }
+
+    @Test
+    public void implicitDateConversion() {
+        final Date date = new Date();
+        final Query query = Query.compile("${dateTime:format('yyyy/MM/dd 
HH:mm:ss.SSS')}");
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("dateTime", date.toString());
+        
+        // the date.toString() above will end up truncating the milliseconds. 
So remove millis from the Date before
+        // formatting it
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd 
HH:mm:ss.SSS");
+        final long millis = date.getTime() % 1000L;
+        final Date roundedToNearestSecond = new Date(date.getTime() - millis);
+        final String formatted = sdf.format(roundedToNearestSecond);
+        
+        final QueryResult<?> result = query.evaluate(attributes);
+        assertEquals(ResultType.STRING, result.getResultType());
+        assertEquals(formatted, result.getValue());
+    }
+
+    
+    @Test
+    public void testEmbeddedExpressionsAndQuotes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("x", "abc");
+        attributes.put("a", "abc");
+        
+        verifyEquals("${x:equals(${a})}", attributes, true);
+        
+        Query.validateExpression("${x:equals('${a}')}", false);
+        assertEquals("true", Query.evaluateExpressions("${x:equals('${a}')}", 
attributes, null));
+        
+        Query.validateExpression("${x:equals(\"${a}\")}", false);
+        assertEquals("true", 
Query.evaluateExpressions("${x:equals(\"${a}\")}", attributes, null));
+    }
+    
+    
+    @Test
+    public void testCurlyBracesInQuotes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "My Valuee");
+        
+        assertEquals("Val", evaluateQueryForEscape("${attr:replaceAll('My 
(Val)ue{1,2}', '$1')}", attributes));
+        assertEquals("Val", evaluateQueryForEscape("${attr:replaceAll(\"My 
(Val)ue{1,2}\", '$1')}", attributes));
+    }
+    
+    
+    private String evaluateQueryForEscape(final String queryString, final 
Map<String, String> attributes) {
+        FlowFile mockFlowFile = Mockito.mock(FlowFile.class);
+        Mockito.when(mockFlowFile.getAttributes()).thenReturn(attributes);
+        Mockito.when(mockFlowFile.getId()).thenReturn(1L);
+        
Mockito.when(mockFlowFile.getEntryDate()).thenReturn(System.currentTimeMillis());
+        Mockito.when(mockFlowFile.getSize()).thenReturn(1L);
+        Mockito.when(mockFlowFile.getLineageIdentifiers()).thenReturn(new 
HashSet<String>());
+        
Mockito.when(mockFlowFile.getLineageStartDate()).thenReturn(System.currentTimeMillis());
+        return Query.evaluateExpressions(queryString, mockFlowFile);
+    }
+    
+    
+    @Test
+    public void testGetAttributeValue() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "My Value");
+        verifyEquals("${attr}", attributes, "My Value");
+    }
+    
+    @Test
+    public void testGetAttributeValueEmbedded() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "XX ");
+        attributes.put("XX", "My Value");
+        verifyEquals("${${attr:trim()}}", attributes, "My Value");
+    }
+    
+    @Test
+    public void testSimpleSubstring() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "My Value");
+        verifyEquals("${attr:substring(2, 5)}", attributes, " Va");
+    }
+    
+    @Test
+    public void testCallToFunctionWithSubjectResultOfAnotherFunctionCall() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "   My Value   ");
+        verifyEquals("${attr:trim():substring(2, 5)}", attributes, " Va");
+    }
+
+    @Test
+    public void testProblematic1() {
+        // There was a bug that prevented this expression from compiling. This 
test just verifies that it now compiles.
+        final String queryString = "${xx:append( \"120101\" ):toDate( 
'yyMMddHHmmss' ):format( \"yy-MM-dd’T’HH:mm:ss\") }";
+        Query.compile(queryString);
+    }
+
+    @Test
+    public void testEquals() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", " XX    ");
+        verifyEquals("${attr:trim():equals('XX')}", attributes, true);
+    }
+    
+    @Test
+    public void testDeeplyEmbedded() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("x", "false");
+        attributes.put("abc", "a");
+        attributes.put("a", "a");
+        
+        verifyEquals("${x:or( ${${abc}:length():equals(1)} )}", attributes, 
true);
+    }
+    
+    
+    @Test
+    public void testExtractExpressionRanges() {
+        List<Range> ranges = Query.extractExpressionRanges("hello");
+        assertTrue(ranges.isEmpty());
+        
+        ranges = Query.extractExpressionRanges("${hello");
+        assertTrue(ranges.isEmpty());
+        
+        ranges = Query.extractExpressionRanges("hello}");
+        assertTrue(ranges.isEmpty());
+        
+        ranges = Query.extractExpressionRanges("$${hello");
+        assertTrue(ranges.isEmpty());
+
+        ranges = Query.extractExpressionRanges("$he{ll}o");
+        assertTrue(ranges.isEmpty());
+
+        ranges = Query.extractExpressionRanges("${hello}");
+        assertEquals(1, ranges.size());
+        Range range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(7, range.getEnd());
+        
+        ranges = Query.extractExpressionRanges("${hello:equals( ${goodbye} 
)}");
+        assertEquals(1, ranges.size());
+        range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(28, range.getEnd());
+        
+        ranges = Query.extractExpressionRanges("${hello:equals( $${goodbye} 
)}");
+        assertEquals(1, ranges.size());
+        range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(29, range.getEnd());
+        
+        ranges = Query.extractExpressionRanges("${hello:equals( $${goodbye} )} 
or just hi, ${bob:or(${jerry})}");
+        assertEquals(2, ranges.size());
+        range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(29, range.getEnd());
+        
+        range = ranges.get(1);
+        assertEquals(43, range.getStart());
+        assertEquals(61, range.getEnd());
+        
+        
+        ranges = Query.extractExpressionRanges("${hello:equals( ${goodbye} )} 
or just hi, ${bob}, are you ${bob.age:toNumber()} yet? $$$${bob}");
+        assertEquals(3, ranges.size());
+        range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(28, range.getEnd());
+        
+        range = ranges.get(1);
+        assertEquals(42, range.getStart());
+        assertEquals(47, range.getEnd());
+        
+        range = ranges.get(2);
+        assertEquals(58, range.getStart());
+        assertEquals(78, range.getEnd());
+        
+        ranges = Query.extractExpressionRanges("${x:matches( '.{4}' )}");
+        assertEquals(1, ranges.size());
+        range = ranges.get(0);
+        assertEquals(0, range.getStart());
+        assertEquals(21, range.getEnd());
+    }
+    
+    
+    @Test
+    public void testExtractExpressionTypes() {
+        List<ResultType> types = Query.extractResultTypes("${hello:equals( 
${goodbye} )} or just hi, ${bob}, are you ${bob.age:toNumber()} yet? 
$$$${bob}");
+        assertEquals(3, types.size());
+        assertEquals(ResultType.BOOLEAN, types.get(0));
+        assertEquals(ResultType.STRING, types.get(1));
+        assertEquals(ResultType.NUMBER, types.get(2));
+    }
+    
+    
+    @Test
+    public void testEqualsEmbedded() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("x", "hello");
+        attributes.put("y", "good-bye");
+        
+        verifyEquals("${x:equals( ${y} )}", attributes, false);
+        
+        attributes.put("y", "hello");
+        verifyEquals("${x:equals( ${y} )}", attributes, true);
+        
+        attributes.put("x", "4");
+        attributes.put("y", "3");
+        attributes.put("z", "1");
+        attributes.put("h", "100");
+        verifyEquals("${x:toNumber():lt( ${y:toNumber():plus( ${h:toNumber()} 
)} )}", attributes, true);
+        verifyEquals("${h:toNumber():ge( ${y:toNumber():plus( ${z:toNumber()} 
)} )}", attributes, true);
+        verifyEquals("${x:toNumber():equals( ${y:toNumber():plus( 
${z:toNumber()} )} )}", attributes, true);
+
+        attributes.put("x", "88");
+        verifyEquals("${x:toNumber():gt( ${y:toNumber():plus( ${z:toNumber()} 
)} )}", attributes, true);
+
+        attributes.put("y", "88");
+        assertEquals("true", Query.evaluateExpressions("${x:equals( '${y}' 
)}", attributes, null));
+    }
+    
+    
+    @Test
+    public void testComplicatedEmbeddedExpressions() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("fox", "quick, brown");
+        attributes.put("dog", "lazy");
+        
+        verifyEquals("${fox:substring( ${ 'dog' :substring(2):length()}, 5 
):equals( 'ick' )}", attributes, true);
+        verifyEquals("${fox:substring( ${ 'dog' :substring(2):length()}, 5 
):equals( 'ick' )}", attributes, true);
+    }
+    
+    @Test
+    public void testQuotingQuotes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("xx", "say 'hi'");
+
+        String query = "${xx:replaceAll( \"'.*'\", '\\\"hello\\\"' )}";
+        verifyEquals(query, attributes, "say \"hello\"");
+
+        query = "${xx:replace( \"'\", '\"')}";
+        verifyEquals(query, attributes, "say \"hi\"");
+
+        query = "${xx:replace( '\\'', '\"')}";
+        System.out.println(query);
+        verifyEquals(query, attributes, "say \"hi\"");
+    }
+    
+    @Test
+    public void testDoubleQuotesWithinSingleQuotes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("xx", "say 'hi'");
+
+        String query = "${xx:replace( \"'hi'\", '\\\"hello\\\"' )}";
+        System.out.println(query);
+        verifyEquals(query, attributes, "say \"hello\"");
+    }
+    
+    @Test
+    public void testEscapeQuotes() {
+        final long timestamp = 1403620278642L;
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("date", String.valueOf(timestamp));
+        
+        final String format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
+        
+        final String query = 
"startDateTime=\"${date:toNumber():toDate():format(\"" + format + "\")}\"";
+        final String result = Query.evaluateExpressions(query, attributes, 
null);
+        
+        final String expectedTime = new 
SimpleDateFormat(format).format(timestamp);
+        assertEquals("startDateTime=\"" + expectedTime + "\"", result);
+        
+        final List<Range> ranges = Query.extractExpressionRanges(query);
+        assertEquals(1, ranges.size());
+    }
+    
+    @Test
+    public void testDateConversion() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("date", "1403620278642");
+        
+        verifyEquals("${date:format('yyyy')}", attributes, "2014");
+        verifyEquals("${date:toDate():format('yyyy')}", attributes, "2014");
+        verifyEquals("${date:toNumber():format('yyyy')}", attributes, "2014");
+        verifyEquals("${date:toNumber():toDate():format('yyyy')}", attributes, 
"2014");
+        verifyEquals("${date:toDate():toNumber():format('yyyy')}", attributes, 
"2014");
+        
verifyEquals("${date:toDate():toNumber():toDate():toNumber():toDate():toNumber():format('yyyy')}",
 attributes, "2014");
+    }
+    
+    @Test
+    public void testSingleLetterAttribute() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("A", "0123456789");
+        
+        verifyEquals("${A}", attributes, "0123456789");
+        verifyEquals("${'A'}", attributes, "0123456789");
+    }
+
+    
+    @Test
+    public void testImplicitConversions() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("A", "0123456789");
+        attributes.put("b", "true");
+        attributes.put("c", "false");
+        attributes.put("d", "Quick Brown Fox");
+        attributes.put("F", "-48");
+        attributes.put("n", "2014/04/04 00:00:00");
+        
+        final Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.YEAR, 2014);
+        cal.set(Calendar.MONTH, 3);
+        cal.set(Calendar.DAY_OF_MONTH, 4);
+        cal.set(Calendar.HOUR, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 45);
+        
+        final String dateString = cal.getTime().toString();
+        attributes.put("z", dateString);
+
+        
+        verifyEquals("${A:plus(4)}", attributes, 123456793L);
+        verifyEquals("${A:plus( ${F} )}", attributes, 123456741L);
+
+        verifyEquals("${F:lt( ${A} )}", attributes, true);
+        verifyEquals("${A:substring(2,3):plus(21):substring(1,2):plus(0)}", 
attributes, 3L);
+        verifyEquals("${n:format( 'yyyy' )}", attributes, "2014");
+        verifyEquals("${z:format( 'yyyy' )}", attributes, "2014");
+        
+        attributes.put("n", "2014/04/04 00:00:00.045");
+        verifyEquals("${n:format( 'yyyy' ):append(','):append( ${n:format( 
'SSS' )} )}", attributes, "2014,045");
+    }
+    
+    @Test
+    public void testNewLinesAndTabsInQuery() {
+        final String query = "${ abc:equals('abc'):or( \n\t${xx:isNull()}\n) 
}";
+        assertEquals(ResultType.BOOLEAN, Query.getResultType(query));
+        Query.validateExpression(query, false);
+        assertEquals("true", Query.evaluateExpressions(query));
+    }
+    
+    @Test
+    public void testAttributeReferencesWithWhiteSpace() {
+        final Map<String, String> attrs = new HashMap<>();
+        attrs.put("a b c,d", "abc");
+        
+        final String query = "${ 'a b c,d':equals('abc') }";
+        verifyEquals(query, attrs, true);
+    }
+
+    @Test
+    public void testComments() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "xyz");
+
+        final String expression = 
+            "# hello, world\n" + 
+            "${# ref attr\n" +
+            "\t" +
+                "abc" +
+            "\t" +
+                "#end ref attr\n" +
+            "}";
+
+        Query query = Query.compile(expression);
+        QueryResult<?> result = query.evaluate(attributes);
+        assertEquals(ResultType.STRING, result.getResultType());
+        assertEquals("xyz", result.getValue());
+        
+        query = Query.compile("${abc:append('# hello') #good-bye \n}");
+        result = query.evaluate(attributes);
+        assertEquals(ResultType.STRING, result.getResultType());
+        assertEquals("xyz# hello", result.getValue());
+    }
+    
+    @Test
+    public void testAppendPrepend() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "XX");
+        attributes.put("YXXX", "bingo");
+        
+        verifyEquals("${${attr:append('X'):prepend('Y')}}", attributes, 
"bingo");
+    }
+    
+    @Test
+    public void testIsNull() {
+        final Map<String, String> attributes = new HashMap<>();
+        verifyEquals("${attr:isNull()}", attributes, true);
+    }
+    
+    @Test
+    public void testNotNull() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "");
+        
+        verifyEquals("${attr:notNull()}", attributes, true);
+    }
+    
+    @Test
+    public void testIsNullOrLengthEquals0() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "");
+        attributes.put("xyz", "xyz");
+        attributes.put("xx", "  ");
+        
+        verifyEquals("${abc:isNull():or( ${abc:length():equals(0)} )}", 
attributes, true);
+        verifyEquals("${xyz:isNull():or( ${xyz:length():equals(0)} )}", 
attributes, false);
+        verifyEquals("${none:isNull():or( ${none:length():equals(0)} )}", 
attributes, true);
+        verifyEquals("${xx:isNull():or( ${xx:trim():length():equals(0)} )}", 
attributes, true);
+    }
+    
+    @Test
+    public void testReplaceNull() {
+        final Map<String, String> attributes = new HashMap<>();
+        verifyEquals("${attr:replaceNull('hello')}", attributes, "hello");
+    }
+    
+    @Test
+    public void testReplace() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "hello");
+        verifyEquals("${attr:replace('hell', 'yell')}", attributes, "yello");
+    }
+
+    @Test
+    public void testReplaceAll() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "hello");
+        attributes.put("xyz", "00-00TEST.2014_01_01_000000_value");
+        
+        
verifyEquals("${xyz:replaceAll(\"^([^.]+)\\.([0-9]{4})_([0-9]{2})_([0-9]{2}).*$\",
 \"$3\")}", attributes, "01");
+        verifyEquals("${attr:replaceAll('l+', 'r')}", attributes, "hero");
+        
+        attributes.clear();
+        attributes.put("filename1", "abc.gz");
+        attributes.put("filename2", "abc.g");
+        attributes.put("filename3", "abc.gz.gz");
+        attributes.put("filename4", "abc.gz.g");
+        attributes.put("abc", "hello world");
+
+        verifyEquals("${filename3:replaceAll('\\\\\\.gz$', '')}", attributes, 
"abc.gz.gz");
+        verifyEquals("${filename3:replaceAll('\\\\\\\\.gz$', '')}", 
attributes, "abc.gz.gz");
+        verifyEquals("${filename1:replaceAll('\\.gz$', '')}", attributes, 
"abc");
+        verifyEquals("${filename2:replaceAll('\\.gz$', '')}", attributes, 
"abc.g");
+        verifyEquals("${filename4:replaceAll('\\\\.gz$', '')}", attributes, 
"abc.gz.g");
+
+        verifyEquals("${abc:replaceAll( 'lo wor(ld)', '$0')}", attributes, 
"hello world");
+        verifyEquals("${abc:replaceAll( 'he(llo) world', '$1')}", attributes, 
"llo");
+        verifyEquals("${abc:replaceAll( 'xx', '$0')}", attributes, "hello 
world");
+        verifyEquals("${abc:replaceAll( '(xx)', '$1')}", attributes, "hello 
world");
+        verifyEquals("${abc:replaceAll( 'lo wor(ld)', '$1')}", attributes, 
"helld");
+        
+    }
+    
+    
+    @Test
+    public void testReplaceAllWithOddNumberOfBackslashPairs() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename", "C:\\temp\\.txt");
+
+        verifyEquals("${filename:replace('\\\\', '/')}", attributes, 
"C:/temp/.txt");
+        verifyEquals("${filename:replaceAll('\\\\\\\\', '/')}", attributes, 
"C:/temp/.txt");
+        verifyEquals("${filename:replaceAll('\\\\\\.txt$', '')}", attributes, 
"C:\\temp");
+    }
+    
+    @Test
+    public void testReplaceAllWithMatchingGroup() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "hello");
+        
+        verifyEquals("${attr:replaceAll('.*?(l+).*', '$1')}", attributes, 
"ll");
+    }
+    
+    @Test
+    public void testMathOperations() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("one", "1");
+        attributes.put("two", "2");
+        attributes.put("three", "3");
+        attributes.put("four", "4");
+        attributes.put("five", "5");
+        attributes.put("hundred", "100");
+
+        
verifyEquals("${hundred:toNumber():multiply(2):divide(3):plus(1):mod(5)}", 
attributes, 2L);
+    }
+
+    @Test
+    public void testIndexOf() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("attr", "https://abc.go";);
+        verifyEquals("${attr:indexOf('/')}", attributes, 6L);
+    }
+    
+    @Test
+    public void testDate() {
+        final Calendar now = Calendar.getInstance();
+        final int year = now.get(Calendar.YEAR);
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("entryDate", String.valueOf(now.getTimeInMillis()));
+        
+        verifyEquals("${entryDate:toNumber():toDate():format('yyyy')}", 
attributes, String.valueOf(year));
+        
+        attributes.clear();
+        attributes.put("month", "3");
+        attributes.put("day", "4");
+        attributes.put("year", "2013");
+        assertEquals("63", 
Query.evaluateExpressions("${year:append('/'):append(${month}):append('/'):append(${day}):toDate('yyyy/MM/dd'):format('D')}",
 attributes, null));
+        assertEquals("63", 
Query.evaluateExpressions("${year:append('/'):append('${month}'):append('/'):append('${day}'):toDate('yyyy/MM/dd'):format('D')}",
 attributes, null));
+
+        
verifyEquals("${year:append('/'):append(${month}):append('/'):append(${day}):toDate('yyyy/MM/dd'):format('D')}",
 attributes, "63");
+    }
+    
+    @Test
+    public void testSystemProperty() {
+        System.setProperty("hello", "good-bye");
+        assertEquals("good-bye", Query.evaluateExpressions("${hello}"));
+        assertEquals("good-bye", 
Query.compile("${hello}").evaluate().getValue());
+    }
+    
+    @Test
+    public void testAnyAttribute() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "zzz");
+        attributes.put("xyz", "abc");
+        
+        verifyEquals("${anyAttribute('abc', 'xyz', 
'missingAttr'):substring(1,2):equals('b')}", attributes, true);
+        verifyEquals("${anyAttribute('abc', 
'xyz'):substring(1,2):equals('b')}", attributes, true);
+        verifyEquals("${anyAttribute('xyz', 
'abc'):substring(1,2):equals('b')}", attributes, true);
+        verifyEquals("${anyAttribute('zz'):substring(1,2):equals('b')}", 
attributes, false);
+        verifyEquals("${anyAttribute('abc', 'zz'):isNull()}", attributes, 
true);
+    }
+    
+    @Test
+    public void testAnyMatchingAttribute() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "zzz");
+        attributes.put("xyz", "abc");
+        attributes.put("123.cba", "hello");
+        
+        verifyEquals("${anyMatchingAttribute('.{2}x', 
'.{2}z'):substring(1,2):equals('b')}", attributes, true);
+        
verifyEquals("${anyMatchingAttribute('.*'):substring(1,2):equals('b')}", 
attributes, true);
+        
verifyEquals("${anyMatchingAttribute('x{44}'):substring(1,2):equals('b')}", 
attributes, false);
+        
verifyEquals("${anyMatchingAttribute('abc'):substring(1,2):equals('b')}", 
attributes, false);
+        
verifyEquals("${anyMatchingAttribute('xyz'):substring(1,2):equals('b')}", 
attributes, true);
+        verifyEquals("${anyMatchingAttribute('xyz'):notNull()}", attributes, 
true);
+        verifyEquals("${anyMatchingAttribute('xyz'):isNull()}", attributes, 
false);
+        verifyEquals("${anyMatchingAttribute('xxxxxxxxx'):notNull()}", 
attributes, false);
+        verifyEquals("${anyMatchingAttribute('123\\.c.*'):matches('hello')}", 
attributes, true);
+        
verifyEquals("${anyMatchingAttribute('123\\.c.*|a.c'):matches('zzz')}", 
attributes, true);
+    }
+    
+    
+    @Test
+    public void testAnyDelineatedValue() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "a,b,c");
+        attributes.put("xyz", "abc");
+        
+        final String query = "${anyDelineatedValue('${abc}', 
','):equals('b')}";
+        assertEquals(ResultType.BOOLEAN, Query.getResultType(query));
+        
+        assertEquals("true", Query.evaluateExpressions(query, attributes, 
null));
+        assertEquals("true", 
Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):equals('a')}", 
attributes, null));
+        assertEquals("true", 
Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):equals('c')}", 
attributes, null));
+        assertEquals("false", 
Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):equals('d')}", 
attributes, null));
+        
+        verifyEquals("${anyDelineatedValue(${abc}, ','):equals('b')}", 
attributes, true);
+        verifyEquals("${anyDelineatedValue(${abc}, ','):equals('a')}", 
attributes, true);
+        verifyEquals("${anyDelineatedValue(${abc}, ','):equals('c')}", 
attributes, true);
+        verifyEquals("${anyDelineatedValue(${abc}, ','):equals('d')}", 
attributes, false);
+    }
+    
+    @Test
+    public void testAllDelineatedValues() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "a,b,c");
+        attributes.put("xyz", "abc");
+        
+        final String query = "${allDelineatedValues('${abc}', 
','):matches('[abc]')}";
+        
+        assertEquals(ResultType.BOOLEAN, Query.getResultType(query));
+        assertEquals("true", Query.evaluateExpressions(query, attributes, 
null));
+        assertEquals("true", Query.evaluateExpressions(query, attributes, 
null));
+        assertEquals("false", 
Query.evaluateExpressions("${allDelineatedValues('${abc}', 
','):matches('[abd]')}", attributes, null));
+        assertEquals("false", 
Query.evaluateExpressions("${allDelineatedValues('${abc}', 
','):equals('a'):not()}", attributes, null));
+        
+        verifyEquals("${allDelineatedValues(${abc}, ','):matches('[abc]')}", 
attributes, true);
+        verifyEquals("${allDelineatedValues(${abc}, ','):matches('[abd]')}", 
attributes, false);
+        verifyEquals("${allDelineatedValues(${abc}, ','):equals('a'):not()}", 
attributes, false);
+    }
+    
+    
+    @Test
+    public void testAllAttributes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "1234");
+        attributes.put("xyz", "4132");
+        attributes.put("hello", "world!");
+
+        verifyEquals("${allAttributes('abc', 'xyz'):matches('\\d+')}", 
attributes, true);
+        verifyEquals("${allAttributes('abc', 'xyz'):toNumber():lt(99999)}", 
attributes, true);
+        verifyEquals("${allAttributes('abc', 'hello'):length():gt(3)}", 
attributes, true);
+        verifyEquals("${allAttributes('abc', 'hello'):length():equals(4)}", 
attributes, false);
+        verifyEquals("${allAttributes('abc', 'xyz'):length():equals(4)}", 
attributes, true);
+        verifyEquals("${allAttributes('abc', 'xyz', 'other'):isNull()}", 
attributes, false);
+        
+        try {
+            Query.compile("${allAttributes('#ah'):equals('hello')");
+            Assert.fail("Was able to compile with allAttributes and an invalid 
attribute name");
+        } catch (final AttributeExpressionLanguageParsingException e) {
+            // expected behavior
+        }
+    }
+    
+    
+    @Test
+    public void testMathOperators() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "1234");
+        attributes.put("xyz", "4132");
+        attributes.put("hello", "world!");
+
+        verifyEquals("${xyz:toNumber():gt( ${abc:toNumber()} )}", attributes, 
true);
+    }
+    
+    @Test
+    public void testAllMatchingAttributes() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "1234");
+        attributes.put("xyz", "4132");
+        attributes.put("hello", "world!");
+        attributes.put("123.cba", "hell.o");
+
+        System.out.println( 
printTree(Query.compile("${allMatchingAttributes('(abc|xyz)'):matches('\\\\d+')}").getTree())
 );
+        
+        verifyEquals("${'123.cba':matches('hell\\.o')}", attributes, true);
+        verifyEquals("${allMatchingAttributes('123\\.cba'):equals('hell.o')}", 
attributes, true);
+        verifyEquals("${allMatchingAttributes('(abc|xyz)'):matches('\\d+')}", 
attributes, true);
+        
verifyEquals("${allMatchingAttributes('[ax].*'):toNumber():lt(99999)}", 
attributes, true);
+        verifyEquals("${allMatchingAttributes('hell.'):length():gt(3)}", 
attributes, true);
+        
+        verifyEquals("${allMatchingAttributes('123\\.cba'):equals('no')}", 
attributes, false);
+    }
+    
+    @Test
+    public void testMatches() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "1234xyz4321");
+        attributes.put("end", "xyz");
+        attributes.put("xyz", "4132");
+        attributes.put("hello", "world!");
+        attributes.put("dotted", "abc.xyz");
+
+        final String evaluated = 
Query.evaluateExpressions("${abc:matches('1234${end}4321')}", attributes, null);
+        assertEquals("true", evaluated);
+        
+        attributes.put("end", "888");
+        final String secondEvaluation = 
Query.evaluateExpressions("${abc:matches('1234${end}4321')}", attributes, null);
+        assertEquals("false", secondEvaluation);
+        
+        verifyEquals("${dotted:matches('abc\\.xyz')}", attributes, true);
+   }
+    
+    
+    @Test
+    public void testFind() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "1234xyz4321");
+        attributes.put("end", "xyz");
+        attributes.put("xyz", "4132");
+        attributes.put("hello", "world!");
+        attributes.put("dotted", "abc.xyz");
+
+        final String evaluated = 
Query.evaluateExpressions("${abc:find('1234${end}4321')}", attributes, null);
+        assertEquals("true", evaluated);
+        
+        attributes.put("end", "888");
+        final String secondEvaluation = 
Query.evaluateExpressions("${abc:find('${end}4321')}", attributes, null);
+        assertEquals("false", secondEvaluation);
+        
+        verifyEquals("${dotted:find('\\.')}", attributes, true);
+   }
+    
+    @Test
+    public void testSubstringAfter() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename", "file-255");
+        
+        verifyEquals("${filename:substringAfter('')}", attributes, "file-255");
+        verifyEquals("${filename:substringAfterLast('')}", attributes, 
"file-255");
+        verifyEquals("${filename:substringBefore('')}", attributes, 
"file-255");
+        verifyEquals("${filename:substringBeforeLast('')}", attributes, 
"file-255");
+        verifyEquals("${filename:substringBefore('file')}", attributes, "");
+        
+        attributes.put("uri", "sftp://some.uri";);
+        verifyEquals("${uri:substringAfter('sftp')}", attributes, 
"://some.uri");
+    }
+    
+    @Test
+    public void testSubstringAfterLast() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename", "file-file-255");
+        
+        verifyEquals("${filename:substringAfterLast('file-')}", attributes, 
"255");
+        verifyEquals("${filename:substringAfterLast('5')}", attributes, "");
+        verifyEquals("${filename:substringAfterLast('x')}", attributes, 
"file-file-255");
+    }
+    
+    @Test
+    public void testSubstringBefore() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("something", "some {} or other");
+        
+        verifyEquals("${something:substringBefore('}')}", attributes, "some 
{");
+    }
+    
+    @Test
+    public void testSubstring() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename", "file-255");
+        
+        verifyEquals("${filename:substring(1, 2)}", attributes, "i");
+        verifyEquals("${filename:substring(4)}", attributes, "-255");
+    }
+    
+    @Test
+    public void testToRadix() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename", "file-255");
+        attributes.put("filename2", "file-99999");
+
+
+        
verifyEquals("${filename:substringAfter('-'):toNumber():toRadix(16):toUpper()}",
 attributes, "FF");
+        verifyEquals("${filename:substringAfter('-'):toNumber():toRadix(16, 
4):toUpper()}", attributes, "00FF");
+        verifyEquals("${filename:substringAfter('-'):toNumber():toRadix(36, 
3):toUpper()}", attributes, "073");
+    }
+    
+    @Test
+    public void testDateFormatConversion() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("blue", "20130917162643");
+        verifyEquals("${blue:toDate('yyyyMMddHHmmss'):format(\"yyyy/MM/dd 
HH:mm:ss.SSS'Z'\")}", attributes, "2013/09/17 16:26:43.000Z");
+    }
+
+    
+    @Test
+    public void testNot() {
+        verifyEquals("${ab:notNull():not()}", new HashMap<String, String>(), 
true);
+    }
+    
+    @Test
+    public void testAttributesWithSpaces() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("ab", "abc");
+        attributes.put("a  b", "abc");
+        
+        verifyEquals("${ab}", attributes, "abc");
+        verifyEquals("${'a  b'}", attributes, "abc");
+        verifyEquals("${'a b':replaceNull('')}", attributes, "");
+    }
+    
+    @Test
+    public void testOr() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename1", "xabc");
+        attributes.put("filename2", "yabc");
+        attributes.put("filename3", "abcxy");
+
+        verifyEquals("${filename1:startsWith('x'):or(true)}", attributes, 
true);
+        verifyEquals("${filename1:startsWith('x'):or( 
${filename1:startsWith('y')} )}", attributes, true);
+        verifyEquals("${filename2:startsWith('x'):or( 
${filename2:startsWith('y')} )}", attributes, true);
+        verifyEquals("${filename3:startsWith('x'):or( 
${filename3:startsWith('y')} )}", attributes, false);
+        verifyEquals("${filename1:startsWith('x'):or( 
${filename2:startsWith('y')} )}", attributes, true);
+        verifyEquals("${filename2:startsWith('x'):or( 
${filename1:startsWith('y')} )}", attributes, false);
+    }
+    
+    @Test
+    public void testAnd() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename1", "xabc");
+        attributes.put("filename2", "yabc");
+        attributes.put("filename 3", "abcxy");
+
+        verifyEquals("${filename1:startsWith('x'):and(true)}", attributes, 
true);
+        verifyEquals("${filename1:startsWith('x') : and( false )}", 
attributes, false);
+        verifyEquals("${filename1:startsWith('x'):and( 
${filename1:startsWith('y')} )}", attributes, false);
+        verifyEquals("${filename2:startsWith('x'):and( 
${filename2:startsWith('y')} )}", attributes, false);
+        verifyEquals("${filename3:startsWith('x'):and( 
${filename3:startsWith('y')} )}", attributes, false);
+        verifyEquals("${filename1:startsWith('x'):and( 
${filename2:startsWith('y')} )}", attributes, true);
+        verifyEquals("${filename2:startsWith('x'):and( 
${filename1:startsWith('y')} )}", attributes, false);
+        verifyEquals("${filename1:startsWith('x'):and( ${'filename 
3':endsWith('y')} )}", attributes, true);
+    }
+    
+    @Test
+    public void testAndOrNot() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename1", "xabc");
+        attributes.put("filename2", "yabc");
+        attributes.put("filename 3", "abcxy");
+
+        final String query = 
+            "${" +
+            "     'non-existing':notNull():not():and(" +                       
                 // true AND (
+            "     ${filename1:startsWith('y')" +                               
                     // false
+            "     :or(" +                                                      
                     // or
+            "       ${ filename1:startsWith('x'):and(false) }" +               
                     // false
+            "     ):or(" +                                                     
                     // or
+            "       ${ filename2:endsWith('xxxx'):or( ${'filename 
3':length():gt(1)} ) }" +         // true )
+            "     )}" +
+            "     )" +
+            "}";
+        
+        System.out.println(query);
+        verifyEquals(query, attributes, true);
+    }
+    
+    @Test
+    public void testAndOrLogicWithAnyAll() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("filename1", "xabc");
+        attributes.put("filename2", "yabc");
+        attributes.put("filename 3", "abcxy");
+        
+        
verifyEquals("${anyMatchingAttribute('filename.*'):contains('abc'):and( 
${filename2:equals('yabc')} )}", attributes, true);
+        
verifyEquals("${anyMatchingAttribute('filename.*'):contains('abc'):and( 
${filename2:equals('xabc')} )}", attributes, false);
+        
verifyEquals("${anyMatchingAttribute('filename.*'):contains('abc'):not():or( 
${filename2:equals('yabc')} )}", attributes, true);
+        
verifyEquals("${anyMatchingAttribute('filename.*'):contains('abc'):not():or( 
${filename2:equals('xabc')} )}", attributes, false);
+    }
+    
+    @Test
+    public void testKeywords() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("UUID", "123");
+        verifyEquals("${ 'UUID':toNumber():equals(123) }", attributes, true);
+    }
+    
+    @Test
+    public void testEqualsNumber() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("abc", "123");
+        verifyEquals("${ abc:toNumber():equals(123) }", attributes, true);
+    }
+    
+    @Test
+    public void testSubjectAsEmbeddedExpressionWithSurroundChars() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("b", "x");
+        attributes.put("abcxcba", "hello");
+        
+        final String evaluated = Query.evaluateExpressions("${ 
'abc${b}cba':substring(0, 1) }", attributes, null);
+        assertEquals("h", evaluated);
+    }
+    
+    @Test
+    public void testToNumberFunctionReturnsNumberType() {
+        assertEquals(ResultType.NUMBER, 
Query.getResultType("${header.size:toNumber()}"));
+    }
+    
+    
+    private void verifyEquals(final String expression, final Map<String, 
String> attributes, final Object expectedResult) {
+        Query.validateExpression(expression, false);
+        assertEquals(String.valueOf(expectedResult), 
Query.evaluateExpressions(expression, attributes, null));
+        
+        Query query = Query.compile(expression);
+        QueryResult<?> result = query.evaluate(attributes);
+        
+        if ( expectedResult instanceof Number ) {
+            assertEquals(ResultType.NUMBER, result.getResultType());
+        } else if ( expectedResult instanceof Boolean ) {
+            assertEquals(ResultType.BOOLEAN, result.getResultType());
+        } else {
+            assertEquals(ResultType.STRING, result.getResultType());
+        }
+        
+        assertEquals(expectedResult, result.getValue());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestStandardPreparedQuery.java
----------------------------------------------------------------------
diff --git 
a/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestStandardPreparedQuery.java
 
b/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestStandardPreparedQuery.java
new file mode 100644
index 0000000..398a23b
--- /dev/null
+++ 
b/commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestStandardPreparedQuery.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.attribute.expression.language;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class TestStandardPreparedQuery {
+
+    @Test
+    public void testSimpleReference() {
+        final Map<String, String> attrs = new HashMap<>();
+        attrs.put("xx", "world");
+        
+        assertEquals("world", evaluate("${xx}", attrs));
+        assertEquals("hello, world!", evaluate("hello, ${xx}!", attrs));
+    }
+    
+    @Test
+    public void testEmbeddedReference() {
+        final Map<String, String> attrs = new HashMap<>();
+        attrs.put("xx", "yy");
+        attrs.put("yy", "world");
+        
+        assertEquals("world", evaluate("${${xx}}", attrs));
+    }
+    
+    @Test
+    public void test10MIterations() {
+        final Map<String, String> attrs = new HashMap<>();
+        attrs.put("xx", "world");
+        
+        final StandardPreparedQuery prepared = (StandardPreparedQuery) 
Query.prepare("${xx}");
+        final long start = System.nanoTime();
+        for (int i=0; i < 10000000; i++) {
+            assertEquals( "world", prepared.evaluateExpressions(attrs, null) );
+        }
+        final long nanos = System.nanoTime() - start;
+        System.out.println(TimeUnit.NANOSECONDS.toMillis(nanos));
+    }
+    
+    @Test
+    @Ignore("Takes too long")
+    public void test10MIterationsWithQuery() {
+        final Map<String, String> attrs = new HashMap<>();
+        attrs.put("xx", "world");
+
+        final long start = System.nanoTime();
+        for (int i=0; i < 10000000; i++) {
+            assertEquals( "world", Query.evaluateExpressions("${xx}", attrs) );
+        }
+        final long nanos = System.nanoTime() - start;
+        System.out.println(TimeUnit.NANOSECONDS.toMillis(nanos));
+
+    }
+    
+    @Test
+    public void testSeveralSequentialExpressions() {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put("audience", "World");
+        attributes.put("comma", ",");
+        attributes.put("question", " how are you?");
+        assertEquals("Hello, World, how are you?!", evaluate("Hello, 
${audience}${comma}${question}!", attributes));
+
+    }
+    
+    private String evaluate(final String query, final Map<String, String> 
attrs) {
+        final String evaluated = ((StandardPreparedQuery) 
Query.prepare(query)).evaluateExpressions(attrs, null);
+        return evaluated;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/4d998c12/commons/nifi-file-utils/pom.xml
----------------------------------------------------------------------
diff --git a/commons/nifi-file-utils/pom.xml b/commons/nifi-file-utils/pom.xml
new file mode 100644
index 0000000..e3cf792
--- /dev/null
+++ b/commons/nifi-file-utils/pom.xml
@@ -0,0 +1,35 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <!--
+      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.
+    -->
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-file-utils</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>NiFi File Utils</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.10</version>
+        </dependency>
+    </dependencies>
+</project>

Reply via email to