001package gudusoft.gsqlparser.sqlenv;
002
003import java.text.CollationKey;
004import java.text.Collator;
005import java.util.Locale;
006import java.util.concurrent.ConcurrentHashMap;
007
008/**
009 * Collator 提供者(SQL Server COLLATION_BASED 专用)
010 *
011 * <p><strong>关键设计:</strong>
012 * <ul>
013 * <li>{@link Collator} 非线程安全,使用 {@link ThreadLocal} 缓存
014 * <li>按 collation 名称缓存实例(每个 collation 一个 ThreadLocal)
015 * <li>支持常见 SQL Server collation(CI_AS, CS_AS 等)
016 * </ul>
017 *
018 * <p>使用示例:
019 * <pre>
020 * CollatorProvider provider = new CollatorProvider();
021 *
022 * // 获取 Collator(自动 ThreadLocal 缓存)
023 * Collator collator = provider.getCollator("SQL_Latin1_General_CP1_CI_AS");
024 *
025 * // 比较标识符
026 * int result = collator.compare("MyTable", "MYTABLE");
027 * // CI (Case Insensitive): result == 0
028 * // CS (Case Sensitive): result != 0
029 * </pre>
030 *
031 * <p><strong>性能特性:</strong>
032 * <ul>
033 * <li>首次访问:创建 Collator 实例(~100μs)
034 * <li>后续访问:ThreadLocal.get()(~10ns)
035 * <li>内存开销:每线程每 collation 一个实例(~1KB)
036 * </ul>
037 *
038 * @since 3.1.0.9
039 */
040public class CollatorProvider {
041
042    // Per-collation 的 ThreadLocal 缓存
043    // Key: collation 名称(如 "SQL_Latin1_General_CP1_CI_AS")
044    // Value: ThreadLocal<Collator>(每线程独立实例)
045    private final ConcurrentHashMap<String, ThreadLocal<Collator>> collatorCache =
046        new ConcurrentHashMap<>();
047
048    // ===== 公共接口 =====
049
050    /**
051     * 获取 Collator 实例(ThreadLocal 缓存)
052     *
053     * <p>此方法线程安全,每个线程获得独立的 Collator 实例。
054     *
055     * @param collationName SQL Server collation 名称(例如:SQL_Latin1_General_CP1_CI_AS)
056     * @return Collator 实例(线程本地)
057     */
058    public Collator getCollator(String collationName) {
059        if (collationName == null || collationName.isEmpty()) {
060            // 默认:当前 Locale 的 Collator
061            return Collator.getInstance();
062        }
063
064        // 为每个 collation 名称创建一个 ThreadLocal
065        ThreadLocal<Collator> tl = collatorCache.computeIfAbsent(collationName, key ->
066            ThreadLocal.withInitial(() -> createCollator(key))
067        );
068
069        return tl.get();
070    }
071
072    /**
073     * 清理缓存(用于测试或内存管理)
074     *
075     * <p>警告:调用此方法后,所有线程的 Collator 实例将失效,下次访问时重新创建。
076     */
077    public void clearCache() {
078        collatorCache.clear();
079    }
080
081    // ===== 内部实现 =====
082
083    /**
084     * 根据 SQL Server collation 名称创建 Collator
085     *
086     * <p>SQL Server collation 名称格式:
087     * <pre>
088     * {Language}_{Region}_CP{CodePage}_{CaseSensitivity}_{AccentSensitivity}
089     *
090     * 例如:
091     * - SQL_Latin1_General_CP1_CI_AS  (Case Insensitive, Accent Sensitive)
092     * - SQL_Latin1_General_CP1_CS_AS  (Case Sensitive, Accent Sensitive)
093     * - Chinese_PRC_CI_AS             (简体中文, Case Insensitive)
094     * </pre>
095     *
096     * @param collationName SQL Server collation 名称
097     * @return 配置好的 Collator 实例
098     */
099    private Collator createCollator(String collationName) {
100        // 解析 collation 名称
101        Locale locale = parseLocale(collationName);
102        Collator collator = Collator.getInstance(locale);
103
104        // Accent-insensitive (_AI) collations whose accent equivalence was measured on the engine
105        // (SqlServerAccentFolding: Latin1_General_CI_AI / _CS_AI, SQL_Latin1_General_CP1_CI_AI).
106        // SQL Server 2022: under Latin1_General_CI_AI `FROM CAFE` finds the table café; under
107        // Latin1_General_CS_AI `FROM cafe` finds café while `Cafe` stays a different name; o/ø,
108        // l/ł, d/đ, i/ı are equal under both, U+AC00 and its jamo U+1100 U+1161 are not. Java's
109        // Collator cannot say that at any strength (PRIMARY also equates `a b`/`ab`, `a-b`/`ab`),
110        // and stripping combining marks after NFD gets ø/ł/đ/ı wrong and merges the Hangul pair.
111        // So the name is first folded by the measured table, then compared exactly as the
112        // sibling _AS collation is compared: CI_AI by the CI_AS collator, CS_AI exactly (a _CS_AS
113        // cell is SENSITIVE). Every other _AI collation keeps the shape below.
114        if (SqlServerAccentFolding.isMeasured(collationName)) {
115            if (SqlServerAccentFolding.isCaseSensitive(collationName)) {
116                return new AccentInsensitiveCollator(collationName, null);
117            }
118            // The sibling _CI_AS collation, spelled with upper-case CI/AS: collation names are
119            // case-insensitive, and the strength/decomposition checks below match upper case.
120            String sibling = collationName.replaceAll("(?i)(?<=_)AI(?=_|$)", "AS")
121                    .replaceAll("(?i)(?<=_)CI(?=_|$)", "CI");
122            return new AccentInsensitiveCollator(collationName, createCollator(sibling));
123        }
124
125        // 设置强度(Strength)
126        if (collationName.contains("_CI_") || collationName.contains("_CI")) {
127            // Case Insensitive(CI)
128            collator.setStrength(Collator.SECONDARY);  // 忽略大小写
129        } else if (collationName.contains("_CS_") || collationName.contains("_CS")) {
130            // Case Sensitive(CS)
131            collator.setStrength(Collator.TERTIARY);   // 区分大小写
132        } else {
133            // 默认:根据 Locale 决定(通常是 TERTIARY)
134            collator.setStrength(Collator.TERTIARY);
135        }
136
137        // 设置分解模式(Decomposition)
138        if (collationName.contains("_AS")) {
139            // Accent Sensitive(AS)
140            collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
141        } else if (collationName.contains("_AI")) {
142            // Accent Insensitive(AI)
143            collator.setDecomposition(Collator.NO_DECOMPOSITION);
144        } else {
145            // 默认:Canonical Decomposition
146            collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
147        }
148
149        return collator;
150    }
151
152    /**
153     * 从 collation 名称解析 Locale
154     *
155     * <p>这是简化实现,仅支持常见 collation。
156     * 生产环境需要完整的 SQL Server collation → Java Locale 映射表。
157     *
158     * @param collationName SQL Server collation 名称
159     * @return 对应的 Java Locale
160     */
161    private Locale parseLocale(String collationName) {
162        String lowerName = collationName.toLowerCase(Locale.ROOT);
163
164        // SQL Server 常见 collation 映射
165        if (lowerName.contains("latin1") || lowerName.contains("sql_")) {
166            // Latin1 / SQL_ 前缀 → 英语(美国)
167            return new Locale("en", "US");
168        } else if (lowerName.contains("chinese_prc") || lowerName.contains("chinese_simplified")) {
169            // 简体中文
170            return new Locale("zh", "CN");
171        } else if (lowerName.contains("chinese_taiwan") || lowerName.contains("chinese_traditional")) {
172            // 繁体中文(台湾)
173            return new Locale("zh", "TW");
174        } else if (lowerName.contains("chinese_hong_kong")) {
175            // 繁体中文(香港)
176            return new Locale("zh", "HK");
177        } else if (lowerName.contains("japanese")) {
178            // 日语
179            return new Locale("ja", "JP");
180        } else if (lowerName.contains("korean")) {
181            // 韩语
182            return new Locale("ko", "KR");
183        } else if (lowerName.contains("arabic")) {
184            // 阿拉伯语
185            return new Locale("ar");
186        } else if (lowerName.contains("french")) {
187            // 法语
188            return new Locale("fr", "FR");
189        } else if (lowerName.contains("german")) {
190            // 德语
191            return new Locale("de", "DE");
192        } else if (lowerName.contains("spanish")) {
193            // 西班牙语
194            return new Locale("es", "ES");
195        } else if (lowerName.contains("italian")) {
196            // 意大利语
197            return new Locale("it", "IT");
198        } else if (lowerName.contains("russian")) {
199            // 俄语
200            return new Locale("ru", "RU");
201        } else {
202            // 默认:英语(美国)
203            return Locale.US;
204        }
205
206        // 生产环境实现:
207        // 1. 维护完整的 SQL Server collation → Locale 映射表
208        // 2. 解析 collation 名称的各个组成部分
209        // 3. 根据 Language 和 Region 构造 Locale
210        // 4. 参考:https://docs.microsoft.com/en-us/sql/relational-databases/collations/collation-and-unicode-support
211    }
212
213    /**
214     * Whether a SQL Server collation name says accent-insensitive: one of its underscore-separated
215     * parts is {@code AI} ({@code Latin1_General_CI_AI}, {@code Latin1_General_100_CS_AI_SC},
216     * {@code SQL_Latin1_General_CP1_CI_AI}). Collation names are case-insensitive.
217     *
218     * @param collationName the collation name; null is not accent-insensitive
219     * @return true for an {@code _AI} collation
220     */
221    public static boolean isAccentInsensitive(String collationName) {
222        return hasToken(collationName, "AI");
223    }
224
225    /**
226     * Whether a SQL Server collation compares case-sensitively, from its name: a {@code _CS}
227     * part ({@code Latin1_General_CS_AS}, {@code Latin1_General_CS_AI}) or a binary collation
228     * ({@code _BIN}, {@code _BIN2}) says yes, anything else ({@code _CI}, or no collation, which
229     * means the case-insensitive default) says no.
230     *
231     * <p>The one answer to "is this collation case-sensitive" for a
232     * {@link IdentifierRules.CaseCompare#COLLATION_BASED} cell, whose compare kind alone does not
233     * say it: {@code Latin1_General_CS_AI} and {@code Latin1_General_BIN2} are collation-based and
234     * case-sensitive, {@code SQL_Latin1_General_CP1_CI_AS} is collation-based and not.
235     *
236     * @param collationName the collation name; null or empty is the case-insensitive default
237     * @return true when names that differ only in case are different names
238     */
239    public static boolean isCaseSensitiveCollation(String collationName) {
240        return hasToken(collationName, "CS") || hasToken(collationName, "BIN")
241                || hasToken(collationName, "BIN2");
242    }
243
244    private static boolean hasToken(String collationName, String token) {
245        if (collationName == null) {
246            return false;
247        }
248        int n = collationName.length();
249        int start = 0;
250        while (start <= n) {
251            int end = collationName.indexOf('_', start);
252            if (end < 0) {
253                end = n;
254            }
255            if (end - start == token.length()
256                    && collationName.regionMatches(true, start, token, 0, token.length())) {
257                return true;
258            }
259            start = end + 1;
260        }
261        return false;
262    }
263
264    /**
265     * Whether GSP compares this collation with a MEASURED accent equivalence
266     * ({@link SqlServerAccentFolding}): {@code Latin1_General_CI_AI}, {@code Latin1_General_CS_AI}
267     * and {@code SQL_Latin1_General_CP1_CI_AI}. Other {@code _AI} collations were not measured and
268     * keep the comparison they had before (a {@code _CS_AI} one is then exact).
269     *
270     * @param collationName the collation name; null is not measured
271     * @return true when the measured accent table applies
272     */
273    public static boolean hasMeasuredAccentFolding(String collationName) {
274        return SqlServerAccentFolding.isMeasured(collationName);
275    }
276
277    /**
278     * A measured accent-insensitive collation: both names are folded by
279     * {@link SqlServerAccentFolding} (accents mapped to their base or deleted, as measured on the
280     * engine), then compared as the sibling {@code _AS} collation compares — by the case-insensitive
281     * {@code _CI_AS} collator ({@code _CI_AI}), or exactly ({@code _CS_AI}, as a {@code _CS_AS}
282     * cell compares). Its collation keys are built from the same folded text, so key equality is
283     * exactly {@code compare == 0}.
284     *
285     * <p>Like every {@link Collator} it is not thread-safe; {@link CollatorProvider} hands each
286     * thread its own instance.
287     */
288    static final class AccentInsensitiveCollator extends Collator {
289
290        /** The measured collation whose table folds the text. */
291        private final String collationName;
292        /** The sibling _CI_AS collator; null for _CS_AI, which compares the folded text exactly. */
293        private final Collator caseInsensitiveBase;
294
295        AccentInsensitiveCollator(String collationName, Collator caseInsensitiveBase) {
296            if ((caseInsensitiveBase == null) != SqlServerAccentFolding.isCaseSensitive(collationName)) {
297                throw new IllegalArgumentException("a _CI_AI collator needs its _CI_AS base, a _CS_AI one none");
298            }
299            this.collationName = collationName;
300            this.caseInsensitiveBase = caseInsensitiveBase;
301        }
302
303        /** The text with the measured accent equivalence applied; the same instance when unchanged. */
304        String fold(String s) {
305            return SqlServerAccentFolding.fold(s, collationName);
306        }
307
308        @Override
309        public int compare(String source, String target) {
310            String a = fold(source);
311            String b = fold(target);
312            return caseInsensitiveBase != null ? caseInsensitiveBase.compare(a, b) : a.compareTo(b);
313        }
314
315        @Override
316        public CollationKey getCollationKey(String source) {
317            if (source == null) {
318                return null;
319            }
320            String folded = fold(source);
321            byte[] bytes;
322            if (caseInsensitiveBase != null) {
323                bytes = caseInsensitiveBase.getCollationKey(folded).toByteArray();
324            } else {
325                // UTF-16 code units, big-endian: unsigned byte order is String.compareTo order.
326                bytes = new byte[folded.length() * 2];
327                for (int i = 0; i < folded.length(); i++) {
328                    char c = folded.charAt(i);
329                    bytes[2 * i] = (byte) (c >>> 8);
330                    bytes[2 * i + 1] = (byte) c;
331                }
332            }
333            return new BytesKey(source, bytes);
334        }
335
336        @Override
337        public boolean equals(Object that) {
338            if (this == that) {
339                return true;
340            }
341            if (!(that instanceof AccentInsensitiveCollator)) {
342                return false;
343            }
344            AccentInsensitiveCollator other = (AccentInsensitiveCollator) that;
345            return collationName.equalsIgnoreCase(other.collationName) // non-identifier-compare: collation name
346                    && (caseInsensitiveBase == null ? other.caseInsensitiveBase == null
347                        : caseInsensitiveBase.equals(other.caseInsensitiveBase));
348        }
349
350        @Override
351        public int hashCode() {
352            return collationName.toLowerCase(Locale.ROOT).hashCode() ^ 0x5A1;
353        }
354
355        /** Collation key over precomputed bytes, ordered as unsigned byte strings. */
356        private static final class BytesKey extends CollationKey {
357            private final byte[] bytes;
358
359            BytesKey(String source, byte[] bytes) {
360                super(source);
361                this.bytes = bytes;
362            }
363
364            @Override
365            public int compareTo(CollationKey target) {
366                byte[] other = target.toByteArray();
367                int len = Math.min(bytes.length, other.length);
368                for (int i = 0; i < len; i++) {
369                    int d = (bytes[i] & 0xFF) - (other[i] & 0xFF);
370                    if (d != 0) {
371                        return d;
372                    }
373                }
374                return bytes.length - other.length;
375            }
376
377            @Override
378            public byte[] toByteArray() {
379                return bytes.clone();
380            }
381        }
382    }
383
384    // ===== 调试方法 =====
385
386    /**
387     * 获取缓存大小(不同 collation 的数量)
388     *
389     * @return 缓存的 collation 数量
390     */
391    public int getCacheSize() {
392        return collatorCache.size();
393    }
394
395    /**
396     * 测试 collation 比较(用于调试)
397     *
398     * @param collationName collation 名称
399     * @param str1 字符串 1
400     * @param str2 字符串 2
401     * @return 比较结果(0: 相等, <0: str1 < str2, >0: str1 > str2)
402     */
403    public int testCompare(String collationName, String str1, String str2) {
404        Collator collator = getCollator(collationName);
405        return collator.compare(str1, str2);
406    }
407}