You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2019/10/17 09:59:47 UTC

[archiva-components] 02/02: Adding expression evaluator

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva-components.git

commit a054f7b9f952158da70fadd94eda448e36c0dc13
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Thu Oct 17 11:58:20 2019 +0200

    Adding expression evaluator
---
 expression-evaluator/.gitignore                    |  10 +
 expression-evaluator/pom.xml                       |  53 +++++
 .../evaluator/DefaultExpressionEvaluator.java      | 159 +++++++++++++++
 .../components/evaluator/EvaluatorException.java   |  50 +++++
 .../components/evaluator/ExpressionEvaluator.java  |  63 ++++++
 .../components/evaluator/ExpressionSource.java     |  37 ++++
 .../sources/PropertiesExpressionSource.java        |  65 +++++++
 .../sources/SystemPropertyExpressionSource.java    |  44 +++++
 expression-evaluator/src/site/site.xml             |  34 ++++
 .../sources/DefaultExpressionEvaluatorTest.java    | 214 +++++++++++++++++++++
 10 files changed, 729 insertions(+)

diff --git a/expression-evaluator/.gitignore b/expression-evaluator/.gitignore
new file mode 100644
index 0000000..7b6772c
--- /dev/null
+++ b/expression-evaluator/.gitignore
@@ -0,0 +1,10 @@
+.idea/**
+.project
+.classpath
+.settings
+.java-version
+target
+.DS_Store
+.site-content
+out
+*.iml
diff --git a/expression-evaluator/pom.xml b/expression-evaluator/pom.xml
new file mode 100644
index 0000000..fd69415
--- /dev/null
+++ b/expression-evaluator/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<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/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.archiva.components</groupId>
+    <artifactId>archiva-components</artifactId>
+    <version>3.0-SNAPSHOT</version>
+  </parent>
+
+  <version>3.0-SNAPSHOT</version>
+  <artifactId>archiva-components-expression-evaluator</artifactId>
+  <name>Archiva Components :: Expression Evaluator</name>
+
+  <properties>
+    <site.staging.base>${project.basedir}/..</site.staging.base>
+  </properties>
+
+
+  <url>${webUrl}/${project.artifactId}</url>
+
+  <scm>
+    <url>${scmBrowseUrl}</url>
+  </scm>
+
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/DefaultExpressionEvaluator.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/DefaultExpressionEvaluator.java
new file mode 100644
index 0000000..00cfef5
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/DefaultExpressionEvaluator.java
@@ -0,0 +1,159 @@
+package org.apache.archiva.components.evaluator;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * DefaultExpressionEvaluator 
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public class DefaultExpressionEvaluator
+    implements ExpressionEvaluator
+{
+    private List<ExpressionSource> expressionSources;
+
+    public DefaultExpressionEvaluator()
+    {
+        expressionSources = new ArrayList<>();
+    }
+
+    public void addExpressionSource( ExpressionSource source )
+    {
+        expressionSources.add( source );
+    }
+
+    public String expand( String str )
+        throws EvaluatorException
+    {
+        return recursiveExpand( str, new ArrayList<String>() );
+    }
+
+    private String recursiveExpand( String str, List<String> seenExpressions )
+        throws EvaluatorException
+    {
+        if ( StringUtils.isEmpty( str ) )
+        {
+            // Empty string. Fail fast.
+            return str;
+        }
+
+        if ( str.indexOf( "${" ) < 0 )
+        {
+            // Contains no potential expressions.  Fail fast.
+            return str;
+        }
+
+        if ( this.expressionSources.isEmpty() )
+        {
+            throw new EvaluatorException( "Unable to expand expressions with empty ExpressionSource list." );
+        }
+
+        Pattern pat = Pattern.compile( "(?<=[^$]|^)(\\$\\{[^}]*\\})" );
+        Matcher mat = pat.matcher( str );
+        int offset = 0;
+        String expression;
+        String value;
+        StringBuilder expanded = new StringBuilder();
+
+        while ( mat.find( offset ) )
+        {
+            expression = mat.group( 1 );
+
+            if ( seenExpressions.contains( expression ) )
+            {
+                throw new EvaluatorException( "A recursive cycle has been detected with expression " + expression + "." );
+            }
+
+            seenExpressions.add( expression );
+
+            expanded.append( str.substring( offset, mat.start( 1 ) ) );
+            value = findValue( expression );
+            if ( value != null )
+            {
+                String resolvedValue = recursiveExpand( value, seenExpressions );
+                expanded.append( resolvedValue );
+            }
+            else
+            {
+                expanded.append( expression );
+            }
+            offset = mat.end( 1 );
+        }
+
+        expanded.append( str.substring( offset ) );
+
+        if ( expanded.indexOf( "$$" ) >= 0 )
+        {
+            // Special case for escaped content.
+            return expanded.toString().replaceAll( "\\$\\$", "\\$" );
+        }
+        else
+        {
+            // return expanded
+            return expanded.toString();
+        }
+    }
+
+    private String findValue( String expression )
+    {
+        String newExpression = expression.trim();
+        if ( newExpression.startsWith( "${" ) && newExpression.endsWith( "}" ) )
+        {
+            newExpression = newExpression.substring( 2, newExpression.length() - 1 );
+        }
+
+        if ( StringUtils.isEmpty( newExpression ) )
+        {
+            return null;
+        }
+
+        String value = null;
+        Iterator it = this.expressionSources.iterator();
+        while ( it.hasNext() )
+        {
+            ExpressionSource source = (ExpressionSource) it.next();
+            value = source.getExpressionValue( newExpression );
+            if ( value != null )
+            {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public List getExpressionSourceList()
+    {
+        return this.expressionSources;
+    }
+
+    public boolean removeExpressionSource( ExpressionSource source )
+    {
+        return this.expressionSources.remove( source );
+    }
+}
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/EvaluatorException.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/EvaluatorException.java
new file mode 100644
index 0000000..b2c68cc
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/EvaluatorException.java
@@ -0,0 +1,50 @@
+package org.apache.archiva.components.evaluator;
+
+/*
+ * 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.
+ */
+
+/**
+ * EvaluatorException 
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public class EvaluatorException
+    extends Exception
+{
+    public EvaluatorException()
+    {
+        super();
+    }
+
+    public EvaluatorException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+    public EvaluatorException( String message )
+    {
+        super( message );
+    }
+
+    public EvaluatorException( Throwable cause )
+    {
+        super( cause );
+    }
+}
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionEvaluator.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionEvaluator.java
new file mode 100644
index 0000000..0aeae57
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionEvaluator.java
@@ -0,0 +1,63 @@
+package org.apache.archiva.components.evaluator;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+/**
+ * ExpressionEvaluator
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public interface ExpressionEvaluator
+{
+    /**
+     * Add a source for expression resolution.
+     *
+     * @param source the source to add.
+     */
+    void addExpressionSource( ExpressionSource source );
+
+    /**
+     * Evaluate a string, and expand expressions as needed.
+     *
+     * @param str the expression
+     * @return the value of the expression
+     * @throws EvaluatorException if a problem occurs whilst evaluating
+     */
+    String expand( String str )
+        throws EvaluatorException;
+
+    /**
+     * Get the List of expression sources.
+     *
+     * @return the list of expression sources.
+     */
+    List getExpressionSourceList();
+
+    /**
+     * Remove a specific expression source.
+     *
+     * @param source the source to remove.
+     * @return true if expression source was removed.
+     */
+    boolean removeExpressionSource( ExpressionSource source );
+}
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionSource.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionSource.java
new file mode 100644
index 0000000..39d15cb
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/ExpressionSource.java
@@ -0,0 +1,37 @@
+package org.apache.archiva.components.evaluator;
+
+/*
+ * 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.
+ */
+
+/**
+ * ExpressionSource
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public interface ExpressionSource
+{
+    /**
+     * Gets a value for a provided Expression.
+     *
+     * @param expression the expression to attempt to get a value for.
+     * @return the value for the expression, or null if no value found.
+     */
+    String getExpressionValue( String expression );
+}
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/PropertiesExpressionSource.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/PropertiesExpressionSource.java
new file mode 100644
index 0000000..4f157cb
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/PropertiesExpressionSource.java
@@ -0,0 +1,65 @@
+package org.apache.archiva.components.evaluator.sources;
+
+/*
+ * 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.
+ */
+
+import org.apache.archiva.components.evaluator.ExpressionSource;
+
+import java.util.Properties;
+
+/**
+ * PropertiesExpressionSource 
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ * 
+ */
+public class PropertiesExpressionSource
+    implements ExpressionSource
+{
+    private Properties properties;
+
+    public String getExpressionValue( String expression )
+    {
+        if ( properties == null )
+        {
+            throw new IllegalStateException( "Properties object has not been initialized." );
+        }
+
+        try
+        {
+            return properties.getProperty( expression );
+        }
+        catch ( Exception e )
+        {
+            return null;
+        }
+    }
+
+    public Properties getProperties()
+    {
+        return properties;
+    }
+
+    public void setProperties( Properties properties )
+    {
+        this.properties = properties;
+    }
+
+}
diff --git a/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/SystemPropertyExpressionSource.java b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/SystemPropertyExpressionSource.java
new file mode 100644
index 0000000..522c973
--- /dev/null
+++ b/expression-evaluator/src/main/java/org/apache/archiva/components/evaluator/sources/SystemPropertyExpressionSource.java
@@ -0,0 +1,44 @@
+package org.apache.archiva.components.evaluator.sources;
+
+/*
+ * 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.
+ */
+
+import org.apache.archiva.components.evaluator.ExpressionSource;
+
+/**
+ * SystemPropertyExpressionSource 
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public class SystemPropertyExpressionSource
+    implements ExpressionSource
+{
+    public String getExpressionValue( String expression )
+    {
+        try
+        {
+            return System.getProperty( expression );
+        }
+        catch ( Exception e )
+        {
+            return null;
+        }
+    }
+}
diff --git a/expression-evaluator/src/site/site.xml b/expression-evaluator/src/site/site.xml
new file mode 100644
index 0000000..c3cfc21
--- /dev/null
+++ b/expression-evaluator/src/site/site.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  ~ 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.
+  -->
+
+<project name="Expression Evaluator" >
+
+  <publishDate format="yyyy-MM-dd" position="none" />
+
+  <body>
+    <menu ref="modules" />
+    <menu ref="reports" />
+    <menu ref="ASF" />
+    <breadcrumbs>
+      <item name="Archiva Components" href="../index.html" />
+      <item name="Expression Evaluator" href="index.html" />
+    </breadcrumbs>
+  </body>
+</project>
diff --git a/expression-evaluator/src/test/java/org/apache/archiva/components/evaluator/sources/DefaultExpressionEvaluatorTest.java b/expression-evaluator/src/test/java/org/apache/archiva/components/evaluator/sources/DefaultExpressionEvaluatorTest.java
new file mode 100644
index 0000000..423ea6f
--- /dev/null
+++ b/expression-evaluator/src/test/java/org/apache/archiva/components/evaluator/sources/DefaultExpressionEvaluatorTest.java
@@ -0,0 +1,214 @@
+package org.apache.archiva.components.evaluator.sources;
+
+/*
+ * 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.
+ */
+
+import junit.framework.TestCase;
+import org.apache.archiva.components.evaluator.DefaultExpressionEvaluator;
+import org.apache.archiva.components.evaluator.EvaluatorException;
+import org.apache.archiva.components.evaluator.ExpressionEvaluator;
+import org.apache.archiva.components.evaluator.sources.PropertiesExpressionSource;
+import org.apache.archiva.components.evaluator.sources.SystemPropertyExpressionSource;
+
+import java.util.Properties;
+
+/**
+ * DefaultExpressionEvaluatorTest
+ *
+ * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
+ *
+ */
+public class DefaultExpressionEvaluatorTest
+    extends TestCase
+{
+    private ExpressionEvaluator evaluator;
+
+    protected void setUp()
+        throws Exception
+    {
+        super.setUp();
+
+        evaluator = new DefaultExpressionEvaluator();
+    }
+
+    public void testSimple()
+        throws EvaluatorException
+    {
+        Properties props = new Properties();
+        props.setProperty( "fruit", "apple" );
+
+        PropertiesExpressionSource propsSource = new PropertiesExpressionSource();
+        propsSource.setProperties( props );
+        evaluator.addExpressionSource( propsSource );
+
+        String expression = "${fruit}";
+        String expected = "apple";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testSimpleStartOfLine()
+        throws EvaluatorException
+    {
+        Properties props = new Properties();
+        props.setProperty( "fruit", "apple" );
+
+        PropertiesExpressionSource propsSource = new PropertiesExpressionSource();
+        propsSource.setProperties( props );
+        evaluator.addExpressionSource( propsSource );
+
+        String expression = "${fruit} is good for you.";
+        String expected = "apple is good for you.";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testSimpleEndOfLine()
+        throws EvaluatorException
+    {
+        Properties props = new Properties();
+        props.setProperty( "fruit", "apple" );
+
+        PropertiesExpressionSource propsSource = new PropertiesExpressionSource();
+        propsSource.setProperties( props );
+        evaluator.addExpressionSource( propsSource );
+
+        String expression = "watch out for the worm in the ${fruit}";
+        String expected = "watch out for the worm in the apple";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testSimpleSystemProperty()
+        throws EvaluatorException
+    {
+        evaluator.addExpressionSource( new SystemPropertyExpressionSource() );
+
+        String userHome = System.getProperty( "user.home" );
+        String expression = "My HOME directory is ${user.home}";
+        String expected = "My HOME directory is " + userHome;
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testMultiExpression()
+        throws EvaluatorException
+    {
+        evaluator.addExpressionSource( new SystemPropertyExpressionSource() );
+
+        String userName = System.getProperty( "user.name" );
+        String userHome = System.getProperty( "user.home" );
+        String expression = "${user.name}'s home directory is ${user.home}";
+        String expected = userName + "'s home directory is " + userHome;
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    /**
+     * This use case was discovered by a user of archiva.
+     * The last expression doesn't get evaluated properly.
+     * <p/>
+     * The result (with the bug) was "2.0.4${prj.ver.suf}"
+     */
+    public void testMultiExpressionVersionBug()
+        throws EvaluatorException
+    {
+        Properties props = new Properties();
+        props.setProperty( "prj.ver.maj", "2" );
+        props.setProperty( "prj.ver.min", "0" );
+        props.setProperty( "prj.ver.inc", "4" );
+        props.setProperty( "prj.ver.suf", "-SNAPSHOT" );
+
+        PropertiesExpressionSource propsSource = new PropertiesExpressionSource();
+        propsSource.setProperties( props );
+        evaluator.addExpressionSource( propsSource );
+
+        String expression = "${prj.ver.maj}.${prj.ver.min}.${prj.ver.inc}${prj.ver.suf}";
+        String expected = "2.0.4-SNAPSHOT";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testEscaping()
+        throws EvaluatorException
+    {
+        evaluator.addExpressionSource( new SystemPropertyExpressionSource() );
+
+        String userName = System.getProperty( "user.name" );
+        String userHome = System.getProperty( "user.home" );
+        String expression = "${user.name}'s home directory is ${user.home} (fetched via $${user.home} expression)";
+        String expected = userName + "'s home directory is " + userHome + " (fetched via ${user.home} expression)";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testRecursiveSimple()
+        throws EvaluatorException
+    {
+        PropertiesExpressionSource propsource = new PropertiesExpressionSource();
+        Properties props = new Properties();
+
+        // Create intentional recursive lookup.
+        props.setProperty( "main.dir", "${target.dir}/classes" );
+        props.setProperty( "target.dir", "./target" );
+
+        propsource.setProperties( props );
+
+        evaluator.addExpressionSource( propsource );
+        evaluator.addExpressionSource( new SystemPropertyExpressionSource() );
+
+        String expression = "My classes directory is ${main.dir}";
+        String expected = "My classes directory is ./target/classes";
+
+        String actual = evaluator.expand( expression );
+        assertEquals( expected, actual );
+    }
+
+    public void testRecursiveCycle()
+    {
+        PropertiesExpressionSource propsource = new PropertiesExpressionSource();
+        Properties props = new Properties();
+
+        // Create intentional recursive lookup.
+        props.setProperty( "main.dir", "${test.dir}/target/classes" );
+        props.setProperty( "test.dir", "${main.dir}/target/test-classes" );
+
+        propsource.setProperties( props );
+
+        evaluator.addExpressionSource( propsource );
+        evaluator.addExpressionSource( new SystemPropertyExpressionSource() );
+
+        try
+        {
+            evaluator.expand( "My main dir is ${main.dir}" );
+            fail( "Should have thrown an EvaluatorException due to recursive cycle." );
+        }
+        catch ( EvaluatorException e )
+        {
+            // Expected path.
+        }
+    }
+}