diff --git a/src/java/org/apache/commons/lang/Interpolation.java b/src/java/org/apache/commons/lang/Interpolation.java new file mode 100644 index 000000000..bef9b1498 --- /dev/null +++ b/src/java/org/apache/commons/lang/Interpolation.java @@ -0,0 +1,182 @@ +package org.apache.commons.lang; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Performs basic variable interpolation on a String for variables within + * a Map. Variables of the form, ${var}, are supported. + * + * @author Ken Fitzpatrick + * @author Henri Yandell + */ +public class Interpolation { + + // QUERY: Anyway to escape the ${..} variable so it is not interpolated? + + // TODO: Consider making these configurable? + private static final String SYMBOLIC_VALUE_MARKER_START = "${"; + private static final String SYMBOLIC_VALUE_MARKER_END = "}"; + + /** + *

+ * Returns a String that is the result of having performed + * variable interpolation on templateString, + * using the value set found in values. + *

+ *

+ * The solution is compatible with all JDK versions + * where Jakarta/Commons/Lang also is supported. + *

+ *

+ * The expected format of templateString is: + *

+     *   The ${animal} jumped over the ${target}.
+     *
+ * such that the key/value pairs found in values + * are substituted into the string at the ${key-name} markers. + * In the above example, valuesMap could have been populated as: + *
+     *   Map valuesMap = HashMap();
+     *   valuesMap.put( "animal", "quick brown fox" );
+     *   valuesMap.put( "target", "lazy dog" );
+     *   String resolvedString = StringUtils.interpolate( templateString, valuesMap );
+     *
+ * yielding: + *
+     *   The quick brown fox jumped over the lazy dog.
+     *
+ *

+ *

+ * The same templateString from the above example could be reused as: + *

+     *   Map valuesMap = HashMap();
+     *   valuesMap.put( "animal", "cow" );
+     *   valuesMap.put( "target", "moon" );
+     *   String resolvedString = StringUtils.interpolate( templateString, valuesMap );
+     *
+ * yielding: + *
+     *   The cow jumped over the moon.
+     *
+ *

+ *

+ * The value of templateString is returned in an unaltered if templateString + * is null, empty, or contains no marked variables that can be resolved by the key/value pairs found in + * valuesMap, or if valuesMap is null, empty or has no key/value pairs that can be + * applied to the marked variables within templateString. + *

+ * @param templateString String containing any mixture of variable and non-variable + * content, to be used as a template for the value substitution process + * @param valuesMap Map containing the key/value pairs to be used to resolve + * the values of the marked variables found within templateString + * @return String + */ + public static String interpolate( String templateString, Map valuesMap ) { + // pre-conditions + if ( valuesMap == null ) + return templateString; + if ( templateString == null ) + return templateString; + if ( templateString.length() < 1 ) + return templateString; + if ( valuesMap.isEmpty() ) + return templateString; + + // default the returned String to the templateString + String returnString = templateString; + String nextKey = null; + Object substitutionBean = null; + String substitutionValue = null; + String nextValueToBeSubstituted = null; + + // get a list of substitution valuesMap + Iterator keys = valuesMap.keySet().iterator(); + + while( keys.hasNext() ) { + nextKey = ( String ) keys.next(); + substitutionValue = StringUtils.defaultString( ( String ) valuesMap.get( nextKey ) ); + nextValueToBeSubstituted = SYMBOLIC_VALUE_MARKER_START + nextKey + SYMBOLIC_VALUE_MARKER_END; + + returnString = StringUtils.replace( returnString, nextValueToBeSubstituted, substitutionValue ); + } + return returnString; + } + + + /** + *

+ * Returns a String that is the result of having performed variable interpolation on + * templateString, using the value set found in values, + * repeatedly until there are no changes. + *

+ *

+ * The expected format of templateString is: + *

+     *   The ${animal} jumped over the ${target}.
+     *
+ * such that the key/value pairs found in values are substituted into the string at the + * ${key-name} markers. In the above example, valuesMap + * could have been populated as: + *
+     *   Map valuesMap = HashMap();
+     *   valuesMap.put( "animal", "${critter}" );
+     *   valuesMap.put( "target", "${pet}" );
+     *   valuesMap.put( "pet", "${petCharacteristic} dog" );
+     *   valuesMap.put( "petCharacteristic", "lazy" );
+     *   valuesMap.put( "critter", "${critterSpeed} ${critterColor} ${critterType}" );
+     *   valuesMap.put( "critterSpeed", "quick" );
+     *   valuesMap.put( "critterColor", "brown" );
+     *   valuesMap.put( "critterType", "fox" );
+     *   String resolvedString = StringUtils.interpolate( templateString, valuesMap, true );
+     *
+ * yielding: + *
+     *   The quick brown fox jumped over the lazy dog.
+     *
+ *

+ * yielding: + *
+     *   The cow jumped over the moon.
+     *
+ *

+ *

+ * The value of templateString is returned in an unaltered form if + * templateString is null, empty, or + * contains no marked variables that can be resolved by the key/value pairs found in + * valuesMap, or if valuesMap is null, empty or has no key/value + * pairs that can be applied to the marked variables within templateString. + *

+ * @param templateString String containing any mixture of variable and non-variable + * content, to be used as a template for the value substitution process + * @param valuesMap Map containing the key/value pairs to be used to resolve + * the values of the marked variables found within templateString + * @return String + */ + public static String interpolateRepeatedly( + String templateString, + Map valuesMap) + { + // pre-conditions + if ( valuesMap == null ) + return templateString; + if ( templateString == null ) + return templateString; + if ( templateString.length() < 1 ) + return templateString; + if ( valuesMap.isEmpty() ) + return templateString; + + String currentResult = templateString; + String previousResult = null; + while( ! StringUtils.equals( currentResult, previousResult ) ) + { + previousResult = currentResult; + currentResult = Interpolation.interpolate( previousResult, valuesMap ); + } + + return currentResult; + } + +} diff --git a/src/test/org/apache/commons/lang/InterpolationTest.java b/src/test/org/apache/commons/lang/InterpolationTest.java new file mode 100644 index 000000000..991849abb --- /dev/null +++ b/src/test/org/apache/commons/lang/InterpolationTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import junit.textui.TestRunner; + +import java.util.Map; +import java.util.HashMap; + +/** + * Unit tests {@link org.apache.commons.lang.Interpolation}. + * + * @author Henri Yandell + * @author Ken Fitzpatrick + * @version $Id: InterpolationTest.java,v 1.1 2004/07/04 04:51:25 bayard Exp $ + */ +public class InterpolationTest extends TestCase { + + private static final String INPUT_TEMPLATE = "The ${animal} jumped over the ${target}."; + private static final String EXPECTED_RESULTS_1 = "The quick brown fox jumped over the lazy dog."; + private static final String EXPECTED_RESULTS_2 = "The cow jumped over the moon."; + + public InterpolationTest(String name) { + super(name); + } + + public static void main(String[] args) { + TestRunner.run(suite()); + } + + public static Test suite() { + TestSuite suite = new TestSuite(InterpolationTest.class); + suite.setName("Interpolation Tests"); + return suite; + } + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testSimpleVariableSubstitution() { + + // test case: "The quick brown fox jumped over the lazy dog." + Map valuesMap = new HashMap(); + valuesMap.put( "animal", "quick brown fox" ); + valuesMap.put( "target", "lazy dog" ); + assertEquals( "Test case 1: simple variable substitution", EXPECTED_RESULTS_1, + Interpolation.interpolate( INPUT_TEMPLATE, valuesMap) ); + + // test case: "The cow jumped over the moon." + valuesMap = new HashMap(); + valuesMap.put( "animal", "cow" ); + valuesMap.put( "target", "moon" ); + assertEquals( "Test case 2: template reuse, different results" ,EXPECTED_RESULTS_2, + Interpolation.interpolate( INPUT_TEMPLATE, valuesMap) ); + } + + public void testNullMap() { + // negative test case: Map == null + Map valuesMap = null; + assertEquals( "Test case 3: Map == null", INPUT_TEMPLATE, + Interpolation.interpolate( INPUT_TEMPLATE, valuesMap) ); + } + + public void testEmptyMap() { + // negative test case: Map.isEmpty() + Map valuesMap = new HashMap(); + assertEquals( "Test case 4: Map.isEmpty()", INPUT_TEMPLATE, + Interpolation.interpolate( INPUT_TEMPLATE, valuesMap) ); + } + + public void testNullTemplate() { + // negative test case: INPUT_TEMPLATE == null + Map valuesMap = new HashMap(); + valuesMap.put( "animal", "cow" ); + valuesMap.put( "target", "moon" ); + assertNull( "Test case 5: template == null", + Interpolation.interpolate( null, valuesMap) ); + } + + public void testRecursive() { + // test case: process repeatedly + Map valuesMap = new HashMap(); + valuesMap.put( "animal", "${critter}" ); + valuesMap.put( "target", "${pet}" ); + valuesMap.put( "pet", "${petCharacteristic} dog" ); + valuesMap.put( "petCharacteristic", "lazy" ); + valuesMap.put( "critter", "${critterSpeed} ${critterColor} ${critterType}" ); + valuesMap.put( "critterSpeed", "quick" ); + valuesMap.put( "critterColor", "brown" ); + valuesMap.put( "critterType", "fox" ); + assertEquals( "Test case 6: interpolateRepeatedly", EXPECTED_RESULTS_1, + Interpolation.interpolateRepeatedly( INPUT_TEMPLATE, valuesMap ) ); + + // test case: process repeatedly + valuesMap = new HashMap(); + valuesMap.put( "animal", "cow" ); + valuesMap.put( "target", "${celestialObject}" ); + valuesMap.put( "celestialObject", "moon" ); + assertEquals( "Test case 8: interpolateRepeatedly", EXPECTED_RESULTS_2, + Interpolation.interpolateRepeatedly( INPUT_TEMPLATE, valuesMap ) ); + } + +} diff --git a/src/test/org/apache/commons/lang/LangTestSuite.java b/src/test/org/apache/commons/lang/LangTestSuite.java index cb733f649..dbc69aedb 100644 --- a/src/test/org/apache/commons/lang/LangTestSuite.java +++ b/src/test/org/apache/commons/lang/LangTestSuite.java @@ -26,7 +26,7 @@ import junit.textui.TestRunner; * @author Stephen Colebourne * @author Ringo De Smet * @author Matthew Hawthorne - * @version $Id: LangTestSuite.java,v 1.26 2004/02/18 23:06:19 ggregory Exp $ + * @version $Id: LangTestSuite.java,v 1.27 2004/07/04 04:51:25 bayard Exp $ */ public class LangTestSuite extends TestCase { @@ -62,6 +62,7 @@ public class LangTestSuite extends TestCase { suite.addTest(EntitiesTest.suite()); suite.addTest(IllegalClassExceptionTest.suite()); suite.addTest(IncompleteArgumentExceptionTest.suite()); + suite.addTest(InterpolationTest.suite()); suite.addTest(NotImplementedExceptionTest.suite()); suite.addTest(NullArgumentExceptionTest.suite()); suite.addTest(NumberRangeTest.suite());