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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1b888b03f2 TIKA-4921: locale followups (#3244)
1b888b03f2 is described below

commit 1b888b03f2485540bd4620c2d211edbf0351736b
Author: Tim Allison <[email protected]>
AuthorDate: Thu Sep 24 13:37:52 2026 -0400

    TIKA-4921: locale followups (#3244)
---
 .../tika/parser/ocr/TesseractOCRParserTest.java    |   9 +-
 .../org/apache/tika/parser/ogg/OggAudioParser.java | 170 ++++++++++++++++-----
 .../org/apache/tika/parser/ogg/FlacParserTest.java |   3 -
 .../tika/parser/ogg/VorbisCommentsLocaleTest.java  |  76 +++++++++
 .../apache/tika/parser/ocr/ImagePreprocessor.java  |   2 +-
 .../tika/pipes/ignite/IgniteConfigStore.java       |  20 ++-
 .../pipes/ignite/server/IgniteStoreServer.java     |  12 +-
 .../tika/pipes/ignite/IgniteConfigStoreTest.java   |  25 ++-
 8 files changed, 262 insertions(+), 55 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
index 4685dacb14..9434b43971 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
@@ -222,8 +222,6 @@ public class TesseractOCRParserTest extends TikaTest {
     }
 
 
-    // TODO TIKA-4923: metadata-extractor lowercases the resolution unit in 
the default locale
-    @DisabledIfSystemProperty(named = "user.language", matches = "tr")
     @Test
     public void getNormalMetadataToo() throws Exception {
         //this should be successful whether or not TesseractOCR is 
installed/active
@@ -258,6 +256,13 @@ public class TesseractOCRParserTest extends TikaTest {
         m = getXML("testTIFF.tif").metadata;
         assertEquals("100", m.get(TIFF.IMAGE_WIDTH));
         assertEquals("75", m.get(TIFF.IMAGE_LENGTH));
+    }
+
+    // TODO TIKA-4923: metadata-extractor lowercases the resolution unit in 
the default locale
+    @DisabledIfSystemProperty(named = "user.language", matches = "tr")
+    @Test
+    public void getNormalMetadataTooUnknownField() throws Exception {
+        Metadata m = getXML("testTIFF.tif").metadata;
         assertEquals("72 dots per inch", 
m.get(ImageMetadataExtractor.UNKNOWN_IMG_NS + "Exif IFD0:Y Resolution"));
     }
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
index 381469365c..4d13d4eabf 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.ogg;
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
@@ -26,8 +27,13 @@ import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
 
 import org.gagravarr.ogg.audio.OggAudioHeaders;
 import org.gagravarr.ogg.audio.OggAudioInfoHeader;
@@ -82,9 +88,9 @@ public abstract class OggAudioParser extends AbstractParser {
      * Returns the first positive integer found under the given comment keys,
      * or null if there is none.
      */
-    private static Integer firstPositiveInteger(VorbisStyleComments comments, 
String... keys) {
+    private static Integer firstPositiveInteger(Map<String, List<String>> 
fields, String... keys) {
         for (String key : keys) {
-            for (String value : comments.getComments(key)) {
+            for (String value : all(fields, key)) {
                 try {
                     int parsed = Integer.parseInt(value.trim());
                     if (parsed > 0) {
@@ -123,53 +129,60 @@ public abstract class OggAudioParser extends 
AbstractParser {
     protected static List<PictureBlock> extractComments(Metadata metadata,
             XHTMLContentHandler xhtml, VorbisStyleComments comments)
             throws IOException, TikaException, SAXException {
+        Map<String, List<String>> fields = fields(comments);
+        String title = first(fields, VorbisComments.KEY_TITLE);
+        String artist = first(fields, VorbisComments.KEY_ARTIST);
+        String album = first(fields, VorbisComments.KEY_ALBUM);
+        String trackNumber = first(fields, VorbisComments.KEY_TRACKNUMBER);
         // Get the specific known comments
-        metadata.set(TikaCoreProperties.TITLE, comments.getTitle());
-        metadata.set(TikaCoreProperties.CREATOR, comments.getArtist());
-        metadata.set(XMPDM.ARTIST, comments.getArtist());
-        metadata.set(XMPDM.ALBUM, comments.getAlbum());
-        metadata.set(XMPDM.GENRE, comments.getGenre());
-        metadata.set(XMPDM.RELEASE_DATE, comments.getDate());
+        metadata.set(TikaCoreProperties.TITLE, title);
+        metadata.set(TikaCoreProperties.CREATOR, artist);
+        metadata.set(XMPDM.ARTIST, artist);
+        metadata.set(XMPDM.ALBUM, album);
+        metadata.set(XMPDM.GENRE, first(fields, VorbisComments.KEY_GENRE));
+        metadata.set(XMPDM.RELEASE_DATE, first(fields, 
VorbisComments.KEY_DATE));
         metadata.add(XMP.CREATOR_TOOL, comments.getVendor());
         metadata.add(VORBIS_VENDOR, comments.getVendor());
 
         //xmpDM:copyright is single-valued, so map the first comment; like
         //vendor, the raw comments also stay available under the vorbis: name
-        List<String> copyrights = comments.getComments("copyright");
-        if (!copyrights.isEmpty()) {
-            metadata.set(XMPDM.COPYRIGHT, copyrights.get(0));
+        String copyright = first(fields, "copyright");
+        if (copyright != null) {
+            metadata.set(XMPDM.COPYRIGHT, copyright);
         }
 
-        for (String comment : comments.getComments("comment")) {
+        for (String comment : all(fields, "comment")) {
             metadata.add(XMPDM.LOG_COMMENT, comment);
         }
 
         // Grab the rest just in case; the pictures become embedded
         //  documents instead, their raw base64 blocks help nobody
-        List<String> done = Arrays.asList(
+        Set<String> done = new HashSet<>();
+        for (String key : Arrays.asList(
                 VorbisComments.KEY_TITLE, VorbisComments.KEY_ARTIST,
                 VorbisComments.KEY_ALBUM, VorbisComments.KEY_GENRE,
                 VorbisComments.KEY_DATE, VorbisComments.KEY_TRACKNUMBER,
-                "vendor", "comment", METADATA_BLOCK_PICTURE
-        );
+                "vendor", "comment", METADATA_BLOCK_PICTURE)) {
+            done.add(fieldName(key));
+        }
         // BAG: a Vorbis comment field can legitimately repeat.
-        for (String key : comments.getAllComments().keySet()) {
-            if (!done.contains(key)) {
-                for (String value : comments.getAllComments().get(key)) {
-                    metadata.add(VORBIS, key, value);
+        for (Map.Entry<String, List<String>> field : fields.entrySet()) {
+            if (!done.contains(field.getKey())) {
+                for (String value : field.getValue()) {
+                    metadata.add(VORBIS, field.getKey(), value);
                 }
             }
         }
 
         // Output as text too
-        xhtml.element("h1", comments.getTitle());
-        xhtml.element("p", comments.getArtist());
+        xhtml.element("h1", title);
+        xhtml.element("p", artist);
 
         // Album and Track number
-        if (comments.getTrackNumber() != null) {
-            xhtml.element("p", comments.getAlbum() + ", track " + 
comments.getTrackNumber());
-            metadata.set(Audio.RAW_TRACK_NUMBER, comments.getTrackNumber());
-            NumberAndTotal trackNumberAndTotal = 
NumberAndTotal.parse(comments.getTrackNumber());
+        if (trackNumber != null) {
+            xhtml.element("p", album + ", track " + trackNumber);
+            metadata.set(Audio.RAW_TRACK_NUMBER, trackNumber);
+            NumberAndTotal trackNumberAndTotal = 
NumberAndTotal.parse(trackNumber);
             if (trackNumberAndTotal != null) {
                 if (trackNumberAndTotal.number != null) {
                     metadata.set(XMPDM.TRACK_NUMBER, 
trackNumberAndTotal.number);
@@ -179,9 +192,9 @@ public abstract class OggAudioParser extends AbstractParser 
{
                 }
             }
         } else {
-            xhtml.element("p", comments.getAlbum());
+            xhtml.element("p", album);
         }
-        for (String discValue : comments.getComments("discnumber")) {
+        for (String discValue : all(fields, "discnumber")) {
             metadata.set(Audio.RAW_DISC_NUMBER, discValue);
             NumberAndTotal discNumberAndTotal = 
NumberAndTotal.parse(discValue);
             if (discNumberAndTotal != null) {
@@ -194,24 +207,111 @@ public abstract class OggAudioParser extends 
AbstractParser {
             }
         }
         //explicit totals win over the combined "n/total" form
-        Integer trackTotal = firstPositiveInteger(comments, "tracktotal", 
"totaltracks");
+        Integer trackTotal = firstPositiveInteger(fields, "tracktotal", 
"totaltracks");
         if (trackTotal != null) {
             metadata.set(Audio.TRACK_COUNT, trackTotal);
         }
-        Integer discTotal = firstPositiveInteger(comments, "disctotal", 
"totaldiscs");
+        Integer discTotal = firstPositiveInteger(fields, "disctotal", 
"totaldiscs");
         if (discTotal != null) {
             metadata.set(Audio.DISC_COUNT, discTotal);
         }
 
         // A few other bits
-        xhtml.element("p", comments.getDate());
-        for (String comment : comments.getComments("comment")) {
+        xhtml.element("p", first(fields, VorbisComments.KEY_DATE));
+        for (String comment : all(fields, "comment")) {
             xhtml.element("p", comment);
         }
-        xhtml.element("p", comments.getGenre());
+        xhtml.element("p", first(fields, VorbisComments.KEY_GENRE));
 
         // The pictures are the caller's to emit
-        return parsePictures(comments);
+        return parsePictures(fields);
+    }
+
+    /**
+     * The comment fields keyed by lower-case name, re-read from the raw 
comment header:
+     * vorbis-java folds names in the default locale and then strips 
non-ASCII, so a
+     * Turkic JVM turns TITLE into "ttle" (TIKA-4921). Falls back to the 
library's map
+     * when there is no raw header, as for comments built in code.
+     */
+    static Map<String, List<String>> fields(VorbisStyleComments comments) {
+        Map<String, List<String>> fields = rawFields(comments);
+        if (fields != null) {
+            return fields;
+        }
+        fields = new LinkedHashMap<>();
+        for (Map.Entry<String, List<String>> field : 
comments.getAllComments().entrySet()) {
+            fields.computeIfAbsent(fieldName(field.getKey()), k -> new 
ArrayList<>())
+                    .addAll(field.getValue());
+        }
+        return fields;
+    }
+
+    private static Map<String, List<String>> rawFields(VorbisStyleComments 
comments) {
+        byte[] data;
+        try {
+            data = comments.getData();
+        } catch (NullPointerException e) {
+            // no packet behind comments built in code
+            return null;
+        }
+        if (data == null) {
+            return null;
+        }
+        // the format-specific header size is protected; find the vendor 
string instead
+        String vendor = comments.getVendor() == null ? "" : 
comments.getVendor();
+        byte[] vendorBytes = vendor.getBytes(StandardCharsets.UTF_8);
+        ByteBuffer buffer = 
ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
+        int offset = -1;
+        for (int headerSize : new int[]{0, 4, 7, 8}) {
+            int end = headerSize + 4 + vendorBytes.length;
+            if (end <= data.length && buffer.getInt(headerSize) == 
vendorBytes.length &&
+                    Arrays.equals(data, headerSize + 4, end, vendorBytes, 0, 
vendorBytes.length)) {
+                offset = end;
+                break;
+            }
+        }
+        if (offset < 0) {
+            return null;
+        }
+        try {
+            buffer.position(offset);
+            int count = buffer.getInt();
+            if (count < 0) {
+                return null;
+            }
+            Map<String, List<String>> fields = new LinkedHashMap<>();
+            for (int i = 0; i < count; i++) {
+                int length = buffer.getInt();
+                if (length < 0 || length > buffer.remaining()) {
+                    return null;
+                }
+                byte[] bytes = new byte[length];
+                buffer.get(bytes);
+                String comment = new String(bytes, StandardCharsets.UTF_8);
+                int eq = comment.indexOf('=');
+                if (eq < 0) {
+                    continue;
+                }
+                fields.computeIfAbsent(fieldName(comment.substring(0, eq)), k 
-> new ArrayList<>())
+                        .add(comment.substring(eq + 1));
+            }
+            return fields;
+        } catch (BufferUnderflowException e) {
+            return null;
+        }
+    }
+
+    private static String fieldName(String name) {
+        return name.toLowerCase(Locale.ROOT);
+    }
+
+    private static String first(Map<String, List<String>> fields, String name) 
{
+        List<String> values = fields.get(fieldName(name));
+        return values == null || values.isEmpty() ? null : values.get(0);
+    }
+
+    private static List<String> all(Map<String, List<String>> fields, String 
name) {
+        return fields.getOrDefault(fieldName(name), Collections.emptyList());
     }
 
     /**
@@ -219,9 +319,9 @@ public abstract class OggAudioParser extends AbstractParser 
{
      * The pictures are carried as base64 encoded FLAC picture blocks;
      * malformed blocks are skipped silently.
      */
-    private static List<PictureBlock> parsePictures(VorbisStyleComments 
comments) {
+    private static List<PictureBlock> parsePictures(Map<String, List<String>> 
fields) {
         List<PictureBlock> pictures = new ArrayList<>();
-        for (String block : comments.getComments(METADATA_BLOCK_PICTURE)) {
+        for (String block : all(fields, METADATA_BLOCK_PICTURE)) {
             byte[] decoded;
             try {
                 decoded = Base64.getMimeDecoder().decode(block);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
index f4398d2307..4fde9e14ee 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
@@ -26,7 +26,6 @@ import java.nio.file.Path;
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
 import org.junit.jupiter.api.io.TempDir;
 
 import org.apache.tika.TikaTest;
@@ -89,8 +88,6 @@ public class FlacParserTest extends TikaTest {
      * both sources are merged before the pick (both are front covers here,
      * so the first one, from the comment, wins).
      */
-    // TODO TIKA-4921: vorbis-java lowercases comment keys in the default 
locale
-    @DisabledIfSystemProperty(named = "user.language", matches = "tr")
     @Test
     public void testCommentAndNativePictureYieldOneThumbnail() throws 
Exception {
         List<Metadata> metadataList =
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisCommentsLocaleTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisCommentsLocaleTest.java
new file mode 100644
index 0000000000..e8a0b703b5
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisCommentsLocaleTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.tika.parser.ogg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.metadata.XMPDM;
+
+/**
+ * TIKA-4921: vorbis-java folds comment field names in the default locale, so 
on a
+ * Turkic JVM "TITLE" is stored as "ttle" and never found again.
+ */
+// sets the JVM-wide default locale
+@Isolated
+public class VorbisCommentsLocaleTest extends TikaTest {
+
+    @Test
+    public void testTurkishLocale(@TempDir Path tmp) throws Exception {
+        // same-length upper-case keys keep every block header valid
+        String flac = new String(
+                
Files.readAllBytes(getResourceAsFile("/test-documents/testFLAC_commentAndNativePicture.flac").toPath()),
+                StandardCharsets.ISO_8859_1);
+        assertEquals(1, count(flac, "title=Test Title"));
+        flac = flac.replace("title=Test Title", "TITLE=Test Title")
+                .replace("artist=Test Artist", "ARTIST=Test Artist");
+        Path upperCased = tmp.resolve("upper.flac");
+        Files.write(upperCased, flac.getBytes(StandardCharsets.ISO_8859_1));
+
+        Locale defaultLocale = Locale.getDefault();
+        Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+        try {
+            List<Metadata> metadataList = getRecursiveMetadata(upperCased);
+            assertEquals("Test Title", 
metadataList.get(0).get(TikaCoreProperties.TITLE));
+            assertEquals("Test Artist", metadataList.get(0).get(XMPDM.ARTIST));
+            assertEquals(3, metadataList.size());
+            assertEquals("Comment cover", 
metadataList.get(1).get(TikaCoreProperties.TITLE));
+        } finally {
+            Locale.setDefault(defaultLocale);
+        }
+    }
+
+    private static int count(String s, String needle) {
+        int n = 0;
+        for (int i = s.indexOf(needle); i >= 0; i = s.indexOf(needle, i + 1)) {
+            n++;
+        }
+        return n;
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/ImagePreprocessor.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/ImagePreprocessor.java
index fdb4c42eff..f1b9fbdb7e 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/ImagePreprocessor.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/ImagePreprocessor.java
@@ -125,7 +125,7 @@ class ImagePreprocessor implements Serializable {
             angle = 0d;
         } else {
             metadata.add(TesseractOCRParser.IMAGE_ROTATION,
-                    String.format(Locale.getDefault(), "%.3f", angle));
+                    String.format(Locale.ROOT, "%.3f", angle));
         }
 
         return angle;
diff --git 
a/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/IgniteConfigStore.java
 
b/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/IgniteConfigStore.java
index e57bd62850..9e7c2909d2 100644
--- 
a/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/IgniteConfigStore.java
+++ 
b/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/IgniteConfigStore.java
@@ -17,6 +17,7 @@
 package org.apache.tika.pipes.ignite;
 
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 import org.apache.ignite.Ignite;
@@ -96,7 +97,7 @@ public class IgniteConfigStore implements ConfigStore {
                     .addresses("127.0.0.1:10800")
                     .build();
 
-            Table table = ignite.tables().table(tableName);
+            Table table = ignite.tables().table(quotedName(tableName));
             if (table == null) {
                 throw new IllegalStateException("Table " + tableName + " not 
found. Ensure IgniteStoreServer is running.");
             }
@@ -148,10 +149,11 @@ public class IgniteConfigStore implements ConfigStore {
     public Set<String> keySet() {
         checkInitialized();
         try {
-            var resultSet = ignite.sql().execute(null, "SELECT id FROM " + 
tableName);
+            var resultSet = ignite.sql().execute(null, "SELECT id FROM " + 
quotedName(tableName));
             Set<String> keys = new HashSet<>();
             while (resultSet.hasNext()) {
-                keys.add(resultSet.next().stringValue("id"));
+                // by position: column lookup by name folds in the default 
locale too
+                keys.add(resultSet.next().stringValue(0));
             }
             return keys;
         } catch (Exception e) {
@@ -164,9 +166,9 @@ public class IgniteConfigStore implements ConfigStore {
     public int size() {
         checkInitialized();
         try {
-            var resultSet = ignite.sql().execute(null, "SELECT COUNT(*) as cnt 
FROM " + tableName);
+            var resultSet = ignite.sql().execute(null, "SELECT COUNT(*) as cnt 
FROM " + quotedName(tableName));
             if (resultSet.hasNext()) {
-                return (int) resultSet.next().longValue("cnt");
+                return (int) resultSet.next().longValue(0);
             }
             return 0;
         } catch (Exception e) {
@@ -205,6 +207,14 @@ public class IgniteConfigStore implements ConfigStore {
         this.tableName = tableName;
     }
 
+    /**
+     * Quoted so Ignite does not fold it in the default locale (TIKA-4922); 
upper-cased
+     * first so it still names a table created unquoted.
+     */
+    public static String quotedName(String tableName) {
+        return '"' + tableName.toUpperCase(Locale.ROOT).replace("\"", "\"\"") 
+ '"';
+    }
+
     public void setReplicas(int replicas) {
         this.replicas = replicas;
     }
diff --git 
a/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/server/IgniteStoreServer.java
 
b/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/server/IgniteStoreServer.java
index 4b03922800..e467c56bf3 100644
--- 
a/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/server/IgniteStoreServer.java
+++ 
b/tika-pipes/tika-pipes-config-store-ignite/src/main/java/org/apache/tika/pipes/ignite/server/IgniteStoreServer.java
@@ -19,7 +19,6 @@ package org.apache.tika.pipes.ignite.server;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Locale;
 import java.util.concurrent.ExecutionException;
 
 import org.apache.ignite.IgniteServer;
@@ -28,6 +27,8 @@ import org.apache.ignite.table.Table;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.tika.pipes.ignite.IgniteConfigStore;
+
 /**
  * Embedded Ignite 3.x server node that hosts the config store table.
  * The {@link org.apache.tika.pipes.ignite.IgniteConfigStore} connects to this 
node as a thin client.
@@ -116,19 +117,18 @@ public class IgniteStoreServer implements AutoCloseable {
             node.api().sql().execute(null, createZoneSql);
             LOG.info("Distribution zone 'tika_zone' created/verified");
 
-            Table existingTable = node.api().tables().table(tableName);
+            Table existingTable = 
node.api().tables().table(IgniteConfigStore.quotedName(tableName));
             if (existingTable != null) {
                 LOG.info("Table {} already exists", tableName);
                 return;
             }
 
-            String createTableSql = String.format(Locale.ROOT,
-                    "CREATE TABLE IF NOT EXISTS %s (" +
+            String createTableSql = "CREATE TABLE IF NOT EXISTS " +
+                    IgniteConfigStore.quotedName(tableName) + " (" +
                     "  id VARCHAR PRIMARY KEY," +
                     "  name VARCHAR," +
                     "  json VARCHAR(10000)" +
-                    ") ZONE tika_zone",
-                    tableName);
+                    ") ZONE tika_zone";
 
             node.api().sql().execute(null, createTableSql);
             LOG.info("Table {} created successfully", tableName);
diff --git 
a/tika-pipes/tika-pipes-config-store-ignite/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
 
b/tika-pipes/tika-pipes-config-store-ignite/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
index bb31368ab3..2f2d6ce6b1 100644
--- 
a/tika-pipes/tika-pipes-config-store-ignite/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
+++ 
b/tika-pipes/tika-pipes-config-store-ignite/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
@@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.nio.file.Path;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 import org.junit.jupiter.api.AfterAll;
@@ -33,8 +34,8 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,8 +45,8 @@ import org.apache.tika.plugins.ExtensionConfig;
 /**
  * Integration tests for {@link IgniteConfigStore} using an embedded Ignite 
3.x server.
  */
-// TODO TIKA-4922: Ignite uppercases unquoted table names in the default locale
-@DisabledIfSystemProperty(named = "user.language", matches = "tr")
+// testTurkishLocale sets the JVM-wide default locale
+@Isolated
 public class IgniteConfigStoreTest {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(IgniteConfigStoreTest.class);
@@ -160,6 +161,24 @@ public class IgniteConfigStoreTest {
         }
     }
 
+    // TIKA-4922: Ignite folds unquoted table names in the default locale
+    @Test
+    public void testTurkishLocale() throws Exception {
+        Locale defaultLocale = Locale.getDefault();
+        Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+        IgniteConfigStore trStore = new IgniteConfigStore();
+        try {
+            trStore.init();
+            trStore.put("tr1", new ExtensionConfig("tr1", "type1", "{}"));
+            assertNotNull(trStore.get("tr1"));
+            assertTrue(trStore.keySet().contains("tr1"));
+            assertEquals(1, trStore.size());
+        } finally {
+            trStore.close();
+            Locale.setDefault(defaultLocale);
+        }
+    }
+
     @Test
     public void testUninitializedStore() {
         IgniteConfigStore uninitializedStore = new IgniteConfigStore();

Reply via email to