001package gudusoft.gsqlparser.lineage.v2.identity; 002 003import java.util.Collections; 004import java.util.LinkedHashMap; 005import java.util.List; 006import java.util.Map; 007 008import gudusoft.gsqlparser.lineage.v2.SqllrResources; 009import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson; 010import gudusoft.gsqlparser.sqlenv.unicode.GspCaseData; 011 012/** 013 * {@code CanonicalSegmentKeyV1} and the decoding of a delimited SQL spelling 014 * ({@code lineage-spec/identifier-rules/FORMAT.md}, Sections 7.1 and 9) for one dialect's rule set under fixed 015 * effective flags and collations. 016 * 017 * <p>The key is what a SQL-LR document carries as a segment's {@code canonicalKey}; it is an encoding of the 018 * standard's rule data, not GSP's name equality. Binding decisions keep going through 019 * {@code IdentifierService}; a producer compares the two and refuses a portable identity where they disagree.</p> 020 * 021 * <p>Instances are immutable and thread-safe.</p> 022 * 023 * @since 4.2.17 024 */ 025public final class SegmentKeyV1 { 026 027 /** The path of the dependency snapshot the jar ships. */ 028 public static final String DRAFT_SNAPSHOT = "snapshots/snapshot-0.1.0-draft.json"; 029 030 public static final String SQL_LEXICAL = "SQL_LEXICAL"; 031 public static final String CATALOG_STORED = "CATALOG_STORED"; 032 public static final String QUOTED = "QUOTED"; 033 public static final String UNQUOTED = "UNQUOTED"; 034 035 /** The outcome of {@link #key}: a canonical segment key, or {@code UNAVAILABLE_COLLATION}. */ 036 public static final class Result { 037 public static final String KEY = "KEY"; 038 public static final String UNAVAILABLE_COLLATION = "UNAVAILABLE_COLLATION"; 039 040 private final String status; 041 private final String key; 042 043 private Result(String status, String key) { 044 this.status = status; 045 this.key = key; 046 } 047 048 /** {@link #KEY} or {@link #UNAVAILABLE_COLLATION}. */ 049 public String getStatus() { 050 return status; 051 } 052 053 /** The key; {@code null} unless the status is {@link #KEY}. */ 054 public String getKey() { 055 return key; 056 } 057 058 public boolean isKey() { 059 return key != null; 060 } 061 062 @Override 063 public String toString() { 064 return key != null ? KEY + " " + key : status; 065 } 066 } 067 068 /** The outcome of {@link #decode}: a segment's {@code text} and {@code quoteState}, or malformed. */ 069 public static final class Decoded { 070 private final String quoteState; 071 private final String text; 072 073 private Decoded(String quoteState, String text) { 074 this.quoteState = quoteState; 075 this.text = text; 076 } 077 078 /** True when the spelling is recognized as delimited but violates its escape rules. */ 079 public boolean isMalformed() { 080 return quoteState == null; 081 } 082 083 /** {@link #QUOTED} or {@link #UNQUOTED}; {@code null} when malformed. */ 084 public String getQuoteState() { 085 return quoteState; 086 } 087 088 /** The decoded text; {@code null} when malformed. */ 089 public String getText() { 090 return text; 091 } 092 093 @Override 094 public String toString() { 095 return isMalformed() ? "MALFORMED" : quoteState + " " + text; 096 } 097 } 098 099 private static final Decoded MALFORMED = new Decoded(null, null); 100 101 private final String dialect; 102 private final Map<?, ?> rules; 103 private final Map<String, Object> flags; 104 private final Map<String, String> collations; 105 106 private SegmentKeyV1(Map<?, ?> rules, Map<String, Object> flags, Map<String, String> collations) { 107 this.dialect = (String) rules.get("dialect"); 108 this.rules = rules; 109 this.flags = flags; 110 this.collations = collations; 111 } 112 113 /** 114 * The key function of a rule set. 115 * 116 * @param rules a rule set file, parsed 117 * @param effectiveFlags flag values; a declared flag that is absent takes its {@code default} 118 * @param effectiveCollations collation scope ({@code INSTANCE}, {@code DATABASE}) to vendor collation name; an 119 * absent scope means no collation is declared for it 120 * @throws IllegalArgumentException when the rule set is for another Unicode version than the case operations, 121 * a flag is not declared, or its value is not an exact value of its domain 122 */ 123 public static SegmentKeyV1 of(Map<?, ?> rules, Map<String, ?> effectiveFlags, 124 Map<String, String> effectiveCollations) { 125 if (!GspCaseData.UNICODE_VERSION.equals(rules.get("unicodeVersion"))) { 126 throw new IllegalArgumentException("the " + rules.get("dialect") + " rule set is for Unicode " 127 + rules.get("unicodeVersion") + "; the case operations implement " + GspCaseData.UNICODE_VERSION); 128 } 129 Map<?, ?> copy = (Map<?, ?>) CanonicalJson.immutableCopy(rules); 130 Map<String, Object> flags = new LinkedHashMap<String, Object>(); 131 Map<?, ?> declared = map(copy.get("flags")); 132 for (Map.Entry<?, ?> flag : declared.entrySet()) { 133 Map<?, ?> declaration = map(flag.getValue()); 134 flags.put((String) flag.getKey(), checkedFlag((String) flag.getKey(), declaration, declaration.get("default"))); 135 } 136 if (effectiveFlags != null) { 137 for (Map.Entry<String, ?> flag : effectiveFlags.entrySet()) { 138 Map<?, ?> declaration = map(declared.get(flag.getKey())); 139 if (declaration.isEmpty()) { 140 throw new IllegalArgumentException("flag " + flag.getKey() + " is not declared by the " 141 + copy.get("dialect") + " rule set"); 142 } 143 flags.put(flag.getKey(), checkedFlag(flag.getKey(), declaration, flag.getValue())); 144 } 145 } 146 Map<String, String> scopes = new LinkedHashMap<String, String>(); 147 if (effectiveCollations != null) { 148 scopes.putAll(effectiveCollations); 149 } 150 return new SegmentKeyV1(copy, Collections.unmodifiableMap(flags), Collections.unmodifiableMap(scopes)); 151 } 152 153 /** A flag value within its declared domain: a boolean, or an exact integer among the declared values (as Long). */ 154 private static Object checkedFlag(String name, Map<?, ?> declaration, Object value) { 155 if ("boolean".equals(declaration.get("type"))) { 156 if (value instanceof Boolean) { 157 return value; 158 } 159 } else if ("integer".equals(declaration.get("type"))) { 160 Long exact = exactLong(value); 161 if (exact != null && containsNumber((List<?>) declaration.get("values"), exact)) { 162 return exact; 163 } 164 } 165 throw new IllegalArgumentException("flag " + name + " = " + value + " is outside its domain"); 166 } 167 168 /** The value as a Long when it is an exact integer of an integral type; otherwise null. */ 169 private static Long exactLong(Object value) { 170 if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { 171 return Long.valueOf(((Number) value).longValue()); 172 } 173 if (value instanceof java.math.BigInteger && ((java.math.BigInteger) value).bitLength() < 64) { 174 return Long.valueOf(((java.math.BigInteger) value).longValue()); 175 } 176 return null; 177 } 178 179 /** 180 * The key function a dependency snapshot declares for a dialect: its rule set with its effective flags and 181 * collations. The rule set is read from the files the jar ships. 182 * 183 * @return {@code null} when the snapshot does not list the dialect 184 * @throws IllegalArgumentException when the entry's dialect, rule set version, digest or Unicode version does 185 * not match the rule set it names 186 */ 187 public static SegmentKeyV1 forSnapshot(Map<?, ?> snapshot, String dialect) { 188 Object entries = snapshot.get("identifierRules"); 189 if (!(entries instanceof List<?>)) { 190 return null; 191 } 192 for (Object item : (List<?>) entries) { 193 Map<?, ?> entry = map(item); 194 if (dialect.equals(entry.get("dialect"))) { 195 Map<?, ?> rules = map(SqllrResources.json((String) entry.get("path"))); 196 String digest = "sha256:" + SqllrResources.sha256Hex(CanonicalJson.jcsBytes(rules)); 197 if (!dialect.equals(rules.get("dialect")) || !sameValue(entry.get("ruleSetVersion"), 198 rules.get("ruleSetVersion")) || !digest.equals(entry.get("digest")) 199 || !sameValue(snapshot.get("unicodeVersion"), rules.get("unicodeVersion"))) { 200 throw new IllegalArgumentException("the snapshot's " + dialect + " entry does not match the rule set " 201 + entry.get("path") + " (dialect " + rules.get("dialect") + ", version " 202 + rules.get("ruleSetVersion") + ", digest " + digest + ")"); 203 } 204 Map<String, Object> flags = new LinkedHashMap<String, Object>(); 205 for (Map.Entry<?, ?> flag : map(entry.get("effectiveFlags")).entrySet()) { 206 flags.put((String) flag.getKey(), flag.getValue()); 207 } 208 Map<String, String> scopes = new LinkedHashMap<String, String>(); 209 for (Map.Entry<?, ?> scope : map(entry.get("collations")).entrySet()) { 210 scopes.put((String) scope.getKey(), (String) scope.getValue()); 211 } 212 return of(rules, flags, scopes); 213 } 214 } 215 return null; 216 } 217 218 /** {@link #forSnapshot} of the snapshot the jar ships ({@link #DRAFT_SNAPSHOT}). */ 219 public static SegmentKeyV1 draft(String dialect) { 220 return forSnapshot(map(SqllrResources.json(DRAFT_SNAPSHOT)), dialect); 221 } 222 223 public String getDialect() { 224 return dialect; 225 } 226 227 /** The effective flags, every declared flag included. */ 228 public Map<String, Object> getFlags() { 229 return flags; 230 } 231 232 /** The effective collations, by scope. */ 233 public Map<String, String> getCollations() { 234 return collations; 235 } 236 237 /** Whether the rule set defines the object type. */ 238 public boolean definesObjectType(String objectType) { 239 return map(rules.get("objectTypes")).containsKey(objectType); 240 } 241 242 /** 243 * {@code CanonicalSegmentKeyV1}. 244 * 245 * @param objectType an object type code of FORMAT.md, Section 4 246 * @param inputForm {@link #SQL_LEXICAL} or {@link #CATALOG_STORED} 247 * @param quoteState {@link #QUOTED} or {@link #UNQUOTED}; read for {@link #SQL_LEXICAL} only 248 * @param text the decoded text (Section 3) 249 * @throws IllegalArgumentException on an input error: unknown object type or input form, missing or invalid 250 * quote state, or text with an unpaired surrogate 251 */ 252 public Result key(String objectType, String inputForm, String quoteState, String text) { 253 if (text == null || !isScalarValues(text)) { 254 throw new IllegalArgumentException("text is not a string of Unicode scalar values"); 255 } 256 boolean lexical = SQL_LEXICAL.equals(inputForm); 257 if (!lexical && !CATALOG_STORED.equals(inputForm)) { 258 throw new IllegalArgumentException("unknown input form " + inputForm); 259 } 260 if (lexical && !QUOTED.equals(quoteState) && !UNQUOTED.equals(quoteState)) { 261 throw new IllegalArgumentException("SQL_LEXICAL needs quoteState QUOTED or UNQUOTED, not " + quoteState); 262 } 263 Map<?, ?> cell = map(map(rules.get("cells")).get(resolve(objectType)[0])); 264 Map<?, ?> unquoted = map(cell.get("unquoted")); 265 Map<?, ?> quoted = map(cell.get("quoted")); 266 String k; 267 if ("COLLATION".equals(quoted.get("compare"))) { 268 String name = collations.get((String) cell.get("collationScope")); 269 Object label = name == null ? null : map(rules.get("collations")).get(name); 270 if (label == null) { 271 return new Result(Result.UNAVAILABLE_COLLATION, null); 272 } 273 Map<?, ?> profile = map(map(rules.get("collationProfiles")).get(label)); 274 if (!withinProfile(profile, text)) { 275 return new Result(Result.UNAVAILABLE_COLLATION, null); 276 } 277 k = "IDENTITY".equals(profile.get("key")) ? text : GspCaseData.simpleFoldCollapse(text); 278 } else if (lexical) { 279 Map<?, ?> branch = QUOTED.equals(quoteState) ? quoted : unquoted; 280 String fold = (String) branch.get("fold"); 281 k = "NONE".equals(fold) ? text : fold(fold, text); 282 if ("INSENSITIVE".equals(branch.get("compare")) && "NONE".equals(fold)) { 283 k = GspCaseData.simpleFoldCollapse(k); 284 } 285 } else if ("SENSITIVE".equals(quoted.get("compare"))) { 286 k = text; 287 } else { 288 String fold = !"NONE".equals(unquoted.get("fold")) ? (String) unquoted.get("fold") : (String) quoted.get("fold"); 289 k = "NONE".equals(fold) ? GspCaseData.simpleFoldCollapse(text) : fold(fold, text); 290 } 291 return new Result(Result.KEY, truncate(k)); 292 } 293 294 /** 295 * FORMAT.md, Section 7.1: the {@code text} and {@code quoteState} of one identifier token's spelling, using 296 * the delimiter set the effective flags resolve for the object type. 297 * 298 * @throws IllegalArgumentException when the object type is not defined 299 */ 300 public Decoded decode(String objectType, String spelling) { 301 List<?> entries = (List<?>) map(rules.get("delimiterSets")).get(resolve(objectType)[1]); 302 if (spelling == null) { 303 throw new IllegalArgumentException("no spelling"); 304 } 305 if (!spelling.isEmpty()) { 306 for (Object item : entries) { 307 Map<?, ?> entry = map(item); 308 String open = (String) entry.get("open"); 309 if (spelling.startsWith(open)) { 310 String text = decodeBody(spelling.substring(open.length()), (String) entry.get("close"), 311 (String) entry.get("escape")); 312 return text == null ? MALFORMED : new Decoded(QUOTED, text); 313 } 314 } 315 } 316 return new Decoded(UNQUOTED, spelling); 317 } 318 319 /** 320 * The preferred delimiters for writing a name of the object type back into SQL (the first entry of its delimiter 321 * set, FORMAT.md Section 7): {open, close}, or {@code null} when the type has no delimited form. 322 */ 323 public String[] preferredDelimiters(String objectType) { 324 List<?> entries = (List<?>) map(rules.get("delimiterSets")).get(resolve(objectType)[1]); 325 if (entries == null || entries.isEmpty()) { 326 return null; 327 } 328 Map<?, ?> first = map(entries.get(0)); 329 return new String[] {(String) first.get("open"), (String) first.get("close")}; 330 } 331 332 /** Whether a delimited form exists for the object type (its delimiter set is not empty). */ 333 public boolean hasQuotedForm(String objectType) { 334 List<?> entries = (List<?>) map(rules.get("delimiterSets")).get(resolve(objectType)[1]); 335 return entries != null && !entries.isEmpty(); 336 } 337 338 /** {cell label, delimiter set label} of an object type under the effective flags (FORMAT.md, Section 8.3). */ 339 private String[] resolve(String objectType) { 340 Map<?, ?> types = map(rules.get("objectTypes")); 341 Map<?, ?> entry = map(types.get(objectType)); 342 if (entry.isEmpty()) { 343 throw new IllegalArgumentException("object type " + objectType + " is not defined by the " + dialect 344 + " rule set"); 345 } 346 String type = objectType; 347 if (entry.containsKey("sameRulesAs")) { 348 type = (String) entry.get("sameRulesAs"); 349 entry = map(types.get(type)); 350 } 351 String cell = (String) entry.get("cell"); 352 String delimiters = (String) entry.get("delimiters"); 353 Object variants = rules.get("variants"); 354 if (variants instanceof List<?>) { 355 for (Object item : (List<?>) variants) { 356 Map<?, ?> variant = map(item); 357 boolean applies = true; 358 for (Map.Entry<?, ?> when : map(variant.get("when")).entrySet()) { 359 applies &= sameValue(flags.get(when.getKey()), when.getValue()); 360 } 361 Map<?, ?> override = map(map(variant.get("objectTypes")).get(type)); 362 if (applies && !override.isEmpty()) { 363 if (override.get("cell") != null) { 364 cell = (String) override.get("cell"); 365 } 366 if (override.get("delimiters") != null) { 367 delimiters = (String) override.get("delimiters"); 368 } 369 } 370 } 371 } 372 return new String[] {cell, delimiters}; 373 } 374 375 private static boolean sameValue(Object a, Object b) { 376 Long x = exactLong(a); 377 Long y = exactLong(b); 378 if (x != null || y != null) { 379 return x != null && x.equals(y); 380 } 381 return a == null ? b == null : a.equals(b); 382 } 383 384 private static boolean containsNumber(List<?> values, Object value) { 385 if (values == null) { 386 return false; 387 } 388 for (Object candidate : values) { 389 if (sameValue(candidate, value)) { 390 return true; 391 } 392 } 393 return false; 394 } 395 396 private static String fold(String fold, String text) { 397 if ("UPPER".equals(fold)) { 398 return GspCaseData.toUpperFull(text); 399 } 400 if ("UPPER_SIMPLE".equals(fold)) { 401 return GspCaseData.toUpperSimple(text); 402 } 403 if ("LOWER_SIMPLE".equals(fold)) { 404 return GspCaseData.toLowerSimple(text); 405 } 406 if ("LOWER_ASCII".equals(fold)) { 407 return GspCaseData.toLowerAscii(text); 408 } 409 throw new IllegalStateException("unknown fold " + fold); 410 } 411 412 private static boolean withinProfile(Map<?, ?> profile, String text) { 413 List<?> ranges = (List<?>) profile.get("repertoire"); 414 int last = -1; 415 for (int i = 0; i < text.length(); ) { 416 int cp = text.codePointAt(i); 417 boolean inside = false; 418 for (Object range : ranges) { 419 List<?> pair = (List<?>) range; 420 if (Integer.parseInt((String) pair.get(0), 16) <= cp && cp <= Integer.parseInt((String) pair.get(1), 16)) { 421 inside = true; 422 break; 423 } 424 } 425 if (!inside) { 426 return false; 427 } 428 last = cp; 429 i += Character.charCount(cp); 430 } 431 Object excluded = profile.get("excludedLast"); 432 if (last >= 0 && excluded instanceof List<?>) { 433 for (Object code : (List<?>) excluded) { 434 if (Integer.parseInt((String) code, 16) == last) { 435 return false; 436 } 437 } 438 } 439 return true; 440 } 441 442 /** FORMAT.md, Section 5.1: the longest prefix of whole code points within the byte limit. */ 443 private String truncate(String key) { 444 Map<?, ?> truncation = map(rules.get("truncation")); 445 if (truncation.isEmpty()) { 446 return key; 447 } 448 if (!"UTF8_BYTES".equals(truncation.get("unit"))) { 449 throw new IllegalStateException("unknown truncation unit " + truncation.get("unit")); 450 } 451 long max = ((Number) truncation.get("maxLength")).longValue(); 452 long bytes = 0; 453 for (int i = 0; i < key.length(); ) { 454 int cp = key.codePointAt(i); 455 int length = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; 456 if (bytes + length > max) { 457 return key.substring(0, i); 458 } 459 bytes += length; 460 i += Character.charCount(cp); 461 } 462 return key; 463 } 464 465 private static String decodeBody(String body, String close, String escape) { 466 if ("DOUBLE_CLOSE".equals(escape)) { 467 return doubleClose(body, close); 468 } 469 if ("GOOGLESQL_BACKSLASH".equals(escape)) { 470 return googleSql(body, close); 471 } 472 if ("UNICODE_ESCAPE_BACKSLASH".equals(escape)) { 473 return unicodeEscape(body, close); 474 } 475 throw new IllegalStateException("unknown escape kind " + escape); 476 } 477 478 private static String doubleClose(String body, String close) { 479 StringBuilder out = new StringBuilder(body.length()); 480 int i = 0; 481 while (i < body.length()) { 482 if (body.startsWith(close, i)) { 483 if (i + close.length() == body.length()) { 484 return out.toString(); 485 } 486 if (!body.startsWith(close, i + close.length())) { 487 return null; 488 } 489 out.append(close); 490 i += 2 * close.length(); 491 } else { 492 int cp = body.codePointAt(i); 493 out.appendCodePoint(cp); 494 i += Character.charCount(cp); 495 } 496 } 497 return null; 498 } 499 500 private static String googleSql(String body, String close) { 501 StringBuilder out = new StringBuilder(body.length()); 502 int i = 0; 503 while (i < body.length()) { 504 if (body.startsWith(close, i)) { 505 return i + close.length() == body.length() ? out.toString() : null; 506 } 507 char c = body.charAt(i); 508 if (c != '\\') { 509 int cp = body.codePointAt(i); 510 out.appendCodePoint(cp); 511 i += Character.charCount(cp); 512 continue; 513 } 514 if (i + 1 >= body.length()) { 515 return null; 516 } 517 char e = body.charAt(i + 1); 518 switch (e) { 519 case 'a': out.append('\u0007'); i += 2; continue; 520 case 'b': out.append('\b'); i += 2; continue; 521 case 'f': out.append('\f'); i += 2; continue; 522 case 'n': out.append('\n'); i += 2; continue; 523 case 'r': out.append('\r'); i += 2; continue; 524 case 't': out.append('\t'); i += 2; continue; 525 case 'v': out.append('\u000B'); i += 2; continue; 526 case '\\': case '?': case '"': case '\'': case '`': out.append(e); i += 2; continue; 527 default: 528 break; 529 } 530 int value; 531 if (e == 'x' || e == 'X') { 532 value = hex(body, i + 2, 2); 533 i += 4; 534 } else if (e >= '0' && e <= '7') { 535 value = octal(body, i + 1); 536 i += 4; 537 } else if (e == 'u') { 538 value = hex(body, i + 2, 4); 539 i += 6; 540 } else if (e == 'U') { 541 value = hex(body, i + 2, 8); 542 i += 10; 543 } else { 544 return null; 545 } 546 if (value < 0 || value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) { 547 return null; 548 } 549 out.appendCodePoint(value); 550 } 551 return null; 552 } 553 554 private static String unicodeEscape(String body, String close) { 555 StringBuilder units = new StringBuilder(body.length()); 556 int i = 0; 557 while (i < body.length()) { 558 if (body.startsWith(close, i)) { 559 if (i + close.length() == body.length()) { 560 return isScalarValues(units.toString()) ? units.toString() : null; 561 } 562 if (!body.startsWith(close, i + close.length())) { 563 return null; 564 } 565 units.append(close); 566 i += 2 * close.length(); 567 continue; 568 } 569 char c = body.charAt(i); 570 if (c != '\\') { 571 int cp = body.codePointAt(i); 572 units.appendCodePoint(cp); 573 i += Character.charCount(cp); 574 continue; 575 } 576 if (body.startsWith("\\\\", i)) { 577 units.append('\\'); 578 i += 2; 579 continue; 580 } 581 if (body.startsWith("\\+", i)) { 582 int value = hex(body, i + 2, 6); 583 if (value < 0 || value > 0x10FFFF) { 584 return null; 585 } 586 if (value <= 0xFFFF) { 587 units.append((char) value); 588 } else { 589 units.appendCodePoint(value); 590 } 591 i += 8; 592 continue; 593 } 594 int unit = hex(body, i + 1, 4); 595 if (unit < 0) { 596 return null; 597 } 598 units.append((char) unit); 599 i += 5; 600 } 601 return null; 602 } 603 604 /** The value of {@code count} ASCII hex digits at {@code start}, or -1. */ 605 private static int hex(String s, int start, int count) { 606 if (start + count > s.length()) { 607 return -1; 608 } 609 long value = 0; 610 for (int j = start; j < start + count; j++) { 611 int d = Character.digit(s.charAt(j), 16); 612 if (d < 0 || s.charAt(j) > 'f') { 613 return -1; 614 } 615 value = value * 16 + d; 616 } 617 return value > Integer.MAX_VALUE ? -1 : (int) value; 618 } 619 620 /** The value of exactly three octal digits at {@code start}, at most 377 octal, or -1. */ 621 private static int octal(String s, int start) { 622 if (start + 3 > s.length()) { 623 return -1; 624 } 625 int value = 0; 626 for (int j = start; j < start + 3; j++) { 627 char c = s.charAt(j); 628 if (c < '0' || c > '7') { 629 return -1; 630 } 631 value = value * 8 + (c - '0'); 632 } 633 return value <= 0377 ? value : -1; 634 } 635 636 private static boolean isScalarValues(String s) { 637 for (int i = 0; i < s.length(); i++) { 638 char c = s.charAt(i); 639 if (Character.isHighSurrogate(c)) { 640 if (i + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(i + 1))) { 641 return false; 642 } 643 i++; 644 } else if (Character.isLowSurrogate(c)) { 645 return false; 646 } 647 } 648 return true; 649 } 650 651 private static Map<?, ?> map(Object value) { 652 return value instanceof Map<?, ?> ? (Map<?, ?>) value : Collections.emptyMap(); 653 } 654}