This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git
The following commit(s) were added to refs/heads/master by this push:
new 5767e54 SLING-12815 Expose methods to escape/unescape resource names
(#60)
5767e54 is described below
commit 5767e54f23e043bce2b2d3785b467b053d06dda4
Author: Konrad Windszus <[email protected]>
AuthorDate: Tue Jun 17 14:15:38 2025 +0200
SLING-12815 Expose methods to escape/unescape resource names (#60)
This takes care of escaping characters not allowed by Sling API in
resource names, namely "/" and names only consisting of ".".
Highlight in javadocs where the escape methods may be useful.
---
.../sling/api/resource/ResourceResolver.java | 5 +-
.../apache/sling/api/resource/ResourceUtil.java | 66 ++++++++++++++++++++++
.../sling/api/resource/SyntheticResource.java | 3 +-
.../sling/api/resource/ResourceUtilTest.java | 16 ++++++
4 files changed, 87 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
index 5f26517..a3197cc 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
@@ -780,17 +780,18 @@ public interface ResourceResolver extends Adaptable,
Closeable {
* The optional resource super type is determined by the property {@value
ResourceResolver#PROPERTY_RESOURCE_SUPER_TYPE}.
*
* @param parent The parent resource
- * @param name The name of the child resource - this is a plain name,
not a path!
+ * @param name The name of the child resource - this is a plain name,
not a path! The name must neither contain a slash nor consist out of dots only.
The underlying resource provider may impose further restrictions on the name.
* @param properties Optional properties for the resource (may be
<code>null</code>).
* @return The new resource
*
* @throws NullPointerException if the resource parameter or name
parameter is null
- * @throws IllegalArgumentException if the name contains a slash
+ * @throws IllegalArgumentException if the name contains a slash or
consists out of dots only.
* @throws UnsupportedOperationException If the underlying resource
provider does not support write operations.
* @throws PersistenceException If the operation fails in the underlying
resource provider, e.g. in case a resource of that name does already exist.
* @throws IllegalStateException if this resource resolver has already been
* {@link #close() closed}.
* @since 2.2 (Sling API Bundle 2.2.0)
+ * @see ResourceUtil#escapeName(String)
*/
@NotNull
Resource create(@NotNull Resource parent, @NotNull String name,
Map<String, Object> properties)
diff --git a/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
b/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
index 9c2f816..e3355cf 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
@@ -19,12 +19,17 @@
package org.apache.sling.api.resource;
import java.text.MessageFormat;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
@@ -39,6 +44,7 @@ import org.jetbrains.annotations.Nullable;
*/
public class ResourceUtil {
+ private static final Pattern UNICODE_ESCAPE_SEQUENCE_PATTERN =
Pattern.compile("\\\\u[0-9a-fA-F]{4}");
/**
* Resolves relative path segments '.' and '..' in the path.
* The path can either be relative or absolute. Relative paths are treated
@@ -291,6 +297,66 @@ public class ResourceUtil {
return normalizedPath.substring(normalizedPath.lastIndexOf('/') + 1);
}
+ /**
+ * Escapes the given <code>name</code> for use in a resource name. It
escapes all invalid characters according to Sling API, i.e.
+ * it escapes the slash and names only consisting of dots. It uses Java
UTF-16 unicode escape sequences for those characters.
+ * @param name
+ * @return the escaped name
+ * @see ResourceResolver#create(Resource, String, Map)
+ * @see SyntheticResource#SyntheticResource(ResourceResolver, String,
String)
+ * @see <a href="https://www.rfc-editor.org/rfc/rfc5137#section-6.3">RFC
5137, section 6.3</a>
+ * @since 2.14.0 (Sling API Bundle 3.0.0)
+ */
+ public static @NotNull String escapeName(@NotNull String name) {
+ if (name.chars().allMatch(c -> c == '.')) {
+ return escapeWithUnicode(name, '.');
+ }
+ return escapeWithUnicode(name, '/');
+ }
+
+ /**
+ * Unescapes the given <code>escapedName</code> previously escaped using
{@link #escapeName(String)}.
+ * It replaces the unicode escape sequences with the original characters.
+ *
+ * @param escapedName The escaped name to unescape.
+ * @return The unescaped name.
+ * @see Resource#getName()
+ * @see <a href="https://www.rfc-editor.org/rfc/rfc5137#section-6.3">RFC
5137, section 6.3</a>
+ * @since 2.14.0 (Sling API Bundle 3.0.0)
+ */
+ public static @NotNull String unescapeName(@NotNull String escapedName) {
+ return unescapeWithUnicode(escapedName);
+ }
+
+ private static String escapeWithUnicode(String text, Character...
additionalCharactersToEscape) {
+ List<Character> charactersToEscape = new LinkedList<>();
+ charactersToEscape.add('\\'); // always escape the backslash as it
used for unicode escaping itself
+ charactersToEscape.addAll(Arrays.asList(additionalCharactersToEscape));
+ for (Character characterToEscape : charactersToEscape) {
+ String escapedChar = getUnicodeEscapeSequence(characterToEscape);
+ text = text.replace(characterToEscape.toString(), escapedChar);
+ }
+ return text;
+ }
+
+ private static String getUnicodeEscapeSequence(char c) {
+ return String.format("\\u%04X", (int) c);
+ }
+
+ private static String unescapeWithUnicode(String escapedText) {
+ Matcher matcher = UNICODE_ESCAPE_SEQUENCE_PATTERN.matcher(escapedText);
+
+ StringBuilder decodedString = new StringBuilder();
+
+ while (matcher.find()) {
+ String unicodeSequence = matcher.group();
+ char unicodeChar = (char)
Integer.parseInt(unicodeSequence.substring(2), 16);
+ matcher.appendReplacement(decodedString,
Character.toString(unicodeChar));
+ }
+ matcher.appendTail(decodedString);
+ return decodedString.toString();
+ }
+
/**
* Returns <code>true</code> if the resource <code>res</code> is a
synthetic
* resource.
diff --git a/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
b/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
index 4fd047b..86f17c0 100644
--- a/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
+++ b/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
@@ -48,8 +48,9 @@ public class SyntheticResource extends AbstractResource {
* Creates a synthetic resource with the given <code>path</code> and
* <code>resourceType</code>.
* @param resourceResolver The resource resolver
- * @param path The resource path
+ * @param path The absolute resource path including the name. Make sure
that each segment of the path only contains valid characters in Sling API
resource names.
* @param resourceType The type of the resource
+ * @see ResourceUtil#escapeName(String)
*/
public SyntheticResource(
@NotNull ResourceResolver resourceResolver, @NotNull String path,
@NotNull String resourceType) {
diff --git a/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
b/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
index 8826020..b957bd0 100644
--- a/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
+++ b/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
@@ -433,4 +433,20 @@ public class ResourceUtilTest {
public void testFindResourceSuperType() {
assertNull(ResourceUtil.findResourceSuperType(null));
}
+
+ @Test
+ public void testEscapeAndUnescapeNameWithSlash() {
+ String nameWithSpecialChars = "this/is/..//u1234test";
+ String escapedName = ResourceUtil.escapeName(nameWithSpecialChars);
+ assertEquals(nameWithSpecialChars,
ResourceUtil.unescapeName(escapedName));
+ assertFalse(escapedName.contains("/"));
+ }
+
+ @Test
+ public void testEscapeAndUnescapeNameWithDots() {
+ String nameWithSpecialChars = "....";
+ String escapedName = ResourceUtil.escapeName(nameWithSpecialChars);
+ assertEquals(nameWithSpecialChars,
ResourceUtil.unescapeName(escapedName));
+ assertFalse(escapedName.contains("."));
+ }
}