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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-text.git

commit 36798250bb098a139042d5138ffd02fa07735a6b
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Sat Apr 6 18:28:20 2024 -0400

    Add StringLookupFactory.builder() for fencing Path resolution of the
    file, properties and XML lookups
---
 src/changes/changes.xml                            |   1 +
 .../commons/text/lookup/StringLookupFactory.java   | 274 +++++++++++++++------
 .../commons/text/lookup/FileStringLookupTest.java  |  57 +++--
 .../text/lookup/PropertiesStringLookupTest.java    |  43 ++--
 .../external/CustomStringSubstitutorTest.java      |  49 ++++
 5 files changed, 317 insertions(+), 107 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7c4228bb..724f5098 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -50,6 +50,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action                  type="add" dev="ggregory" due-to="Gary 
Gregory">Add StringLookupFactory.fileStringLookup(Path...) and deprecated 
fileStringLookup().</action>
     <action                  type="add" dev="ggregory" due-to="Gary 
Gregory">Add StringLookupFactory.propertiesStringLookup(Path...) and deprecated 
propertiesStringLookup().</action>
     <action                  type="add" dev="ggregory" due-to="Gary 
Gregory">Add StringLookupFactory.xmlStringLookup(Map, Path...) and deprecated 
xmlStringLookup() and xmlStringLookup(Map).</action>
+    <action                  type="add" dev="ggregory" due-to="Gary 
Gregory">Add StringLookupFactory.builder() for fencing Path resolution of the 
file, properties and XML lookups.</action>
     <!-- FIX -->
     <action issue="TEXT-232" type="fix" dev="ggregory" due-to="Arnout Engelen, 
Gary Gregory">WordUtils.containsAllWords​() may throw 
PatternSyntaxException.</action>
     <action issue="TEXT-175" type="fix" dev="ggregory" due-to="David Lavati, 
seanfabs, Gary Gregory, Bruno P. Kinoshita">Fix regression for determining 
whitespace in WordUtils #519.</action>
diff --git 
a/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java 
b/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
index a80e13ee..e60e4917 100644
--- a/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
+++ b/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.function.Supplier;
 
 import javax.xml.xpath.XPathFactory;
 
@@ -216,6 +217,49 @@ import org.apache.commons.text.StringSubstitutor;
  */
 public final class StringLookupFactory {
 
+    /**
+     * Builds instance of {@link StringLookupFactory}.
+     *
+     * @since 1.12.0
+     */
+    public static final class Builder implements Supplier<StringLookupFactory> 
{
+
+        /**
+         * Fences.
+         */
+        private Path[] fences;
+
+        @Override
+        public StringLookupFactory get() {
+            return new StringLookupFactory(fences);
+        }
+
+        /**
+         * Sets Path resolution fences.
+         * <p>
+         * Path Fences apply to the file, property, and XML string lookups.
+         * </p>
+         *
+         * @param fences Path resolution fences.
+         * @return this.
+         */
+        public Builder setFences(final Path... fences) {
+            this.fences = fences;
+            return this;
+        }
+
+    }
+
+    /**
+     * Constructs a new {@link Builder}.
+     *
+     * @return a new {@link Builder}
+     * @since 1.12.0
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
     /**
      * Internal class used to construct the default {@link StringLookup} map 
used by {@link StringLookupFactory#addDefaultStringLookups(Map)}.
      */
@@ -570,10 +614,22 @@ public final class StringLookupFactory {
     }
 
     /**
-     * No need to build instances for now.
+     * Fences.
+     */
+    private final Path[] fences;
+
+    /**
+     * Constructs a new instance.
      */
     private StringLookupFactory() {
-        // empty
+        this(null);
+    }
+
+    /**
+     * Constructs a new instance.
+     */
+    private StringLookupFactory(final Path[] fences) {
+        this.fences = fences;
     }
 
     /**
@@ -816,62 +872,94 @@ public final class StringLookupFactory {
     }
 
     /**
-     * Returns the FileStringLookup singleton instance.
+     * Returns a file StringLookup instance.
      * <p>
-     * Using a {@link StringLookup} from the {@link StringLookupFactory}:
+     * If this factory was built using {@link Builder#setFences(Path...)}, 
then the string lookup is fenced and will throw an {@link 
IllegalArgumentException}
+     * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
+     * </p>
+     * <em>Using a fenced StringLookup</em>
+     * <p>
+     * To use a fenced {@link StringLookup}, use {@link 
StringLookupFactory#builder()}:
+     * </p>
+     *
+     * <pre>
+     * // Make the fence the current directory
+     * StringLookupFactory factory = 
StringLookupFactory.builder().setFences(Paths.get("")).get();
+     * factory.fileStringLookup().lookup("UTF-8:com/domain/document.txt");
+     *
+     * // throws IllegalArgumentException
+     * factory.fileStringLookup().lookup("UTF-8:/rootdir/foo/document.txt");
+     *
+     * // throws IllegalArgumentException
+     * factory.fileStringLookup().lookup("UTF-8:../document.txt");
+     * </pre>
+     *
+     * <em>Using an unfenced StringLookup</em>
+     * <p>
+     * To use an unfenced {@link StringLookup}, use {@link 
StringLookupFactory#INSTANCE}:
      * </p>
      *
      * <pre>
      * 
StringLookupFactory.INSTANCE.fileStringLookup().lookup("UTF-8:com/domain/document.properties");
      * </pre>
+     *
+     * <em>Using a StringLookup with StringSubstitutor</em>
      * <p>
-     * Using a {@link StringSubstitutor}:
+     * To build a fenced StringSubstitutor, use:
+     * </p>
+     *
+     * <pre>
+     * // Make the fence the current directory
+     * final StringLookupFactory factory = 
StringLookupFactory.builder().setFences(Paths.get("")).get();
+     * final StringSubstitutor stringSubstitutor = new 
StringSubstitutor(factory.interpolatorStringLookup());
+     * stringSubstitutor.replace("... ${file:UTF-8:com/domain/document.txt} 
..."));
+     *
+     * // throws IllegalArgumentException
+     * stringSubstitutor.replace("... ${file:UTF-8:/rootdir/foo/document.txt} 
..."));
+     * </pre>
+     * <p>
+     * Using an unfenced {@link StringSubstitutor}:
      * </p>
      *
      * <pre>
-     * StringSubstitutor.createInterpolator().replace("... 
${file:UTF-8:com/domain/document.properties} ..."));
+     * StringSubstitutor.createInterpolator().replace("... 
${file:UTF-8:com/domain/document.txt} ..."));
      * </pre>
      * <p>
-     * The above examples convert {@code 
"UTF-8:com/domain/document.properties"} to the contents of the file.
+     * The above examples convert {@code "UTF-8:com/domain/document.txt"} to 
the contents of the file.
      * </p>
      *
-     * @return The FileStringLookup singleton instance.
+     * @return a file StringLookup instance.
      * @since 1.5
-     * @deprecated Use {@link #fileStringLookup(Path...)}.
      */
-    @Deprecated
     public StringLookup fileStringLookup() {
-        return FileStringLookup.INSTANCE;
+        return fences != null ? fileStringLookup(fences) : 
FileStringLookup.INSTANCE;
     }
 
     /**
-     * Returns a fenced FileStringLookup instance.
+     * Returns a fenced file StringLookup instance.
      * <p>
-     * Using a {@link StringLookup} from the {@link StringLookupFactory} 
fenced by the current directory ({@code Paths.get("")}):
+     * To use a {@link StringLookup} fenced by the current directory, use:
      * </p>
      *
      * <pre>
-     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("UTF-8:com/domain/document.properties");
-     * </pre>
-     * <p>
-     * Using a {@link StringSubstitutor} fenced by the current directory 
({@code Paths.get("")}):
-     * </p>
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("UTF-8:com/domain/document.txt");
      *
-     * <pre>
-     * StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
-     * final InterpolatorStringLookup stringLookup = 
(InterpolatorStringLookup) stringSubstitutor.getStringLookup();
-     * stringLookup.getStringLookupMap().replace(StringLookupFactory.KEY_FILE, 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")));
-     * stringSubstitutor.replace("... 
${file:UTF-8:com/domain/document.properties} ..."));
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("UTF-8:/rootdir/foo/document.txt");
+     *
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("UTF-8:../com/domain/document.txt");
      * </pre>
      * <p>
-     * The above examples convert {@code 
"UTF-8:com/domain/document.properties"} to the contents of the file.
+     * The above example converts {@code "UTF-8:com/domain/document.txt"} to 
the contents of the file.
      * </p>
      * <p>
-     * Methods {@link StringSubstitutor#replace(String)} will throw a {@link 
IllegalArgumentException} when a file doesn't resolves in a fence.
+     * {@link StringSubstitutor} methods like {@link 
StringSubstitutor#replace(String)} will throw a {@link 
IllegalArgumentException} when a file doesn't
+     * resolves in a fence.
      * </p>
      *
      * @param fences The fences guarding Path resolution.
-     * @return The FileStringLookup singleton instance.
+     * @return a file StringLookup instance.
      * @since 1.12.0
      */
     public StringLookup fileStringLookup(final Path... fences) {
@@ -1048,9 +1136,13 @@ public final class StringLookupFactory {
     }
 
     /**
-     * Returns the PropertiesStringLookup singleton instance.
+     * Returns a Properties StringLookup instance.
      * <p>
-     * Looks up the value for the key in the format "DocumentPath::MyKey".
+     * If this factory was built using {@link Builder#setFences(Path...)}, 
then the string lookup is fenced and will throw an {@link 
IllegalArgumentException}
+     * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
+     * </p>
+     * <p>
+     * We looks up a value for the key in the format "DocumentPath::MyKey".
      * </p>
      * <p>
      * Note the use of "::" instead of ":" to allow for "C:" drive letters in 
paths.
@@ -1058,16 +1150,48 @@ public final class StringLookupFactory {
      * <p>
      * For example: "com/domain/document.properties::MyKey".
      * </p>
+     * <em>Using a fenced StringLookup</em>
+     * <p>
+     * To use a fenced {@link StringLookup}, use {@link 
StringLookupFactory#builder()}:
+     * </p>
+     *
+     * <pre>
+     * // Make the fence the current directory
+     * StringLookupFactory factory = 
StringLookupFactory.builder().setFences(Paths.get("")).get();
+     * 
factory.propertiesStringLookup().lookup("com/domain/document.properties::MyKey");
+     *
+     * // throws IllegalArgumentException
+     * 
factory.propertiesStringLookup().lookup("/com/domain/document.properties::MyKey");
      *
+     * // throws IllegalArgumentException
+     * 
factory.propertiesStringLookup().lookup("../com/domain/document.properties::MyKey");
+     * </pre>
+     *
+     * <em>Using an unfenced StringLookup</em>
      * <p>
-     * Using a {@link StringLookup} from the {@link StringLookupFactory}:
+     * To use an unfenced {@link StringLookup}, use {@link 
StringLookupFactory#INSTANCE}:
      * </p>
      *
      * <pre>
      * 
StringLookupFactory.INSTANCE.propertiesStringLookup().lookup("com/domain/document.properties::MyKey");
      * </pre>
+     *
+     * <em>Using a StringLookup with StringSubstitutor</em>
      * <p>
-     * Using a {@link StringSubstitutor}:
+     * To build a fenced StringSubstitutor, use:
+     * </p>
+     *
+     * <pre>
+     * // Make the fence the current directory
+     * final StringLookupFactory factory = 
StringLookupFactory.builder().setFences(Paths.get("")).get();
+     * final StringSubstitutor stringSubstitutor = new 
StringSubstitutor(factory.interpolatorStringLookup());
+     * stringSubstitutor.replace("... 
${properties:com/domain/document.properties::MyKey} ..."));
+     *
+     * // throws IllegalArgumentException
+     * stringSubstitutor.replace("... 
${properties:/rootdir/foo/document.properties::MyKey} ..."));
+     * </pre>
+     * <p>
+     * Using an unfenced {@link StringSubstitutor}:
      * </p>
      *
      * <pre>
@@ -1078,17 +1202,15 @@ public final class StringLookupFactory {
      * "com/domain/document.properties".
      * </p>
      *
-     * @return The PropertiesStringLookup singleton instance.
+     * @return a Properties StringLookup instance.
      * @since 1.5
-     * @deprecated Use {@link #propertiesStringLookup(Path...)}.
      */
-    @Deprecated
     public StringLookup propertiesStringLookup() {
-        return PropertiesStringLookup.INSTANCE;
+        return fences != null ? propertiesStringLookup(fences) : 
PropertiesStringLookup.INSTANCE;
     }
 
     /**
-     * Returns a fenced PropertiesStringLookup instance.
+     * Returns a fenced Properties StringLookup instance.
      * <p>
      * Looks up the value for the key in the format "DocumentPath::MyKey":.
      * </p>
@@ -1098,34 +1220,30 @@ public final class StringLookupFactory {
      * <p>
      * For example: "com/domain/document.properties::MyKey".
      * </p>
-     *
      * <p>
-     * Using a {@link StringLookup} from the {@link StringLookupFactory} 
fenced by the current directory ({@code Paths.get("")}):
+     * To use a {@link StringLookup} fenced by the current directory, use:
      * </p>
      *
      * <pre>
-     * 
StringLookupFactory.INSTANCE.propertiesStringLookup(Paths.get("")).lookup("com/domain/document.properties::MyKey");
-     * </pre>
-     * <p>
-     * Using a {@link StringSubstitutor} fenced by the current directory 
({@code Paths.get("")}):
-     * </p>
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("com/domain/document.properties::MyKey");
      *
-     * <pre>
-     * StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
-     * final InterpolatorStringLookup stringLookup = 
(InterpolatorStringLookup) stringSubstitutor.getStringLookup();
-     * 
stringLookup.getStringLookupMap().replace(StringLookupFactory.KEY_PROPERTIES, 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")));
-     * stringSubstitutor.replace("... 
${properties:com/domain/document.properties::MyKey} ..."));
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("com/domain/document.properties::MyKey");
+     *
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")).lookup("com/domain/document.properties::MyKey");
      * </pre>
      * <p>
-     * The above examples convert {@code 
"com/domain/document.properties::MyKey"} to the key value in the properties 
file at the path
+     * The above example converts {@code 
"com/domain/document.properties::MyKey"} to the key value in the properties 
file at the path
      * "com/domain/document.properties".
      * </p>
      * <p>
-     * Methods {@link StringSubstitutor#replace(String)} will throw a {@link 
IllegalArgumentException} when a file doesn't resolves in a fence.
+     * {@link StringSubstitutor} methods like {@link 
StringSubstitutor#replace(String)} will throw a {@link 
IllegalArgumentException} when a file doesn't
+     * resolves in a fence.
      * </p>
      *
      * @param fences The fences guarding Path resolution.
-     * @return The PropertiesStringLookup singleton instance.
+     * @return a Properties StringLookup instance.
      * @since 1.12.0
      */
     public StringLookup propertiesStringLookup(final Path... fences) {
@@ -1434,9 +1552,13 @@ public final class StringLookupFactory {
     }
 
     /**
-     * Returns the XmlStringLookup singleton instance.
+     * Returns an XML StringLookup instance.
+     * <p>
+     * If this factory was built using {@link Builder#setFences(Path...)}, 
then the string lookup is fenced and will throw an {@link 
IllegalArgumentException}
+     * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
+     * </p>
      * <p>
-     * Looks up the value for the key in the format "DocumentPath:XPath".
+     * We look up the value for the key in the format "DocumentPath:XPath".
      * </p>
      * <p>
      * For example: "com/domain/document.xml:/path/to/node".
@@ -1459,19 +1581,21 @@ public final class StringLookupFactory {
      * The above examples convert {@code 
"com/domain/document.xml:/path/to/node"} to the value of the XPath in the XML 
document.
      * </p>
      *
-     * @return The XmlStringLookup singleton instance.
+     * @return An XML StringLookup instance.
      * @since 1.5
-     * @deprecated Use {@link #xmlStringLookup(Map, Path...)}.
      */
-    @Deprecated
     public StringLookup xmlStringLookup() {
-        return XmlStringLookup.INSTANCE;
+        return fences != null ? 
xmlStringLookup(XmlStringLookup.DEFAULT_FEATURES, fences) : 
XmlStringLookup.INSTANCE;
     }
 
     /**
-     * Returns a XmlStringLookup instance.
+     * Returns an XML StringLookup instance.
+     * <p>
+     * If this factory was built using {@link Builder#setFences(Path...)}, 
then the string lookup is fenced and will throw an {@link 
IllegalArgumentException}
+     * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
+     * </p>
      * <p>
-     * Looks up the value for the key in the format "DocumentPath:XPath".
+     * We look up the value for the key in the format "DocumentPath:XPath".
      * </p>
      * <p>
      * For example: "com/domain/document.xml:/path/to/node".
@@ -1495,20 +1619,22 @@ public final class StringLookupFactory {
      * </p>
      *
      * @param xPathFactoryFeatures XPathFactory features to set.
-     * @return The XmlStringLookup singleton instance.
+     * @return An XML StringLookup instance.
      * @see XPathFactory#setFeature(String, boolean)
      * @since 1.11.0
-     * @deprecated Use {@link #xmlStringLookup(Map, Path...)}.
      */
-    @Deprecated
     public StringLookup xmlStringLookup(final Map<String, Boolean> 
xPathFactoryFeatures) {
-        return new XmlStringLookup(xPathFactoryFeatures);
+        return xmlStringLookup(xPathFactoryFeatures, fences);
     }
 
     /**
-     * Returns a fenced XmlStringLookup instance.
+     * Returns a fenced XML StringLookup instance.
+     * <p>
+     * If this factory was built using {@link Builder#setFences(Path...)}, 
then the string lookup is fenced and will throw an {@link 
IllegalArgumentException}
+     * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
+     * </p>
      * <p>
-     * Looks up the value for the key in the format "DocumentPath:XPath".
+     * We look up the value for the key in the format "DocumentPath:XPath".
      * </p>
      * <p>
      * For example: "com/domain/document.xml:/path/to/node".
@@ -1521,22 +1647,30 @@ public final class StringLookupFactory {
      * StringLookupFactory.INSTANCE.xmlStringLookup(map, 
Pathe.get("")).lookup("com/domain/document.xml:/path/to/node");
      * </pre>
      * <p>
-     * Using a {@link StringSubstitutor}:
+     * <p>
+     * To use a {@link StringLookup} fenced by the current directory, use:
      * </p>
      *
      * <pre>
-     * StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
-     * final InterpolatorStringLookup stringLookup = 
(InterpolatorStringLookup) stringSubstitutor.getStringLookup();
-     * 
stringLookup.getStringLookupMap().replace(StringLookupFactory.KEY_PROPERTIES, 
StringLookupFactory.INSTANCE.fileStringLookup(Paths.get("")));
-     * stringSubstitutor.replace("... 
${xml:com/domain/document.xml:/path/to/node} ..."));
+     * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("com/domain/document.xml:/path/to/node");
+     *
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("/rootdir/foo/document.xml:/path/to/node");
+     *
+     * // throws IllegalArgumentException
+     * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("../com/domain/document.xml:/path/to/node");
      * </pre>
      * <p>
      * The above examples convert {@code 
"com/domain/document.xml:/path/to/node"} to the value of the XPath in the XML 
document.
      * </p>
+     * <p>
+     * {@link StringSubstitutor} methods like {@link 
StringSubstitutor#replace(String)} will throw a {@link 
IllegalArgumentException} when a file doesn't
+     * resolves in a fence.
+     * </p>
      *
      * @param xPathFactoryFeatures XPathFactory features to set.
      * @param fences               The fences guarding Path resolution.
-     * @return The XmlStringLookup singleton instance.
+     * @return An XML StringLookup instance.
      * @since 1.12.0
      */
     public StringLookup xmlStringLookup(final Map<String, Boolean> 
xPathFactoryFeatures, final Path... fences) {
diff --git 
a/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java 
b/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java
index c9dd6742..b22af473 100644
--- a/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java
+++ b/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java
@@ -17,14 +17,17 @@
 
 package org.apache.commons.text.lookup;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringSubstitutor;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -33,8 +36,25 @@ import org.junit.jupiter.api.Test;
  */
 public class FileStringLookupTest {
 
+    private static final Path DOCUEMENT_PATH = 
Paths.get("src/test/resources/org/apache/commons/text/document.properties");
     private static final Path CURRENT_PATH = Paths.get(StringUtils.EMPTY);
 
+    public static String readDocumentFixtureString() throws IOException {
+        return new String(Files.readAllBytes(DOCUEMENT_PATH), 
StandardCharsets.UTF_8);
+    }
+
+    public static void testFence(final String expectedString, final 
FileStringLookup fileStringLookup) {
+        Assertions.assertEquals(expectedString, 
fileStringLookup.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
+        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:/src/test/resources/org/apache/commons/text/document.properties"));
+        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:../src/test/resources/org/apache/commons/text/document.properties"));
+    }
+
+    public static void testFence(final StringSubstitutor stringSubstitutor) 
throws IOException {
+        assertEquals(readDocumentFixtureString(), 
stringSubstitutor.replace("${file:UTF-8:" + DOCUEMENT_PATH + "}"));
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${file:UTF-8:/foo.txt}"));
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${file:UTF-8:../foo.txt}"));
+    }
+
     @Test
     public void testDefaultInstanceBadCharsetName() {
         assertThrows(IllegalArgumentException.class,
@@ -58,8 +78,7 @@ public class FileStringLookupTest {
 
     @Test
     public void testDefaultInstanceOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         Assertions.assertEquals(expectedString, 
FileStringLookup.INSTANCE.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
     }
 
@@ -74,47 +93,49 @@ public class FileStringLookupTest {
         final FileStringLookup fileStringLookup = new 
FileStringLookup(Paths.get("dir does not exist at all"));
         assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
         assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:/src/test/resources/org/apache/commons/text/document.properties"));
+        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:../src/test/resources/org/apache/commons/text/document.properties"));
     }
 
     @Test
     public void testFenceBadDirPlusGoodOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         final FileStringLookup fileStringLookup = new 
FileStringLookup(Paths.get("dir does not exist at all"), CURRENT_PATH);
-        Assertions.assertEquals(expectedString, 
fileStringLookup.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
-        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:/src/test/resources/org/apache/commons/text/document.properties"));
+        testFence(expectedString, fileStringLookup);
     }
 
     @Test
     public void testFenceCurrentDirOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         final FileStringLookup fileStringLookup = new 
FileStringLookup(CURRENT_PATH);
-        Assertions.assertEquals(expectedString, 
fileStringLookup.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
-        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:/src/test/resources/org/apache/commons/text/document.properties"));
+        testFence(expectedString, fileStringLookup);
     }
 
     @Test
     public void testFenceCurrentDirPlusOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         final FileStringLookup fileStringLookup = new 
FileStringLookup(Paths.get("target"), CURRENT_PATH);
-        Assertions.assertEquals(expectedString, 
fileStringLookup.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
-        assertThrows(IllegalArgumentException.class, () -> 
fileStringLookup.lookup("UTF-8:/src/test/resources/org/apache/commons/text/document.properties"));
+        testFence(expectedString, fileStringLookup);
     }
 
     @Test
     public void testFenceEmptyOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         Assertions.assertEquals(expectedString, new 
FileStringLookup().lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
     }
 
     @Test
     public void testFenceNullOne() throws Exception {
-        final byte[] expectedBytes = 
Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties"));
-        final String expectedString = new String(expectedBytes, 
StandardCharsets.UTF_8);
+        final String expectedString = readDocumentFixtureString();
         Assertions.assertEquals(expectedString,
                 new FileStringLookup((Path[]) 
null).lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties"));
     }
+
+    @Test
+    public void testInterpolatorReplace() throws IOException {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        assertEquals(readDocumentFixtureString(), 
stringSubstitutor.replace("${file:UTF-8:" + DOCUEMENT_PATH + "}"));
+        final InterpolatorStringLookup stringLookup = 
(InterpolatorStringLookup) stringSubstitutor.getStringLookup();
+        
stringLookup.getStringLookupMap().replace(StringLookupFactory.KEY_FILE, 
StringLookupFactory.INSTANCE.fileStringLookup(CURRENT_PATH));
+        testFence(stringSubstitutor);
+    }
 }
diff --git 
a/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java 
b/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java
index a296c6d5..d7068101 100644
--- 
a/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java
+++ 
b/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java
@@ -35,13 +35,25 @@ import org.junit.jupiter.api.Test;
  */
 public class PropertiesStringLookupTest {
 
-    private static final Path[] NULL_PATH_ARRAY = null;
     private static final Path CURRENT_PATH = Paths.get(StringUtils.EMPTY); // 
NOT "."!
     private static final String DOC_RELATIVE = 
"src/test/resources/org/apache/commons/text/document.properties";
     private static final String DOC_ROOT = "/foo.txt";
     private static final String KEY = "mykey";
     private static final String KEY_RELATIVE = 
PropertiesStringLookup.toPropertyKey(DOC_RELATIVE, KEY);
     private static final String KEY_ROOT = 
PropertiesStringLookup.toPropertyKey(DOC_ROOT, KEY);
+    private static final Path[] NULL_PATH_ARRAY = null;
+
+    public static void testFence(final StringSubstitutor stringSubstitutor) {
+        assertEquals("Hello World!", stringSubstitutor.replace("${properties:" 
+ KEY_RELATIVE + "}"));
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${file:UTF-8:/foo.txt}"));
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${file:UTF-8:../foo.txt}"));
+    }
+
+    @Test
+    public void testFenceOne() {
+        assertThrows(IllegalArgumentException.class, () -> new 
PropertiesStringLookup(CURRENT_PATH).lookup(KEY_ROOT));
+        assertThrows(IllegalArgumentException.class, () -> new 
PropertiesStringLookup(Paths.get("not a dir at all"), 
CURRENT_PATH).lookup(KEY_ROOT));
+    }
 
     @Test
     public void testInterpolator() {
@@ -50,13 +62,21 @@ public class PropertiesStringLookupTest {
     }
 
     @Test
-    public void testInterpolatorReplaceFile() {
+    public void testInterpolatorNestedColon() {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        // Need to handle "C:" in the sys prop user.dir.
+        final String replaced = 
stringSubstitutor.replace("$${properties:${sys:user.dir}/" + KEY_RELATIVE + 
"}");
+        assertEquals("${properties:" + System.getProperty("user.dir") + 
"/src/test/resources/org/apache/commons/text/document.properties::mykey}", 
replaced);
+        assertEquals("Hello World!", stringSubstitutor.replace(replaced));
+    }
+
+    @Test
+    public void testInterpolatorReplace() {
         final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
         assertEquals("Hello World!", stringSubstitutor.replace("${properties:" 
+ KEY_RELATIVE + "}"));
         final InterpolatorStringLookup stringLookup = 
(InterpolatorStringLookup) stringSubstitutor.getStringLookup();
         
stringLookup.getStringLookupMap().replace(StringLookupFactory.KEY_FILE, 
StringLookupFactory.INSTANCE.fileStringLookup(CURRENT_PATH));
-        assertEquals("Hello World!", stringSubstitutor.replace("${properties:" 
+ KEY_RELATIVE + "}"));
-        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${file:UTF-8:/foo.txt}"));
+        testFence(stringSubstitutor);
     }
 
     @Test
@@ -69,15 +89,6 @@ public class PropertiesStringLookupTest {
         assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${properties:UTF-8:/foo.txt}"));
     }
 
-    @Test
-    public void testInterpolatorNestedColon() {
-        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
-        // Need to handle "C:" in the sys prop user.dir.
-        final String replaced = 
stringSubstitutor.replace("$${properties:${sys:user.dir}/" + KEY_RELATIVE + 
"}");
-        assertEquals("${properties:" + System.getProperty("user.dir") + 
"/src/test/resources/org/apache/commons/text/document.properties::mykey}", 
replaced);
-        assertEquals("Hello World!", stringSubstitutor.replace(replaced));
-    }
-
     @Test
     public void testInterpolatorWithParameterizedKey() {
         final Map<String, String> map = new HashMap<>();
@@ -135,12 +146,6 @@ public class PropertiesStringLookupTest {
         assertThrows(IllegalArgumentException.class, () -> new 
PropertiesStringLookup(CURRENT_PATH).lookup(KEY_ROOT));
     }
 
-    @Test
-    public void testFenceOne() {
-        assertThrows(IllegalArgumentException.class, () -> new 
PropertiesStringLookup(CURRENT_PATH).lookup(KEY_ROOT));
-        assertThrows(IllegalArgumentException.class, () -> new 
PropertiesStringLookup(Paths.get("not a dir at all"), 
CURRENT_PATH).lookup(KEY_ROOT));
-    }
-
     @Test
     public void testToString() {
         // does not blow up and gives some kind of string.
diff --git 
a/src/test/java/org/apache/commons/text/lookup/external/CustomStringSubstitutorTest.java
 
b/src/test/java/org/apache/commons/text/lookup/external/CustomStringSubstitutorTest.java
new file mode 100644
index 00000000..81ac1f1e
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/text/lookup/external/CustomStringSubstitutorTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.commons.text.lookup.external;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import org.apache.commons.text.StringSubstitutor;
+import org.apache.commons.text.lookup.FileStringLookupTest;
+import org.apache.commons.text.lookup.PropertiesStringLookupTest;
+import org.apache.commons.text.lookup.StringLookupFactory;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests building a fenced {@link StringSubstitutor} to exclude the use of 
package-private elements.
+ */
+public class CustomStringSubstitutorTest {
+
+    private StringSubstitutor createStringSubstitutor() {
+        final StringLookupFactory factory = 
StringLookupFactory.builder().setFences(Paths.get("")).get();
+        return new StringSubstitutor(factory.interpolatorStringLookup());
+    }
+
+    @Test
+    public void testFencedFiles() throws IOException {
+        FileStringLookupTest.testFence(createStringSubstitutor());
+    }
+
+    @Test
+    public void testFencedProperties() {
+        PropertiesStringLookupTest.testFence(createStringSubstitutor());
+    }
+
+}


Reply via email to