You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2024/01/21 20:49:49 UTC

(jena) branch main updated: GH-2167: Quoted URI output

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

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new 20dfb6952b GH-2167: Quoted URI output
20dfb6952b is described below

commit 20dfb6952be99d4f30c0f79bdf8f4f3a538f45dd
Author: Andy Seaborne <an...@apache.org>
AuthorDate: Sun Jan 14 10:20:28 2024 +0000

    GH-2167: Quoted URI output
---
 .../org/apache/jena/riot/out/quoted/QuotedURI.java | 312 ++++++++++++++++++++-
 .../test/java/org/apache/jena/riot/out/TS_Out.java |   7 +-
 ...ringOutput.java => TestOutputQuotedString.java} |  38 ++-
 .../apache/jena/riot/out/TestOutputQuotedURI.java  |  84 ++++++
 .../riot/out/quoted/TestQuotedURIInternal.java     | 101 +++++++
 .../java/org/apache/jena/atlas/lib/EscapeStr.java  |  15 +-
 6 files changed, 523 insertions(+), 34 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/riot/out/quoted/QuotedURI.java b/jena-arq/src/main/java/org/apache/jena/riot/out/quoted/QuotedURI.java
index 3d0292e4a7..89eef18626 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/out/quoted/QuotedURI.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/out/quoted/QuotedURI.java
@@ -19,31 +19,325 @@
 package org.apache.jena.riot.out.quoted;
 
 import org.apache.jena.atlas.io.AWriter ;
+import org.apache.jena.atlas.io.IndentedLineBuffer;
+import org.apache.jena.atlas.io.OutputUtils;
 import org.apache.jena.atlas.lib.CharSpace ;
 import org.apache.jena.atlas.lib.Chars ;
 import org.apache.jena.atlas.lib.EscapeStr ;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.riot.RiotException;
+import org.apache.jena.riot.SysRIOT;
 
+/**
+ * Writing URI strings for Turtle etc.
+ * <p>If the URI string contains bad characters (control characters
+ * x00 to x20 and characters <>"{}|^`\) various ways to handle this are provided.
+ * They cause either a different URI to written or an illegal one.
+ * <p>
+ * There is no way to
+ * write illegal characters. Percent-encoding is an encoding, not an escape
+ * mechanism. It put actual 3 characters %-X-X.into the URI.
+ */
 public class QuotedURI {
+
     private final CharSpace charSpace ;
 
     public QuotedURI() {
         this(CharSpace.UTF8) ;
     }
-    
+
+    @FunctionalInterface
+    private interface BadCharWriter { void writeChar(AWriter out, char ch); }
+
+    @FunctionalInterface
+    private interface BadCharHandler { void badChar(int idx, String str, char ch); }
+
     public QuotedURI(CharSpace charSpace) {
-        this.charSpace = charSpace ; 
-    } 
-    
+        this.charSpace = charSpace ;
+    }
+
     /** Write a string for a URI on one line. */
     public void writeURI(AWriter w, String s) {
-        // URIs do not have an escape mechanism. %-is an encoding.
-        // We can either print as-is or convert to %-encoding.
-        // (Ignoring host names which be in puny code).
+        // URIs do not have an escape mechanism. %-is an encoding (it is a change to the URI characters).
+        //
+        // The grammar rule: '<' ([^#x00-#x20<>"{}|^`\] | UCHAR)* '>'
+
+        // The Turtle and SPARQL grammar rules are lax by design. They don't include the
+        // whole grammar of RFC 3986. Instead, they allow a range of characters to have
+        // a one line URI rule then require implementations check for legal URIs.
+
         w.print(Chars.CH_LT);
         if ( CharSpace.isAscii(charSpace) )
-            EscapeStr.writeASCII(w, s) ;
+            EscapeStr.writeASCII(w, s);
         else
-            w.print(s) ;
+            systemWriteUnicodeURI(w, s);
         w.print(Chars.CH_GT);
     }
+
+    /** Write a URI string in UTF-8.
+     * This function is the policy for all Turtle/TriG/N-Tiples/N-Quads writing of URIs.
+     * <p>
+     * URIs strings sometimes contain bad characters.
+     * Control characters x00 to x20 and characters <>"{}|^`\ are mentioned
+     * in the grammars - Turtle et al., and SPARQL but there is also the requirement to be a legal URI
+     * according to <a href="https://www.w3.org/TR/rdf-concepts/#iri-abnf">ABNF for URIs</a>
+     * on top of the basic parsing rule.
+     * <p>
+     * Some control characters mess up the output: e.g. \n, \r, \f, BEL (U+0007) and others.
+     * <p>
+     * There are three choices:
+     * <ol>
+     * <li> Write whatever : Raw characters are emitted. {@link #writeRaw}.</li>
+     * <li> \-u control chars and bad characters. This meets the basic grammar requirements of Turtle/SPARQL
+     *      but not the requirement to be a legal IRI : {@link #writeUnicodeEscapeBadChars}.</li>
+     * <li>Only worry about control characters, including ones that mess with the layout : {@link #writeUnicodeEscapeCtlChars}
+     * </li>
+     * <li>Warn about bad characters. {@link #writeWarnBadChars}</li>
+     * </ol>
+     *
+     * {@link #writeUnicodeEscapeCtlChars} is like {@link #writeUnicodeEscapeBadChars}
+     * except it only unicode-scapes control characters and lets characters, including space, through.
+     */
+    private static void systemWriteUnicodeURI(AWriter w, String s) {
+        // Write as-is
+        //writeDirect(w, s);
+
+        // Write with unicode UCHAR escapes. Legal by the grammar rule but still not a legal URI.
+        // All bad characters: Escape with \-u
+        writeUnicodeEscapeBadChars(w, s);
+        // Write with unicode UCHAR escapes just the control characters as UCHAR.
+        // writeUnicodeEscapeCtlChars(w, s);
+
+        // Percent encode. Makes a change of URI.
+        //writePercentEncodedeBadChars(w, s);
+
+        // warn-and-print and exception-or-write.
+        //writeWarnBadChars(w, s);
+        // writeExceptionOnBadChar(w,s);
+    }
+
+    // ---- Write choices.
+
+    /*package*/ static void writeDirect(AWriter out, String uriStr) {
+        out.print(uriStr);
+    }
+
+    /**
+     * Write, but check for bad characters. If there are any, warn but still output the string.
+     * Bad characters are those in the IRIREF productions:
+     * controls chars, and illegal chars - space and {@code <>"{}|^`\}
+     */
+    /*package*/ static void writeWarnBadChars(AWriter out, String uriStr) {
+        driverOnBadChars(out, uriStr, QuotedURI::warn);
+    }
+
+    /**
+     * Check for bad characters. If there are any, throw an exception with no output.
+     * Bad characters are those in the IRIREF productions:
+     * controls chars, and illegal chars - space and {@code <>"{}|^`\}
+     */
+    /*package*/ static void writeExceptionOnBadChar(AWriter out, String uriStr) {
+        driverOnBadChars(out, uriStr, QuotedURI::error);
+    }
+
+    /**
+     * Write, using Unicode escapes for controls chars, and illegal chars -
+     * space and <>"{}|^`\
+     * This function can write strings that have a different meaning.
+     * %-encoding is not an escape mechanism - there a really are three characters in the URI for %20
+     */
+    /*package*/ static void writeUnicodeEscapeBadChars(AWriter out, String uriStr) {
+        driverWriteBadChars(out, uriStr, QuotedURI::escapeUnicode);
+    }
+
+    /**
+     * Write, using Unicode escapes for controls chars, and illegal chars -
+     * space and <>"{}|^`\
+     * This function can write strings that have a different meaning.
+     * %-encoding is not an escape mechanism - there a really are three characters in the URI for %20
+     */
+    /*package*/ static void writePercentEncodedeBadChars(AWriter out, String uriStr) {
+        driverWriteBadChars(out, uriStr, QuotedURI::encodePercent);
+    }
+
+    /**
+     * Write, using Unicode escapes for controls chars - protects against layout characters.
+     * This function can write unparseable strings.
+     */
+    /*package*/ static void writeUnicodeEscapeCtlChars(AWriter out, String uriStr) {
+        driverWriteControlChars(out, uriStr, QuotedURI::escapeUnicode);
+    }
+
+    /**
+     * Check the string first character-by-character then directly print the whole
+     * string. The {@code BadCharHandler} can throw an exception, which terminates
+     * the function without any output; the exception is propagated.
+     */
+    private static void driverOnBadChars(AWriter out, String uriStr, BadCharHandler handler) {
+        int len = uriStr.length();
+        for (int i = 0; i < len; i++) {
+            char c = uriStr.charAt(i);
+            if ( isControlChar(c) ) {
+                handler.badChar(i, uriStr,  c);
+                continue;
+            } else {
+                // And also <>"{}|^`\
+                switch(c) {
+                    case ' ', '<', '>', '"', '{', '}', '|', '^', '`', '\\':
+                    case '\u007F' : // DEL
+                        handler.badChar(i, uriStr,  c);
+                        continue;
+                    default:
+                }
+            }
+        }
+        // OK - print
+        out.print(uriStr);
+    }
+
+    /**
+     * Process the string character-by-character.
+     * Call a bad character handler on control or bad characters.
+     */
+    private static void driverWriteBadChars(AWriter out, String uriStr, BadCharWriter badCharWriter) {
+        int len = uriStr.length();
+        for (int i = 0; i < len; i++) {
+            char c = uriStr.charAt(i);
+            if ( isControlChar(c) ) {
+                badCharWriter.writeChar(out, c);
+                continue;
+            }
+            switch(c) {
+                case ' ', '<', '>', '"', '{', '}', '|', '^', '`', '\\':
+                case '\u007F' : // DEL
+                    badCharWriter.writeChar(out, c);
+                    break;
+                default:
+                    out.print(c);
+            }
+        }
+    }
+
+    /**
+     * Process the string character-by-character.
+     * Call a bad character handler only on control characters.
+     */
+    private static void driverWriteControlChars(AWriter out, String uriStr, BadCharWriter escaper) {
+        int len = uriStr.length();
+        for (int i = 0; i < len; i++) {
+            char c = uriStr.charAt(i);
+            if ( isControlChar(c) ) {
+                escaper.writeChar(out, c);
+                continue;
+            }
+        }
+    }
+
+    private static void escapeUnicode(AWriter out, char c) {
+        out.print("\\u");
+        OutputUtils.printHex(out, c, 4);
+    }
+
+    private static void encodePercent(AWriter out, char c) {
+        if ( c <= 0xFF ) {
+            // One byte
+            out.print("%");
+            OutputUtils.printHex(out, c, 2);
+            return;
+        }
+        if ( c > 0xFF && c <= 0xFFFF) {
+            // 2 byte, Hi-Lo
+            int x = c;
+            out.print("%");
+            OutputUtils.printHex(out, x>>8, 2);
+            out.print("%");
+            OutputUtils.printHex(out, x|0xFF, 2);
+        }
+        throw new RiotException("Very bad character! "+Long.toHexString(c));
+    }
+
+    private static void warn(int idx, String str, char ch) {
+        String msg = formattedMessage(idx, str, ch);
+        FmtLog.warn(SysRIOT.getLogger(), msg);
+    }
+
+    private static void error(int idx, String str, char ch) {
+        String msg = formattedMessage(idx, str, ch);
+        throw new RiotException(msg);
+    }
+
+    private static String formattedMessage(int idx, String str, char ch) {
+        String chStr = Character.toString(ch);
+        if ( ch == ' ' )
+            chStr = " ";
+        if ( ch == '\t' )
+            chStr = "\\t";
+        if ( ch == '\n' )
+            chStr = "\\n";
+        str = displayUnicodeEscapeControlChars(str);
+        return String.format("Bad character in URI <%s> at position %d: '%s' codepoint U+%04X", str, idx, chStr, (int)ch);
+    }
+
+    // ---- Display helpers. Not for output.
+
+    /** Display form, indicating escape characters */
+    private static String displayUnicodeEscapeControlChars(String uriStr) {
+        IndentedLineBuffer out = new IndentedLineBuffer();
+        writeDisplayUnicodeControlChars(out, uriStr);
+        return out.asString();
+    }
+
+    /**
+     * Write, using Unicode codepoints for controls chars, and illegal chars -
+     * space and <>"{}|^`\
+     * This function creates displayable strings.
+     */
+    private static void writeDisplayUnicodeControlChars(AWriter out, String uriStr) {
+        int len = uriStr.length();
+        for (int i = 0; i < len; i++) {
+            char c = uriStr.charAt(i);
+            // Well-known characters
+            String s = displayUnicode(c);
+            if ( s != null ) {
+                out.print(s);
+                continue;
+            }
+            // Other control chars
+            if ( isControlChar(c) ) {
+                displayUnicodeEscape(out, c);
+                continue;
+            }
+            out.print(c);
+        }
+    }
+
+    // ---- Helper functions.
+
+    /**
+     * Control chars mentioned in the specs.
+     */
+    private static boolean isControlChar(char c) {
+        return c < 20 ;
+        // Unicode has a another control char block at 007F to 09FF
+        // In the range [U+0000, U+001F], or range [U+007F, U+009F]
+        //return Character.isISOControl(c);
+    }
+
+    private static String displayUnicode(char c) {
+        return switch (c) {
+            case '\n' -> "[NL]";
+            case '\r' -> "[CR]";
+            case '\f' -> "[FF]";
+            case ' '  -> "[space]";
+            case '\t' -> "[tab]";
+            // Other bad chars? <>"{}|^`\
+            default -> null;
+        };
+    }
+
+    private static void displayUnicodeEscape(AWriter out, char c) {
+        out.print("[U+");
+        OutputUtils.printHex(out, c, 4);
+        out.print("]");
+    }
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/riot/out/TS_Out.java b/jena-arq/src/test/java/org/apache/jena/riot/out/TS_Out.java
index f13735c4ab..447e87d0da 100644
--- a/jena-arq/src/test/java/org/apache/jena/riot/out/TS_Out.java
+++ b/jena-arq/src/test/java/org/apache/jena/riot/out/TS_Out.java
@@ -18,17 +18,18 @@
 
 package org.apache.jena.riot.out;
 
+import org.apache.jena.riot.out.quoted.TestQuotedURIInternal;
 import org.junit.runner.RunWith ;
 import org.junit.runners.Suite ;
 
-
 @RunWith(Suite.class)
 @Suite.SuiteClasses( {
-    TestQuotedStringOutput.class
+    TestOutputQuotedString.class
+    , TestOutputQuotedURI.class
+    , TestQuotedURIInternal.class
     , TestNodeFmt.class
     , TestNodeFmtLib.class
 })
 
 public class TS_Out
 {}
-
diff --git a/jena-arq/src/test/java/org/apache/jena/riot/out/TestQuotedStringOutput.java b/jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedString.java
similarity index 94%
rename from jena-arq/src/test/java/org/apache/jena/riot/out/TestQuotedStringOutput.java
rename to jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedString.java
index 6336dddb9f..0267976a4d 100644
--- a/jena-arq/src/test/java/org/apache/jena/riot/out/TestQuotedStringOutput.java
+++ b/jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedString.java
@@ -21,13 +21,13 @@ package org.apache.jena.riot.out;
 import static org.junit.Assert.assertEquals ;
 
 import org.apache.jena.atlas.io.StringWriterI ;
-import org.apache.jena.riot.out.quoted.QuotedStringOutput ;
-import org.apache.jena.riot.out.quoted.QuotedStringOutputNT ;
-import org.apache.jena.riot.out.quoted.QuotedStringOutputTTL ;
-import org.apache.jena.riot.out.quoted.QuotedStringOutputTTL_MultiLine ;
+import org.apache.jena.riot.out.quoted.QuotedStringOutput;
+import org.apache.jena.riot.out.quoted.QuotedStringOutputNT;
+import org.apache.jena.riot.out.quoted.QuotedStringOutputTTL;
+import org.apache.jena.riot.out.quoted.QuotedStringOutputTTL_MultiLine;
 import org.junit.Test ;
 
-public class TestQuotedStringOutput {
+public class TestOutputQuotedString {
 
     static void testSingleLine(QuotedStringOutput proc, String input, String expected) {
         StringWriterI w = new StringWriterI() ;
@@ -36,62 +36,60 @@ public class TestQuotedStringOutput {
         expected = proc.getQuoteChar()+expected+proc.getQuoteChar() ;
         assertEquals(expected, output) ;
     }
-    
+
     static void testMultiLine(QuotedStringOutput proc, String input, String expected) {
         StringWriterI w = new StringWriterI() ;
         proc.writeStrMultiLine(w, input);
         String output = w.toString() ;
         assertEquals(expected, output) ;
     }
-    
+
     private QuotedStringOutput escProcNT = new QuotedStringOutputNT() ;
     private QuotedStringOutput escProcTTL_S2 = new QuotedStringOutputTTL('"') ;
     private QuotedStringOutput escProcTTL_S1 = new QuotedStringOutputTTL('\'') ;
-    
+
     private QuotedStringOutput escProcTTL_M2 = new QuotedStringOutputTTL_MultiLine('"') ;
     private QuotedStringOutput escProcTTL_M1 = new QuotedStringOutputTTL_MultiLine('\'') ;
-    
+
     @Test public void escape_nt_00() { testSingleLine(escProcNT, "", "") ; }
     @Test public void escape_nt_01() { testSingleLine(escProcNT, "abc", "abc") ; }
     @Test public void escape_nt_02() { testSingleLine(escProcNT, "abc\ndef", "abc\\ndef") ; }
-    
+
     @Test public void escape_nt_03() { testSingleLine(escProcNT, "\"", "\\\"") ; }
     @Test public void escape_nt_04() { testSingleLine(escProcNT, "'", "'") ; }
     @Test public void escape_nt_05() { testSingleLine(escProcNT, "xyz\t", "xyz\\t") ; }
-    
+
     @Test public void escape_ttl_singleline_quote2_00() { testSingleLine(escProcTTL_S2, "", "") ; }
     @Test public void escape_ttl_singleline_quote2_01() { testSingleLine(escProcTTL_S2, "abc", "abc") ; }
     @Test public void escape_ttl_singleline_quote2_02() { testSingleLine(escProcTTL_S2, "abc\ndef", "abc\\ndef") ; }
-    
+
     @Test public void escape_ttl_singleline_quote2_03() { testSingleLine(escProcTTL_S2, "\"", "\\\"") ; }
     @Test public void escape_ttl_singleline_quote2_04() { testSingleLine(escProcTTL_S2, "'", "'") ; }
     @Test public void escape_ttl_singleline_quote2_05() { testSingleLine(escProcTTL_S2, "xyz\t", "xyz\\t") ; }
-    
+
     @Test public void escape_ttl_singleline_quote1_00() { testSingleLine(escProcTTL_S1, "", "") ; }
     @Test public void escape_ttl_singleline_quote1_01() { testSingleLine(escProcTTL_S1, "abc", "abc") ; }
     @Test public void escape_ttl_singleline_quote1_02() { testSingleLine(escProcTTL_S1, "abc\ndef", "abc\\ndef") ; }
-    
+
     @Test public void escape_ttl_singleline_quote1_03() { testSingleLine(escProcTTL_S1, "\"", "\"") ; }
     @Test public void escape_ttl_singleline_quote1_04() { testSingleLine(escProcTTL_S1, "'", "\\'") ; }
     @Test public void escape_ttl_singleline_quote1_05() { testSingleLine(escProcTTL_S1, "xyz\t", "xyz\\t") ; }
-    
+
     // Multiple with single line output processor.
     @Test public void escape_ttl_singleline_quote2_multiline_str0() { testMultiLine(escProcTTL_S2, "", "\"\"") ; }
     @Test public void escape_ttl_singleline_quote2_multiline_str1() { testMultiLine(escProcTTL_S2, "abc", "\"abc\"") ; }
     @Test public void escape_ttl_singleline_quote2_multiline_str2() { testMultiLine(escProcTTL_S2, "abc\ndef", "\"abc\\ndef\"") ; }
-    
+
     @Test public void escape_ttl_singleline_quote2_multiline_str3() { testMultiLine(escProcTTL_S2, "\"", "\"\\\"\"") ; }
     @Test public void escape_ttl_singleline_quote2_multiline_str4() { testMultiLine(escProcTTL_S2, "'", "\"'\"") ; }
     @Test public void escape_ttl_singleline_quote2_multiline_str5() { testMultiLine(escProcTTL_S2, "xyz\t", "\"xyz\\t\"") ; }
-    
+
     // Multiple with single line output processor.
     @Test public void escape_ttl_singleline_quote1_multiline_str0() { testMultiLine(escProcTTL_S1, "", "''") ; }
     @Test public void escape_ttl_singleline_quote1_multiline_str1() { testMultiLine(escProcTTL_S1, "abc", "'abc'") ; }
     @Test public void escape_ttl_singleline_quote1_multiline_str2() { testMultiLine(escProcTTL_S1, "abc\ndef", "'abc\\ndef'") ; }
-    
+
     @Test public void escape_ttl_singleline_quote1_multiline_str3() { testMultiLine(escProcTTL_S1, "\"", "'\"'") ; }
     @Test public void escape_ttl_singleline_quote1_multiline_str4() { testMultiLine(escProcTTL_S1, "'", "'\\''") ; }
     @Test public void escape_ttl_singleline_quote1_multiline_str5() { testMultiLine(escProcTTL_S1, "xyz\t", "'xyz\\t'") ; }
-
-    // Multiline, multiline
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedURI.java b/jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedURI.java
new file mode 100644
index 0000000000..3adc0cd1a5
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/riot/out/TestOutputQuotedURI.java
@@ -0,0 +1,84 @@
+/*
+ * 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.jena.riot.out;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.jena.atlas.io.IndentedLineBuffer;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.riot.SysRIOT;
+import org.apache.jena.riot.out.quoted.QuotedURI;
+import org.apache.jena.riot.out.quoted.TestQuotedURIInternal;
+import org.junit.Test;
+
+/**
+ * Tests for URI output.
+ * See also {@link TestQuotedURIInternal} for other tests that call the implementation options.
+ */
+public class TestOutputQuotedURI {
+
+    @Test public void quoted_uri_api_1() {
+        testQuotedURI("http://example/", "<http://example/>", true);
+    }
+
+    @Test public void quoted_uri_api_2() {
+        testQuotedURI("http://example/αβγ", "<http://example/αβγ>", true);
+    }
+
+    @Test public void quoted_uri_api_3() {
+        testQuotedURI("http://example/١٢٣", "<http://example/١٢٣>", true);
+    }
+
+    @Test public void quoted_uri_api_encoding_1() {
+        // If percent encoding. Space in, something out.
+        //testQuotedURI("http://example/abc def", "<http://example/abc%20def>", false);
+        // If Unicode UCHAR escaping. Note double \\ - use Java escape to get a single \ into the string.
+        testQuotedURI("http://example/abc def", "<http://example/abc\\u0020def>", false);
+    }
+
+    @Test public void quoted_uri_api_encoding_2() {
+        // Test encoding setup. Raw tab in, something for the tab out. Tab is a control character
+        // If percent encoding.
+        //testQuotedURI("http://example/abc\u0009def", "<http://example/abc%09def>", false);
+        // If Unicode UCHAR escaping.
+        testQuotedURI("http://example/abc\u0009def", "<http://example/abc\\u0009def>", false);
+    }
+
+    @Test public void quoted_uri_api_encoding_3() {
+        // Test encoding setup. Illegal {} in the URI string
+        // If percent encoding.
+        //testQuotedURI("http://example/_{_{}_", "<http://example/_%7B_7D_>", false);
+        // If Unicode UCHAR escaping.
+        testQuotedURI("http://example/_{_}_", "<http://example/_\\u007B_\\u007D_>", false);
+    }
+
+    private static void testQuotedURI(String input, String expected, boolean withWarnings) {
+        Runnable r = ()->{
+            QuotedURI quoter = new QuotedURI();
+            IndentedLineBuffer x = new IndentedLineBuffer();
+            quoter.writeURI(x, input);
+            String s = x.asString();
+            assertEquals(expected, s);
+        };
+        if ( withWarnings )
+            r.run();
+        else
+            LogCtl.withLevel(SysRIOT.getLogger(), "ERROR", r);
+    }
+}
diff --git a/jena-arq/src/test/java/org/apache/jena/riot/out/quoted/TestQuotedURIInternal.java b/jena-arq/src/test/java/org/apache/jena/riot/out/quoted/TestQuotedURIInternal.java
new file mode 100644
index 0000000000..41f5593c70
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/riot/out/quoted/TestQuotedURIInternal.java
@@ -0,0 +1,101 @@
+/*
+ * 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.jena.riot.out.quoted;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.function.BiConsumer;
+
+import org.apache.jena.atlas.io.AWriter;
+import org.apache.jena.atlas.io.IndentedLineBuffer;
+import org.apache.jena.riot.RiotException;
+import org.apache.jena.riot.out.TestOutputQuotedURI;
+import org.junit.Test;
+
+/**
+ * Tests for the different options in QuoteURI.
+ * {@link  TestOutputQuotedURI} tests the API.
+ */
+public class TestQuotedURIInternal {
+    // Must be in the same package as QuotedURI.
+
+    // Normal
+    @Test
+    public void write_uri_11() {
+        testQuotedURI("http://example/", "http://example/", QuotedURI::writeDirect);
+    }
+
+    @Test
+    public void write_uri_12() {
+        testQuotedURI("http://example/", "http://example/", QuotedURI::writeUnicodeEscapeBadChars);
+    }
+
+    @Test
+    public void write_uri_13() {
+        testQuotedURI("http://example/", "http://example/", QuotedURI::writeExceptionOnBadChar);
+    }
+
+    @Test
+    public void write_uri_21() {
+        testQuotedURI("http://example/αβγ", "http://example/αβγ", QuotedURI::writeDirect);
+    }
+
+    @Test
+    public void write_uri_22() {
+        testQuotedURI("http://example/αβγ", "http://example/αβγ", QuotedURI::writeUnicodeEscapeBadChars);
+    }
+
+    @Test
+    public void write_uri_23() {
+        testQuotedURI("http://example/αβγ", "http://example/αβγ", QuotedURI::writeExceptionOnBadChar);
+    }
+
+    // Error cases
+    @Test
+    public void write_uri_51() {
+        testQuotedURI("http://example/abc def", "http://example/abc def", QuotedURI::writeDirect);
+    }
+
+    @Test
+    public void write_uri_52() {
+        testQuotedURI("http://example/abc def", "http://example/abc\\u0020def", QuotedURI::writeUnicodeEscapeBadChars);
+    }
+
+    @Test
+    public void write_uri_61() {
+        testQuotedURI("http://example/abc{}def", "http://example/abc\\u007B\\u007Ddef", QuotedURI::writeUnicodeEscapeBadChars);
+    }
+
+    @Test
+    public void write_uri_62() {
+        testQuotedURI("http://example/abc{}def", "http://example/abc%7B%7Ddef", QuotedURI::writePercentEncodedeBadChars);
+    }
+
+    @Test(expected=RiotException.class)
+    public void write_uri_71() {
+        testQuotedURI("http://example/abc def", "http://example/abc", QuotedURI::writeExceptionOnBadChar);
+    }
+
+    private static void testQuotedURI(String input, String expected, BiConsumer<AWriter, String> quoteOperation) {
+        IndentedLineBuffer x = new IndentedLineBuffer();
+        quoteOperation.accept(x, input);
+        String s = x.asString();
+        assertEquals(expected, s);
+    }
+}
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/EscapeStr.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/EscapeStr.java
index f67b4d861f..e40452b816 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/lib/EscapeStr.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/EscapeStr.java
@@ -27,11 +27,22 @@ import org.apache.jena.atlas.io.StringWriterI ;
 public class EscapeStr
 {
     /*
-     * Escape characters in a string according to Turtle rules.
+     * Escape characters in a string according to Turtle rules
+     * for a single line string, using double quotes as the string delimiters
+     * Delimiters are not included in the result.
      */
     public static String stringEsc(String s) {
+        return stringEsc(s, Chars.CH_QUOTE2);
+    }
+
+    /*
+     * Escape characters in a string according to Turtle rules,
+     * where {@code quoteChar} is the delimiter.
+     * Delimiters are not included in the result.
+     */
+    public static String stringEsc(String s, char quoteChar) {
         AWriter w = new StringWriterI() ;
-        stringEsc(w, s, Chars.CH_QUOTE2, true, CharSpace.UTF8) ;
+        stringEsc(w, s, quoteChar, true, CharSpace.UTF8) ;
         return w.toString() ;
     }