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}