You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2015/05/21 12:04:53 UTC

[2/2] jclouds git commit: JCLOUDS-897: Remove the Rocoto dependency

JCLOUDS-897: Remove the Rocoto dependency


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/7053a787
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/7053a787
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/7053a787

Branch: refs/heads/master
Commit: 7053a7870da15c3996ca0fd6567f398ac9a5dfb0
Parents: 68a429f
Author: Ignasi Barrera <na...@apache.org>
Authored: Fri May 1 02:50:12 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Thu May 21 00:26:57 2015 +0200

----------------------------------------------------------------------
 .gitignore                                      |   2 +
 .../swift/internal/BasePayloadTest.java         |  16 ++-
 core/pom.xml                                    |  10 +-
 .../main/java/org/jclouds/ContextBuilder.java   |  23 ++--
 .../config/BindPropertiesToExpandedValues.java  |  62 -----------
 .../org/jclouds/functions/ExpandProperties.java | 110 +++++++++++++++++++
 .../java/org/jclouds/ContextBuilderTest.java    |  37 +++++--
 .../apis/JcloudsTestBlobStoreApiMetadata.java   |   2 +-
 .../BindPropertiesToExpandedValuesTest.java     |  45 --------
 .../jclouds/functions/ExpandPropertiesTest.java |  74 +++++++++++++
 10 files changed, 239 insertions(+), 142 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 3242d22..8c20bff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ TAGS
 atlassian-ide-plugin.xml
 .DS_Store
 .java-version
+.factorypath
+.apt_generated

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/apis/swift/src/test/java/org/jclouds/openstack/swift/internal/BasePayloadTest.java
----------------------------------------------------------------------
diff --git a/apis/swift/src/test/java/org/jclouds/openstack/swift/internal/BasePayloadTest.java b/apis/swift/src/test/java/org/jclouds/openstack/swift/internal/BasePayloadTest.java
index 8694f5a..8142397 100644
--- a/apis/swift/src/test/java/org/jclouds/openstack/swift/internal/BasePayloadTest.java
+++ b/apis/swift/src/test/java/org/jclouds/openstack/swift/internal/BasePayloadTest.java
@@ -20,27 +20,31 @@ import static org.jclouds.reflect.Reflection2.method;
 
 import java.net.URI;
 import java.util.List;
+import java.util.Properties;
 
+import org.jclouds.functions.ExpandProperties;
 import org.jclouds.json.config.GsonModule;
 import org.jclouds.json.config.GsonModule.DateAdapter;
 import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
 import org.jclouds.openstack.swift.SwiftApiMetadata;
 import org.jclouds.reflect.Invocation;
 import org.jclouds.rest.internal.GeneratedHttpRequest;
-import org.nnsoft.guice.rocoto.Rocoto;
-import org.nnsoft.guice.rocoto.configuration.ConfigurationModule;
 
 import com.google.common.base.Throwables;
+import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.name.Names;
 
 public class BasePayloadTest {
-   protected Injector i = Guice.createInjector(Rocoto.expandVariables(new ConfigurationModule() {
-      protected void bindConfigurations() {
-         bindProperties(new SwiftApiMetadata().getDefaultProperties());
+   protected Injector i = Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+         Properties expanded = new ExpandProperties().apply(new SwiftApiMetadata().getDefaultProperties());
+         Names.bindProperties(binder(), expanded);
          bind(DateAdapter.class).to(Iso8601DateAdapter.class);
       }
-   }), new GsonModule());
+   }, new GsonModule());
 
    protected GeneratedHttpRequest requestForArgs(List<Object> args) {
       try {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/pom.xml
----------------------------------------------------------------------
diff --git a/core/pom.xml b/core/pom.xml
index 10123f6..55e8424 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -38,10 +38,7 @@
   </scm>
 
   <properties>
-    <jclouds.osgi.import>
-      org.nnsoft.guice.rocoto*;version="[6.1,7)",
-      *
-    </jclouds.osgi.import>
+    <jclouds.osgi.import>*</jclouds.osgi.import>
     <jclouds.osgi.export>org.jclouds*;version=${project.version};-noimport:=true</jclouds.osgi.export>
     <jclouds.osgi.activator>org.jclouds.osgi.Activator</jclouds.osgi.activator>
   </properties>
@@ -61,11 +58,6 @@
       <artifactId>guice</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.99soft.guice</groupId>
-      <artifactId>rocoto</artifactId>
-      <version>6.2</version>
-    </dependency>
-    <dependency>
       <groupId>javax.inject</groupId>
       <artifactId>javax.inject</artifactId>
       <version>1</version>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/main/java/org/jclouds/ContextBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/jclouds/ContextBuilder.java b/core/src/main/java/org/jclouds/ContextBuilder.java
index e8052a9..8ca422f 100644
--- a/core/src/main/java/org/jclouds/ContextBuilder.java
+++ b/core/src/main/java/org/jclouds/ContextBuilder.java
@@ -58,10 +58,10 @@ import org.jclouds.concurrent.config.ConfiguresExecutorService;
 import org.jclouds.concurrent.config.ExecutorServiceModule;
 import org.jclouds.config.BindApiContextWithWildcardExtendsExplicitAndRawType;
 import org.jclouds.config.BindNameToContext;
-import org.jclouds.config.BindPropertiesToExpandedValues;
 import org.jclouds.domain.Credentials;
 import org.jclouds.events.config.ConfiguresEventBus;
 import org.jclouds.events.config.EventBusModule;
+import org.jclouds.functions.ExpandProperties;
 import org.jclouds.http.config.ConfiguresHttpCommandExecutorService;
 import org.jclouds.http.config.JavaUrlHttpCommandExecutorServiceModule;
 import org.jclouds.javax.annotation.Nullable;
@@ -314,7 +314,7 @@ public class ContextBuilder {
 
       Properties resolved = resolveProperties(unexpanded, providerId, keysToResolve, optionalKeys);
 
-      Properties expanded = expandProperties(resolved);
+      Properties expanded = new ExpandProperties().apply(resolved);
 
       Supplier<Credentials> credentialsSupplier = buildCredentialsSupplier(expanded);
 
@@ -351,10 +351,10 @@ public class ContextBuilder {
    
    private Properties currentStateToUnexpandedProperties() {
       Properties defaults = new Properties();
-      defaults.putAll(apiMetadata.getDefaultProperties());
+      putAllAsString(apiMetadata.getDefaultProperties(), defaults);
       defaults.setProperty(PROPERTY_PROVIDER, providerId);
       if (providerMetadata.isPresent()) {
-         defaults.putAll(providerMetadata.get().getDefaultProperties());
+         putAllAsString(providerMetadata.get().getDefaultProperties(), defaults);
          defaults.setProperty(PROPERTY_ISO3166_CODES, Joiner.on(',').join(providerMetadata.get().getIso3166Codes()));
       }
       if (endpoint.isPresent())
@@ -367,21 +367,22 @@ public class ContextBuilder {
       if (credential != null)
          defaults.setProperty(PROPERTY_CREDENTIAL, credential);
       if (overrides.isPresent())
-         defaults.putAll(checkNotNull(overrides.get(), "overrides"));
-      defaults.putAll(propertiesPrefixedWithJcloudsApiOrProviderId(getSystemProperties(), apiMetadata.getId(), providerId));
+         putAllAsString(overrides.get(), defaults);
+      putAllAsString(propertiesPrefixedWithJcloudsApiOrProviderId(getSystemProperties(), apiMetadata.getId(), providerId), defaults);
       return defaults;
    }
 
+   private static void putAllAsString(Map<?, ?> source, Properties target) {
+      for (Map.Entry<?, ?> entry : source.entrySet()) {
+         target.setProperty(entry.getKey().toString(), entry.getValue().toString());
+      }
+   }
+
    @VisibleForTesting
    protected Properties getSystemProperties() {
       return System.getProperties();
    }
 
-
-   private Properties expandProperties(final Properties resolved) {
-      return Guice.createInjector(GUICE_STAGE, new BindPropertiesToExpandedValues(resolved)).getInstance(Properties.class);
-   }
-
    public static Injector buildInjector(String name, ProviderMetadata providerMetadata, Supplier<Credentials> creds, List<Module> inputModules) {
       List<Module> modules = newArrayList();
       modules.addAll(inputModules);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/main/java/org/jclouds/config/BindPropertiesToExpandedValues.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/jclouds/config/BindPropertiesToExpandedValues.java b/core/src/main/java/org/jclouds/config/BindPropertiesToExpandedValues.java
deleted file mode 100644
index e3ae30b..0000000
--- a/core/src/main/java/org/jclouds/config/BindPropertiesToExpandedValues.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.jclouds.config;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.util.Properties;
-
-import javax.inject.Singleton;
-
-import org.jclouds.internal.FilterStringsBoundToInjectorByName;
-import org.nnsoft.guice.rocoto.Rocoto;
-import org.nnsoft.guice.rocoto.configuration.ConfigurationModule;
-
-import com.google.common.base.Predicates;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-
-/**
- * expands properties.
- */
-public class BindPropertiesToExpandedValues extends AbstractModule {
-   private final Properties resolved;
-
-   public BindPropertiesToExpandedValues(Properties resolved) {
-      this.resolved = checkNotNull(resolved, "resolved");
-   }
-
-   @Override
-   protected void configure() {
-      install(Rocoto.expandVariables(new ConfigurationModule() {
-
-         @Override
-         protected void bindConfigurations() {
-            bindProperties(resolved);
-         }
-
-         @Provides
-         @Singleton
-         protected Properties expanded(FilterStringsBoundToInjectorByName filterStringsBoundByName) {
-            Properties props = new Properties();
-            props.putAll(filterStringsBoundByName.apply(Predicates.<String> alwaysTrue()));
-            return props;
-         }
-      }));
-   }
-
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/main/java/org/jclouds/functions/ExpandProperties.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/jclouds/functions/ExpandProperties.java b/core/src/main/java/org/jclouds/functions/ExpandProperties.java
new file mode 100644
index 0000000..d75dc8d
--- /dev/null
+++ b/core/src/main/java/org/jclouds/functions/ExpandProperties.java
@@ -0,0 +1,110 @@
+/*
+ * 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.jclouds.functions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Maps;
+
+/**
+ * Resolves the values of the properties so they can be inferred from other
+ * properties.
+ */
+public class ExpandProperties implements Function<Properties, Properties> {
+
+   // Matches variables in a string such as ${foo.bar}
+   private static final Pattern VAR = Pattern.compile("\\$\\{[^\\}]+}");
+
+   @Override
+   public Properties apply(final Properties properties) {
+      checkNotNull(properties, "properties cannot be null");
+
+      // Only expand the properties that are Strings
+      Map<String, String> stringProperties = Maps.toMap(properties.stringPropertyNames(),
+            new Function<String, String>() {
+               @Override
+               public String apply(String input) {
+                  return properties.getProperty(input);
+               }
+            });
+
+      boolean pendingReplacements = true;
+      Map<String, String> propertiesToResolve = new HashMap<String, String>(stringProperties);
+
+      while (pendingReplacements) {
+         Map<String, String> leafs = leafs(propertiesToResolve);
+         if (leafs.isEmpty()) {
+            break;
+         }
+         pendingReplacements = resolveProperties(propertiesToResolve, leafs);
+      }
+
+      // Replace the values with the resolved ones
+      Properties resolved = new Properties();
+      resolved.putAll(properties);
+      for (Map.Entry<String, String> entry : propertiesToResolve.entrySet()) {
+         resolved.setProperty(entry.getKey(), entry.getValue());
+      }
+
+      return resolved;
+   }
+
+   private Map<String, String> leafs(Map<String, String> input) {
+      return Maps.filterValues(input, new Predicate<String>() {
+         @Override
+         public boolean apply(String input) {
+            Matcher m = VAR.matcher(input);
+            return !m.find();
+         }
+      });
+   }
+
+   private boolean resolveProperties(Map<String, String> properties, Map<String, String> variables) {
+      boolean anyReplacementDone = false;
+      for (String key : properties.keySet()) {
+         StringBuffer sb = new StringBuffer();
+         Matcher m = VAR.matcher(properties.get(key));
+         while (m.find()) {
+            String match = m.group();
+            // Remove the ${} from the matched variable
+            String var = match.substring(2, match.length() - 1);
+            // Avoid recursive properties. Only get he value if the variable
+            // is different than the current key
+            Optional<String> value = var.equals(key) ? Optional.<String> absent() : Optional.fromNullable(variables
+                  .get(var));
+            // Replace by the value or leave the original value
+            m.appendReplacement(sb, value.or("\\" + match));
+            if (value.isPresent()) {
+               anyReplacementDone = true;
+            }
+         }
+         m.appendTail(sb);
+         properties.put(key, sb.toString());
+      }
+      return anyReplacementDone;
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/test/java/org/jclouds/ContextBuilderTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/ContextBuilderTest.java b/core/src/test/java/org/jclouds/ContextBuilderTest.java
index ac59433..cdceeea 100644
--- a/core/src/test/java/org/jclouds/ContextBuilderTest.java
+++ b/core/src/test/java/org/jclouds/ContextBuilderTest.java
@@ -74,13 +74,13 @@ public class ContextBuilderTest {
       assertEquals(endpoint, URI.create("http://foo.service.com"));
    }
 
-  @Test
-  public void testContextName() {
-    ContextBuilder withNoName = testContextBuilder().endpoint("http://${jclouds.identity}.service.com").name("mytest")
-            .credentials("foo", "bar");
-    Context context = withNoName.build();
-    assertEquals(context.getName(), "mytest");
-  }
+   @Test
+   public void testContextName() {
+     ContextBuilder withNoName = testContextBuilder().endpoint("http://${jclouds.identity}.service.com").name("mytest")
+              .credentials("foo", "bar");
+     Context context = withNoName.build();
+     assertEquals(context.getName(), "mytest");
+   }
 
    @Test
    public void testProviderMetadataBoundWithCorrectEndpoint() {
@@ -129,7 +129,28 @@ public class ContextBuilderTest {
       String version = withVersionInProps.buildInjector().getInstance(Key.get(String.class, ApiVersion.class));
       assertEquals(version, "1.1");
    }
-   
+
+   @Test
+   public void testAllPropertiesAreStrings() {
+      Properties overrides = new Properties();
+      overrides.setProperty("foo", "bar");
+      overrides.put("one", 1);
+      overrides.put("two", 2.0f);
+      overrides.put("true", true);
+      overrides.put("object", new Object() {
+         @Override
+         public String toString() {
+            return "object";
+         }
+      });
+      Context withObjectsInProps = testContextBuilder().overrides(overrides).build();
+      Properties resolved = withObjectsInProps.getProviderMetadata().getDefaultProperties();
+      assertEquals(resolved.getProperty("foo"), "bar");
+      assertEquals(resolved.getProperty("one"), "1");
+      assertEquals(resolved.getProperty("true"), "true");
+      assertEquals(resolved.getProperty("object"), "object");
+   }
+
    @Test
    public void testAddHttpModuleIfNotPresent() {
       List<Module> modules = Lists.newArrayList();

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/test/java/org/jclouds/apis/JcloudsTestBlobStoreApiMetadata.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/apis/JcloudsTestBlobStoreApiMetadata.java b/core/src/test/java/org/jclouds/apis/JcloudsTestBlobStoreApiMetadata.java
index 485ebe0..9ccf6d9 100644
--- a/core/src/test/java/org/jclouds/apis/JcloudsTestBlobStoreApiMetadata.java
+++ b/core/src/test/java/org/jclouds/apis/JcloudsTestBlobStoreApiMetadata.java
@@ -27,7 +27,7 @@ import com.google.auto.service.AutoService;
  * Implementation of {@link ApiMetadata} for testing.
  */
 @AutoService(ApiMetadata.class)
-public class JcloudsTestBlobStoreApiMetadata extends BaseHttpApiMetadata {
+public class JcloudsTestBlobStoreApiMetadata extends BaseHttpApiMetadata<IntegrationTestClient> {
 
    public static Builder builder() {
       return new Builder();

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/test/java/org/jclouds/config/BindPropertiesToExpandedValuesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/config/BindPropertiesToExpandedValuesTest.java b/core/src/test/java/org/jclouds/config/BindPropertiesToExpandedValuesTest.java
deleted file mode 100644
index 35ce5cc..0000000
--- a/core/src/test/java/org/jclouds/config/BindPropertiesToExpandedValuesTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.jclouds.config;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.Properties;
-
-import org.testng.annotations.Test;
-
-import com.google.inject.Guice;
-
-/**
- * Tests behavior of BindPropertiesToExpandedValues
- */
-@Test(groups = "unit", testName = "BindPropertiesToExpandedValuesTest")
-public class BindPropertiesToExpandedValuesTest {
-
-   @Test
-   public void testExpand() {
-      Properties input = new Properties();
-      input.setProperty("id", "1234");
-      input.setProperty("path", "path:${id}");
-      Properties output = Guice.createInjector(new BindPropertiesToExpandedValues(input)).getInstance(Properties.class);
-
-      Properties expected = new Properties();
-      expected.setProperty("id", "1234");
-      expected.setProperty("path", "path:1234");
-      assertEquals(output, expected);
-   }
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7053a787/core/src/test/java/org/jclouds/functions/ExpandPropertiesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/functions/ExpandPropertiesTest.java b/core/src/test/java/org/jclouds/functions/ExpandPropertiesTest.java
new file mode 100644
index 0000000..e2a721f
--- /dev/null
+++ b/core/src/test/java/org/jclouds/functions/ExpandPropertiesTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.jclouds.functions;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Properties;
+
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "ExpandPropertiesTest")
+public class ExpandPropertiesTest {
+
+   @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "properties cannot be null")
+   public void testPropertiesMandatory() {
+      new ExpandProperties().apply(null);
+   }
+
+   @Test
+   public void testResolveProperties() {
+      Properties props = new Properties();
+      props.put("number", 1);
+      props.put("two", "2");
+      props.put("greeting", "hello");
+      props.put("simple", "simple: ${greeting}");
+      props.put("nested", "nested: ${simple}");
+      props.put("mixed", "mixed: ${nested} and ${simple}");
+      props.put("unexisting", "${foobar} substitution");
+      props.put("recursive", "variable5 ${recursive} recursive ${unexisting}");
+      props.put("characters{{$$", "characters");
+      props.put("ugly", "substitute: ${characters{{$$}");
+
+      Properties resolved = new ExpandProperties().apply(props);
+
+      assertEquals(resolved.size(), props.size());
+      assertEquals(resolved.get("number"), 1);
+      assertEquals(resolved.get("two"), "2");
+      assertEquals(resolved.get("greeting"), "hello");
+      assertEquals(resolved.get("simple"), "simple: hello");
+      assertEquals(resolved.get("nested"), "nested: simple: hello");
+      assertEquals(resolved.get("mixed"), "mixed: nested: simple: hello and simple: hello");
+      assertEquals(resolved.get("unexisting"), "${foobar} substitution");
+      assertEquals(resolved.get("recursive"), "variable5 ${recursive} recursive ${unexisting}");
+      assertEquals(resolved.get("ugly"), "substitute: characters");
+   }
+
+   @Test
+   public void testNoLeafs() {
+      Properties props = new Properties();
+      props.put("one", "${two}");
+      props.put("two", "${one}");
+
+      Properties resolved = new ExpandProperties().apply(props);
+
+      assertEquals(resolved.size(), props.size());
+      assertEquals(resolved.get("one"), "${two}");
+      assertEquals(resolved.get("two"), "${one}");
+   }
+
+}