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}