You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by di...@apache.org on 2022/10/25 14:15:18 UTC

[sling-org-apache-sling-scripting-sightly] branch master updated: SLING-10654: add support for ICU MessageFormat (#12)

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

diru pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-scripting-sightly.git


The following commit(s) were added to refs/heads/master by this push:
     new 6cc7704  SLING-10654: add support for ICU MessageFormat  (#12)
6cc7704 is described below

commit 6cc7704f0a2c361708a052df082629cb301e6d9a
Author: Dirk Rudolph <di...@apache.org>
AuthorDate: Tue Oct 25 16:15:11 2022 +0200

    SLING-10654: add support for ICU MessageFormat  (#12)
    
    This adds an optional dependency to icu4j. If they do not exist in the runtime the logic falls back to the existing behaviour.
---
 bnd.bnd                                            |  2 +
 pom.xml                                            |  8 +++
 .../engine/extension/FormatFilterExtension.java    | 62 ++++++++++++-------
 .../extension/FormatFilterExtensionTest.java       | 70 ++++++++++++++++++++++
 4 files changed, 122 insertions(+), 20 deletions(-)

diff --git a/bnd.bnd b/bnd.bnd
index 768c55b..c4c672d 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -12,4 +12,6 @@ Import-Package:     org.apache.sling.scripting.sightly.compiler.*;resolution:=op
                     org.apache.sling.commons.compiler.*;resolution:=optional, \\
                     org.apache.sling.commons.classloader.*;resolution:=optional, \\
                     org.apache.sling.models.*;resolution:=optional, \\
+                    org.apache.sling.models.*;resolution:=optional, \\
+                    com.ibm.icu.*;resolution:=optional, \\
                     *
diff --git a/pom.xml b/pom.xml
index 68b3366..e7b1fb4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -304,6 +304,14 @@
             <scope>provided</scope>
         </dependency>
 
+        <!-- icu -->
+        <dependency>
+            <groupId>com.ibm.icu</groupId>
+            <artifactId>icu4j</artifactId>
+            <version>69.1</version>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.jetbrains</groupId>
             <artifactId>annotations</artifactId>
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtension.java b/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtension.java
index ff438de..c7bd69c 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtension.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtension.java
@@ -41,6 +41,8 @@ import org.osgi.service.component.annotations.Component;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.ibm.icu.text.MessageFormat;
+
 @Component(
         service = RuntimeExtension.class,
         property = {
@@ -60,6 +62,9 @@ public class FormatFilterExtension implements RuntimeExtension {
 
     private static final Logger LOG = LoggerFactory.getLogger(FormatFilterExtension.class);
     private static final Pattern PLACEHOLDER_REGEX = Pattern.compile("\\{\\d+}");
+    private static final Pattern COMPLEX_PLACEHOLDER_REGEX = Pattern.compile("\\{\\d+,[^}]+}");
+
+    protected boolean hasIcuSupport = true;
 
     @Override
     public Object call(final RenderContext renderContext, Object... arguments) {
@@ -70,18 +75,17 @@ public class FormatFilterExtension implements RuntimeExtension {
 
         String formattingType = runtimeObjectModel.toString(options.get(TYPE_OPTION));
         Object formatObject = options.get(FORMAT_OPTION);
-        boolean hasPlaceHolders = PLACEHOLDER_REGEX.matcher(source).find();
+        boolean hasPlaceHolders = PLACEHOLDER_REGEX.matcher(source).find() || COMPLEX_PLACEHOLDER_REGEX.matcher(source).find();
         if (STRING_FORMAT_TYPE.equals(formattingType)) {
-            return getFormattedString(runtimeObjectModel, source, formatObject);
+            return getFormattedString(runtimeObjectModel, source, options, formatObject);
         } else if (DATE_FORMAT_TYPE.equals(formattingType) || (!hasPlaceHolders && runtimeObjectModel.isDate(formatObject))) {
             return getDateFormattedString(runtimeObjectModel, source, options, formatObject);
         } else if (NUMBER_FORMAT_TYPE.equals(formattingType) || (!hasPlaceHolders && runtimeObjectModel.isNumber(formatObject))) {
             return getNumberFormattedString(runtimeObjectModel, source, options, formatObject);
         }
         if (hasPlaceHolders) {
-            return getFormattedString(runtimeObjectModel, source, formatObject);
+            return getFormattedString(runtimeObjectModel, source, options, formatObject);
         }
-
         try {
             // try to parse as DateTimeFormatter
             DateTimeFormatter.ofPattern(source);
@@ -96,12 +100,22 @@ public class FormatFilterExtension implements RuntimeExtension {
         } catch (IllegalArgumentException e) {
             // ignore
         }
-        return getFormattedString(runtimeObjectModel, source, formatObject);
+        return getFormattedString(runtimeObjectModel, source, options, formatObject);
     }
 
-    private Object getFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Object formatObject) {
+    private Object getFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Map<String, Object> options,
+                                      Object formatObject) {
         Object[] params = decodeParams(runtimeObjectModel, formatObject);
-        return formatString(runtimeObjectModel, source, params);
+        if (COMPLEX_PLACEHOLDER_REGEX.matcher(source).find()) {
+            if (hasIcuSupport) {
+                Locale locale = getLocale(runtimeObjectModel, options);
+                return formatStringIcu(source, locale, params);
+            } else {
+                return null;
+            }
+        } else {
+            return formatString(runtimeObjectModel, source, params);
+        }
     }
 
     private String getNumberFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Map<String, Object> options,
@@ -157,25 +171,33 @@ public class FormatFilterExtension implements RuntimeExtension {
         Matcher matcher = PLACEHOLDER_REGEX.matcher(source);
         StringBuilder builder = new StringBuilder();
         int lastPos = 0;
-        boolean matched = true;
-        while (matched) {
-            matched = matcher.find();
-            if (matched) {
-                String group = matcher.group();
-                int paramIndex = Integer.parseInt(group.substring(1, group.length() - 1));
-                String replacement = toString(runtimeObjectModel, params, paramIndex);
-                int matchStart = matcher.start();
-                int matchEnd = matcher.end();
-                builder.append(source, lastPos, matchStart).append(replacement);
-                lastPos = matchEnd;
-            }
+        while (matcher.find()) {
+            String group = matcher.group();
+            int paramIndex = Integer.parseInt(group.substring(1, group.length() - 1));
+            String replacement = toString(runtimeObjectModel, params, paramIndex);
+            int matchStart = matcher.start();
+            int matchEnd = matcher.end();
+            builder.append(source, lastPos, matchStart).append(replacement);
+            lastPos = matchEnd;
         }
         builder.append(source, lastPos, source.length());
         return builder.toString();
     }
 
+    private String formatStringIcu(String source, Locale locale, Object[] params) {
+        try {
+            MessageFormat messageFormat = locale != null ? new MessageFormat(source, locale) : new MessageFormat(source);
+            return messageFormat.format(params);
+        } catch (NoClassDefFoundError ex) {
+            LOG.trace("ICU4J not found", ex);
+            hasIcuSupport = false;
+            return null;
+        }
+    }
+
     private String toString(RuntimeObjectModel runtimeObjectModel, Object[] params, int index) {
-        if (index >= 0 && index < params.length) {
+        // index can only be a signed integer according to FormatFilterExtension#PLACEHOLDER_REGEX
+        if (index < params.length) {
             return runtimeObjectModel.toString(params[index]);
         }
         return "";
diff --git a/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtensionTest.java b/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtensionTest.java
index 9c33bc6..82c3a19 100644
--- a/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtensionTest.java
+++ b/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/FormatFilterExtensionTest.java
@@ -20,6 +20,7 @@ package org.apache.sling.scripting.sightly.impl.engine.extension;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -61,6 +62,23 @@ public class FormatFilterExtensionTest {
         .atZone(ZoneId.of("UTC"))
         .toInstant());
 
+    @Test
+    public void testNoop() {
+        // constructed case, it is actually difficult to find a pattern that is not a date-time or decimal number format
+        assertEquals("0-#",
+            subject.call(renderContext, "0-#", Collections.singletonMap(FormatFilterExtension.FORMAT, "ignored")));
+    }
+
+    @Test
+    public void testNoopNoParameters() {
+        assertNull("0-#", subject.call(renderContext, "0-#", Collections.emptyMap()));
+    }
+
+    @Test(expected = SightlyException.class)
+    public void testMissingOptions() {
+        subject.call(renderContext, "fails");
+    }
+
     @Test
     public void testDateFormatNull() {
         assertNull(subject.call(renderContext, "default", new HashMap<String, Object>() {{
@@ -187,4 +205,56 @@ public class FormatFilterExtensionTest {
         }
         assertEquals(expected, subject.call(renderContext, format, options));
     }
+
+    @Test
+    public void testSimpleStringFormat() {
+        Object result = subject.call(renderContext,
+            "This {0} a {1} format", Collections.singletonMap("format", Arrays.asList("is", "simple")));
+        assertEquals("This is a simple format", result);
+    }
+
+    @Test
+    public void testStringFormat() {
+        Object result = subject.call(renderContext,
+            "This {0} a {1} format", new HashMap<String, Object>() {{
+                put(FormatFilterExtension.FORMAT, Arrays.asList("is", "simple"));
+                put(FormatFilterExtension.TYPE_OPTION, FormatFilterExtension.STRING_FORMAT_TYPE);
+            }});
+        assertEquals("This is a simple format", result);
+    }
+
+    @Test
+    public void testSimpleStringFormatWithSingleParameter() {
+        Object result = subject.call(renderContext,
+            "Hello {0}", Collections.singletonMap(FormatFilterExtension.FORMAT, "world"));
+        assertEquals("Hello world", result);
+    }
+
+    @Test
+    public void testComplexStringFormatNoSimplePlaceholderWithLocale() {
+        Object result = subject.call(renderContext,
+            "This query has {0,plural,zero {# results} one {# result} other {# results}}",
+            new HashMap<String, Object>() {{
+                put(FormatFilterExtension.FORMAT, Collections.singletonList(7));
+                put(FormatFilterExtension.LOCALE_OPTION, "en_US");
+            }});
+        assertEquals("This query has 7 results", result);
+    }
+
+    @Test
+    public void testComplexStringFormatWithSimplePlaceholderNoLocale() {
+        Object result = subject.call(renderContext,
+            "This {0} has {1,plural,zero {# results} one {# result} other {# results}}",
+            Collections.singletonMap(FormatFilterExtension.FORMAT, Arrays.asList("query", 7)));
+        assertEquals("This query has 7 results", result);
+    }
+
+    @Test
+    public void testComplexStringFormatWithSimplePlaceholderNoIcuSupport() {
+        subject.hasIcuSupport = false;
+        Object result = subject.call(renderContext,
+            "This {0} has {1,plural,zero {{1} results} one {{1} result} other {{1} results}}",
+            Collections.singletonMap(FormatFilterExtension.FORMAT, Arrays.asList("query", 7)));
+        assertNull(result);
+    }
 }