Enable Java 17 unicode blocks
[yangtools.git] / parser / yang-parser-rfc7950 / src / main / java / org / opendaylight / yangtools / yang / parser / rfc7950 / stmt / pattern / RegexUtils.java
1 /*
2  * Copyright (c) 2017 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.parser.rfc7950.stmt.pattern;
9
10 import com.google.common.collect.ImmutableSet;
11 import java.lang.Character.UnicodeBlock;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14 import java.util.regex.PatternSyntaxException;
15 import org.slf4j.Logger;
16 import org.slf4j.LoggerFactory;
17
18 /**
19  * Utilities for converting YANG XSD regexes into Java-compatible regexes.
20  */
21 final class RegexUtils {
22     private static final Logger LOG = LoggerFactory.getLogger(RegexUtils.class);
23     private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
24
25     /**
26      * Unicode blocks known to Java. We do not use {@link UnicodeBlock#forName(String)} due to the need to differentiate
27      * runtime-supported and compile-time supported blocks. We are limited to the latter, i.e. even if we are running
28      * on (for example) Java 17, we must rely only on blocks supported by our compilation target (for example) Java 11.
29      *
30      * <p>
31      * Furthermore we take a page from
32      * <a href="https://www.w3.org/TR/xmlschema11-2/#charcter-classes">G.4.2.3 Block escapes</a> and only match properly
33      * normalized names, which is different from what Java does.
34      */
35     private static final ImmutableSet<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
36         // Java 7 and earlier
37         .add("AegeanNumbers")
38         .add("AlchemicalSymbols")
39         .add("AlphabeticPresentationForms")
40         .add("AncientGreekMusicalNotation")
41         .add("AncientGreekNumbers")
42         .add("AncientSymbols")
43         .add("Arabic")
44         .add("ArabicPresentationForms-A")
45         .add("ArabicPresentationForms-B")
46         .add("ArabicSupplement")
47         .add("Armenian")
48         .add("Arrows")
49         .add("Avestan")
50         .add("Balinese")
51         .add("Bamum")
52         .add("BamumSupplement")
53         .add("BasicLatin")
54         .add("Batak")
55         .add("Bengali")
56         .add("BlockElements")
57         .add("Bopomofo")
58         .add("BopomofoExtended")
59         .add("BoxDrawing")
60         .add("Brahmi")
61         .add("BraillePatterns")
62         .add("Buginese")
63         .add("Buhid")
64         .add("ByzantineMusicalSymbols")
65         .add("Carian")
66         .add("Cham")
67         .add("Cherokee")
68         .add("CJKCompatibility")
69         .add("CJKCompatibilityForms")
70         .add("CJKCompatibilityIdeographs")
71         .add("CJKCompatibilityIdeographsSupplement")
72         .add("CJKRadicalsSupplement")
73         .add("CJKStrokes")
74         .add("CJKSymbolsandPunctuation")
75         .add("CJKUnifiedIdeographs")
76         .add("CJKUnifiedIdeographsExtensionA")
77         .add("CJKUnifiedIdeographsExtensionB")
78         .add("CJKUnifiedIdeographsExtensionC")
79         .add("CJKUnifiedIdeographsExtensionD")
80         .add("CombiningDiacriticalMarks")
81         .add("CombiningDiacriticalMarksSupplement")
82         .add("CombiningHalfMarks")
83         .add("CombiningDiacriticalMarksforSymbols")
84         .add("CommonIndicNumberForms")
85         .add("ControlPictures")
86         .add("Coptic")
87         .add("CountingRodNumerals")
88         .add("Cuneiform")
89         .add("CuneiformNumbersandPunctuation")
90         .add("CurrencySymbols")
91         .add("CypriotSyllabary")
92         .add("Cyrillic")
93         .add("CyrillicExtended-A")
94         .add("CyrillicExtended-B")
95         .add("CyrillicSupplementary")
96         .add("Deseret")
97         .add("Devanagari")
98         .add("DevanagariExtended")
99         .add("Dingbats")
100         .add("DominoTiles")
101         .add("EgyptianHieroglyphs")
102         .add("Emoticons")
103         .add("EnclosedAlphanumericSupplement")
104         .add("EnclosedAlphanumerics")
105         .add("EnclosedCJKLettersandMonths")
106         .add("EnclosedIdeographicSupplement")
107         .add("Ethiopic")
108         .add("EthiopicExtended")
109         .add("EthiopicExtended-A")
110         .add("EthiopicSupplement")
111         .add("GeneralPunctuation")
112         .add("GeometricShapes")
113         .add("Georgian")
114         .add("GeorgianSupplement")
115         .add("Glagolitic")
116         .add("Gothic")
117         .add("GreekandCoptic")
118         .add("GreekExtended")
119         .add("Gujarati")
120         .add("Gurmukhi")
121         .add("HalfwidthandFullwidthForms")
122         .add("HangulCompatibilityJamo")
123         .add("HangulJamo")
124         .add("HangulJamoExtended-A")
125         .add("HangulJamoExtended-B")
126         .add("HangulSyllables")
127         .add("Hanunoo")
128         .add("Hebrew")
129         .add("HighPrivateUseSurrogates")
130         .add("HighSurrogates")
131         .add("Hiragana")
132         .add("IdeographicDescriptionCharacters")
133         .add("ImperialAramaic")
134         .add("InscriptionalPahlavi")
135         .add("InscriptionalParthian")
136         .add("IPAExtensions")
137         .add("Javanese")
138         .add("Kaithi")
139         .add("KanaSupplement")
140         .add("Kanbun")
141         .add("Kangxi Radicals")
142         .add("Kannada")
143         .add("Katakana")
144         .add("KatakanaPhoneticExtensions")
145         .add("KayahLi")
146         .add("Kharoshthi")
147         .add("Khmer")
148         .add("KhmerSymbols")
149         .add("Lao")
150         .add("Latin-1Supplement")
151         .add("LatinExtended-A")
152         .add("LatinExtendedAdditional")
153         .add("LatinExtended-B")
154         .add("LatinExtended-C")
155         .add("LatinExtended-D")
156         .add("Lepcha")
157         .add("LetterlikeSymbols")
158         .add("Limbu")
159         .add("LinearBIdeograms")
160         .add("LinearBSyllabary")
161         .add("Lisu")
162         .add("LowSurrogates")
163         .add("Lycian")
164         .add("Lydian")
165         .add("MahjongTiles")
166         .add("Malayalam")
167         .add("Mandaic")
168         .add("MathematicalAlphanumericSymbols")
169         .add("MathematicalOperators")
170         .add("MeeteiMayek")
171         .add("MiscellaneousMathematicalSymbols-A")
172         .add("MiscellaneousMathematicalSymbols-B")
173         .add("MiscellaneousSymbols")
174         .add("MiscellaneousSymbolsandArrows")
175         .add("MiscellaneousSymbolsAndPictographs")
176         .add("MiscellaneousTechnical")
177         .add("ModifierToneLetters")
178         .add("Mongolian")
179         .add("MusicalSymbols")
180         .add("Myanmar")
181         .add("MyanmarExtended-A")
182         .add("NewTaiLue")
183         .add("NKo")
184         .add("NumberForms")
185         .add("Ogham")
186         .add("OlChiki")
187         .add("OldItalic")
188         .add("OldPersian")
189         .add("OldSouthArabian")
190         .add("OldTurkic")
191         .add("OpticalCharacterRecognition")
192         .add("Oriya")
193         .add("Osmanya")
194         .add("Phags-pa")
195         .add("PhaistosDisc")
196         .add("Phoenician")
197         .add("PhoneticExtensions")
198         .add("PhoneticExtensionsSupplement")
199         .add("PlayingCards")
200         .add("PrivateUseArea")
201         .add("Rejang")
202         .add("RumiNumeralSymbols")
203         .add("Runic")
204         .add("Samaritan")
205         .add("Saurashtra")
206         .add("Shavian")
207         .add("Sinhala")
208         .add("SmallFormVariants")
209         .add("SpacingModifierLetters")
210         .add("Specials")
211         .add("Sundanese")
212         .add("SuperscriptsandSubscripts")
213         .add("SupplementalArrows-A")
214         .add("SupplementalArrows-B")
215         .add("SupplementalMathematicalOperators")
216         .add("SupplementalPunctuation")
217         .add("SupplementaryPrivateUseArea-A")
218         .add("SupplementaryPrivateUseArea-B")
219         .add("SylotiNagri")
220         .add("Syriac")
221         .add("Tagalog")
222         .add("Tagbanwa")
223         .add("Tags")
224         .add("TaiLe")
225         .add("TaiTham")
226         .add("TaiViet")
227         .add("TaiXuanJingSymbols")
228         .add("Tamil")
229         .add("Telugu")
230         .add("Thaana")
231         .add("Thai")
232         .add("Tibetan")
233         .add("Tifinagh")
234         .add("TransportAndMapSymbols")
235         .add("Ugaritic")
236         .add("UnifiedCanadianAboriginalSyllabics")
237         .add("UnifiedCanadianAboriginalSyllabicsExtended")
238         .add("Vai")
239         .add("VariationSelectors")
240         .add("VariationSelectorsSupplement")
241         .add("VedicExtensions")
242         .add("VerticalForms")
243         .add("YiRadicals")
244         .add("YiSyllables")
245         .add("YijingHexagramSymbols")
246
247         // Java 8:
248         .add("ArabicExtended-A")
249         .add("ArabicMathematicalAlphabeticSymbols")
250         .add("Chakma")
251         .add("MeeteiMeyekExtensions")
252         .add("MeroiticCursive")
253         .add("MeroiticHieroglyphs")
254         .add("Miao")
255         .add("Sharada")
256         .add("SoraSompeng")
257         .add("SundaneseSupplement")
258         .add("Takri")
259
260         // Java 9:
261         .add("Ahom")
262         .add("AnatolianHieroglyphs")
263         .add("BassaVah")
264         .add("CaucasianAlbanian")
265         .add("CherokeeSupplement")
266         .add("CJKUnifiedIdeographsExtensionE")
267         .add("CombiningDiacriticalMarksExtended")
268         .add("CopticEpactNumbers")
269         .add("Duployan")
270         .add("EarlyDynasticCuneiform")
271         .add("Elbasan")
272         .add("GeometricShapesExtended")
273         .add("Grantha")
274         .add("Hatran")
275         .add("Khojki")
276         .add("Khudawadi")
277         .add("LatinExtended-E")
278         .add("LinearA")
279         .add("Mahajani")
280         .add("Manichaean")
281         .add("MendeKikakui")
282         .add("Modi")
283         .add("Mro")
284         .add("Multani")
285         .add("MyanmarExtended-B")
286         .add("Nabataean")
287         .add("OldHungarian")
288         .add("OldNorthArabian")
289         .add("OldPermic")
290         .add("OrnamentalDingbats")
291         .add("PahawhHmong")
292         .add("Palmyrene")
293         .add("PauCinHau")
294         .add("PsalterPahlavi")
295         .add("ShorthandFormatControls")
296         .add("Siddham")
297         .add("SinhalaArchaicNumbers")
298         .add("SupplementalArrows-C")
299         .add("SupplementalSymbolsandPictographs")
300         .add("SuttonSignWriting")
301         .add("Tirhuta")
302         .add("WarangCiti")
303
304         // Java 11
305         .add("Adlam")
306         .add("Bhaiksuki")
307         .add("CJKUnifiedIdeographsExtensionF")
308         .add("CyrillicExtended-C")
309         .add("GlagoliticSupplement")
310         .add("IdeographicSymbolsandPunctuation")
311         .add("KanaExtended-A")
312         .add("Marchen")
313         .add("MasaramGondi")
314         .add("MongolianSupplement")
315         .add("Newa")
316         .add("Nushu")
317         .add("Osage")
318         .add("Soyombo")
319         .add("SyriacSupplement")
320         .add("Tangut")
321         .add("TangutComponents")
322         .add("ZanabazarSquare")
323
324         // Java 12
325         .add("ChessSymbols")
326         .add("Dogra")
327         .add("GeorgianExtended")
328         .add("GunjalaGondi")
329         .add("HanifiRohingya")
330         .add("IndicSiyaqNumbers")
331         .add("Makasar")
332         .add("MayanNumerals")
333         .add("Medefaidrin")
334         .add("OldSogdian")
335         .add("Sogdian")
336
337         // Java 13
338         .add("EgyptianHieroglyphFormatControls")
339         .add("Elymaic")
340         .add("Nandinagari")
341         .add("NyiakengPuachueHmong")
342         .add("OttomanSiyaqNumbers")
343         .add("SmallKanaExtension")
344         .add("SymbolsandPictographsExtended-A")
345         .add("TamilSupplement")
346         .add("Wancho")
347         .build();
348
349     private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
350
351     private RegexUtils() {
352         // Hidden on purpose
353     }
354
355     /**
356      * Converts XSD regex to Java-compatible regex.
357      *
358      * @param xsdRegex XSD regex pattern as it is defined in a YANG source
359      * @return Java-compatible regex
360      */
361     static String getJavaRegexFromXSD(final String xsdRegex) {
362         // Note: we are using a non-capturing group to deal with internal structure issues, like branches and similar.
363         return "^(?:" + fixUnicodeScriptPattern(escapeChars(xsdRegex)) + ")$";
364     }
365
366     /*
367      * As both '^' and '$' are special anchor characters in java regular
368      * expressions which are implicitly present in XSD regular expressions,
369      * we need to escape them in case they are not defined as part of
370      * character ranges i.e. inside regular square brackets.
371      */
372     private static String escapeChars(final String regex) {
373         final StringBuilder result = new StringBuilder(regex.length());
374         int bracket = 0;
375         boolean escape = false;
376         for (int i = 0; i < regex.length(); i++) {
377             final char ch = regex.charAt(i);
378             switch (ch) {
379                 case '[':
380                     if (!escape) {
381                         bracket++;
382                     }
383                     escape = false;
384                     result.append(ch);
385                     break;
386                 case ']':
387                     if (!escape) {
388                         bracket--;
389                     }
390                     escape = false;
391                     result.append(ch);
392                     break;
393                 case '\\':
394                     escape = !escape;
395                     result.append(ch);
396                     break;
397                 case '^':
398                 case '$':
399                     if (bracket == 0) {
400                         result.append('\\');
401                     }
402                     escape = false;
403                     result.append(ch);
404                     break;
405                 default:
406                     escape = false;
407                     result.append(ch);
408             }
409         }
410         return result.toString();
411     }
412
413     private static String fixUnicodeScriptPattern(String rawPattern) {
414         for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
415             try {
416                 Pattern.compile(rawPattern);
417                 return rawPattern;
418             } catch (final PatternSyntaxException ex) {
419                 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
420                 final String msg = ex.getMessage();
421                 if (msg.startsWith("Unknown character script name")
422                         || msg.startsWith("Unknown character property name")) {
423                     rawPattern = fixUnknownScripts(msg, rawPattern);
424                 } else {
425                     return rawPattern;
426                 }
427             }
428         }
429
430         LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
431         return rawPattern;
432     }
433
434     private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
435         StringBuilder result = new StringBuilder(rawPattern);
436         final Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
437         if (matcher.find()) {
438             String capturedGroup = matcher.group(1);
439             if (capturedGroup.startsWith("In/Is")) {
440                 // Java 9 changed the reporting string
441                 capturedGroup = capturedGroup.substring(5);
442             } else if (capturedGroup.startsWith("Is")) {
443                 // Java 14 changed the reporting string (https://bugs.openjdk.java.net/browse/JDK-8230338)
444                 capturedGroup = capturedGroup.substring(2);
445             }
446
447             if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
448                 final int idx = rawPattern.indexOf("Is" + capturedGroup);
449                 result = result.replace(idx, idx + 2, "In");
450             }
451         }
452         return result.toString();
453     }
454 }