716a48fa9a391ecad6cb3e51f4dfd60147900e59
[yangtools.git] / yang / yang-parser-impl / src / main / java / org / opendaylight / yangtools / yang / parser / stmt / rfc6020 / Utils.java
1 /*
2  * Copyright (c) 2015 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.stmt.rfc6020;
9
10 import static org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils.firstAttributeOf;
11
12 import com.google.common.base.CharMatcher;
13 import com.google.common.base.Preconditions;
14 import com.google.common.base.Splitter;
15 import com.google.common.base.Strings;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.ImmutableMap.Builder;
18 import com.google.common.collect.ImmutableSet;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.Set;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.regex.PatternSyntaxException;
31 import javax.annotation.Nullable;
32 import javax.xml.xpath.XPath;
33 import javax.xml.xpath.XPathExpressionException;
34 import javax.xml.xpath.XPathFactory;
35 import org.antlr.v4.runtime.tree.TerminalNode;
36 import org.opendaylight.yangtools.antlrv4.code.gen.YangStatementParser;
37 import org.opendaylight.yangtools.yang.common.QName;
38 import org.opendaylight.yangtools.yang.common.QNameModule;
39 import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
40 import org.opendaylight.yangtools.yang.model.api.DeviateKind;
41 import org.opendaylight.yangtools.yang.model.api.ModuleIdentifier;
42 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
43 import org.opendaylight.yangtools.yang.model.api.Status;
44 import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
45 import org.opendaylight.yangtools.yang.model.api.stmt.BelongsToStatement;
46 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleStatement;
47 import org.opendaylight.yangtools.yang.model.api.stmt.RevisionStatement;
48 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
49 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Relative;
50 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleStatement;
51 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
52 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
53 import org.opendaylight.yangtools.yang.model.util.RevisionAwareXPathImpl;
54 import org.opendaylight.yangtools.yang.parser.spi.meta.CopyType;
55 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
56 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
57 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
58 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
60 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
61 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
62 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
63 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
64 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
65 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
66 import org.opendaylight.yangtools.yang.parser.stmt.reactor.RootStatementContext;
67 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 public final class Utils {
72     private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
73     private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
74     private static final CharMatcher LEFT_PARENTHESIS_MATCHER = CharMatcher.is('(');
75     private static final CharMatcher RIGHT_PARENTHESIS_MATCHER = CharMatcher.is(')');
76     private static final CharMatcher AMPERSAND_MATCHER = CharMatcher.is('&');
77     private static final CharMatcher QUESTION_MARK_MATCHER = CharMatcher.is('?');
78     private static final Splitter SLASH_SPLITTER = Splitter.on('/').omitEmptyStrings().trimResults();
79     private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
80     private static final Splitter COLON_SPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
81     private static final Pattern PATH_ABS = Pattern.compile("/[^/].*");
82     private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
83     private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
84             .add("AegeanNumbers")
85             .add("AlchemicalSymbols")
86             .add("AlphabeticPresentationForms")
87             .add("AncientGreekMusicalNotation")
88             .add("AncientGreekNumbers")
89             .add("AncientSymbols")
90             .add("Arabic")
91             .add("ArabicPresentationForms-A")
92             .add("ArabicPresentationForms-B")
93             .add("ArabicSupplement")
94             .add("Armenian")
95             .add("Arrows")
96             .add("Avestan")
97             .add("Balinese")
98             .add("Bamum")
99             .add("BamumSupplement")
100             .add("BasicLatin")
101             .add("Batak")
102             .add("Bengali")
103             .add("BlockElements")
104             .add("Bopomofo")
105             .add("BopomofoExtended")
106             .add("BoxDrawing")
107             .add("Brahmi")
108             .add("BraillePatterns")
109             .add("Buginese")
110             .add("Buhid")
111             .add("ByzantineMusicalSymbols")
112             .add("Carian")
113             .add("Cham")
114             .add("Cherokee")
115             .add("CJKCompatibility")
116             .add("CJKCompatibilityForms")
117             .add("CJKCompatibilityIdeographs")
118             .add("CJKCompatibilityIdeographsSupplement")
119             .add("CJKRadicalsSupplement")
120             .add("CJKStrokes")
121             .add("CJKSymbolsandPunctuation")
122             .add("CJKUnifiedIdeographs")
123             .add("CJKUnifiedIdeographsExtensionA")
124             .add("CJKUnifiedIdeographsExtensionB")
125             .add("CJKUnifiedIdeographsExtensionC")
126             .add("CJKUnifiedIdeographsExtensionD")
127             .add("CombiningDiacriticalMarks")
128             .add("CombiningDiacriticalMarksSupplement")
129             .add("CombiningHalfMarks")
130             .add("CombiningDiacriticalMarksforSymbols")
131             .add("CommonIndicNumberForms")
132             .add("ControlPictures")
133             .add("Coptic")
134             .add("CountingRodNumerals")
135             .add("Cuneiform")
136             .add("CuneiformNumbersandPunctuation")
137             .add("CurrencySymbols")
138             .add("CypriotSyllabary")
139             .add("Cyrillic")
140             .add("CyrillicExtended-A")
141             .add("CyrillicExtended-B")
142             .add("CyrillicSupplementary")
143             .add("Deseret")
144             .add("Devanagari")
145             .add("DevanagariExtended")
146             .add("Dingbats")
147             .add("DominoTiles")
148             .add("EgyptianHieroglyphs")
149             .add("Emoticons")
150             .add("EnclosedAlphanumericSupplement")
151             .add("EnclosedAlphanumerics")
152             .add("EnclosedCJKLettersandMonths")
153             .add("EnclosedIdeographicSupplement")
154             .add("Ethiopic")
155             .add("EthiopicExtended")
156             .add("EthiopicExtended-A")
157             .add("EthiopicSupplement")
158             .add("GeneralPunctuation")
159             .add("GeometricShapes")
160             .add("Georgian")
161             .add("GeorgianSupplement")
162             .add("Glagolitic")
163             .add("Gothic")
164             .add("GreekandCoptic")
165             .add("GreekExtended")
166             .add("Gujarati")
167             .add("Gurmukhi")
168             .add("HalfwidthandFullwidthForms")
169             .add("HangulCompatibilityJamo")
170             .add("HangulJamo")
171             .add("HangulJamoExtended-A")
172             .add("HangulJamoExtended-B")
173             .add("HangulSyllables")
174             .add("Hanunoo")
175             .add("Hebrew")
176             .add("HighPrivateUseSurrogates")
177             .add("HighSurrogates")
178             .add("Hiragana")
179             .add("IdeographicDescriptionCharacters")
180             .add("ImperialAramaic")
181             .add("InscriptionalPahlavi")
182             .add("InscriptionalParthian")
183             .add("IPAExtensions")
184             .add("Javanese")
185             .add("Kaithi")
186             .add("KanaSupplement")
187             .add("Kanbun")
188             .add("Kangxi Radicals")
189             .add("Kannada")
190             .add("Katakana")
191             .add("KatakanaPhoneticExtensions")
192             .add("KayahLi")
193             .add("Kharoshthi")
194             .add("Khmer")
195             .add("KhmerSymbols")
196             .add("Lao")
197             .add("Latin-1Supplement")
198             .add("LatinExtended-A")
199             .add("LatinExtendedAdditional")
200             .add("LatinExtended-B")
201             .add("LatinExtended-C")
202             .add("LatinExtended-D")
203             .add("Lepcha")
204             .add("LetterlikeSymbols")
205             .add("Limbu")
206             .add("LinearBIdeograms")
207             .add("LinearBSyllabary")
208             .add("Lisu")
209             .add("LowSurrogates")
210             .add("Lycian")
211             .add("Lydian")
212             .add("MahjongTiles")
213             .add("Malayalam")
214             .add("Mandaic")
215             .add("MathematicalAlphanumericSymbols")
216             .add("MathematicalOperators")
217             .add("MeeteiMayek")
218             .add("MiscellaneousMathematicalSymbols-A")
219             .add("MiscellaneousMathematicalSymbols-B")
220             .add("MiscellaneousSymbols")
221             .add("MiscellaneousSymbolsandArrows")
222             .add("MiscellaneousSymbolsAndPictographs")
223             .add("MiscellaneousTechnical")
224             .add("ModifierToneLetters")
225             .add("Mongolian")
226             .add("MusicalSymbols")
227             .add("Myanmar")
228             .add("MyanmarExtended-A")
229             .add("NewTaiLue")
230             .add("NKo")
231             .add("NumberForms")
232             .add("Ogham")
233             .add("OlChiki")
234             .add("OldItalic")
235             .add("OldPersian")
236             .add("OldSouthArabian")
237             .add("OldTurkic")
238             .add("OpticalCharacterRecognition")
239             .add("Oriya")
240             .add("Osmanya")
241             .add("Phags-pa")
242             .add("PhaistosDisc")
243             .add("Phoenician")
244             .add("PhoneticExtensions")
245             .add("PhoneticExtensionsSupplement")
246             .add("PlayingCards")
247             .add("PrivateUseArea")
248             .add("Rejang")
249             .add("RumiNumeralSymbols")
250             .add("Runic")
251             .add("Samaritan")
252             .add("Saurashtra")
253             .add("Shavian")
254             .add("Sinhala")
255             .add("SmallFormVariants")
256             .add("SpacingModifierLetters")
257             .add("Specials")
258             .add("Sundanese")
259             .add("SuperscriptsandSubscripts")
260             .add("SupplementalArrows-A")
261             .add("SupplementalArrows-B")
262             .add("SupplementalMathematicalOperators")
263             .add("SupplementalPunctuation")
264             .add("SupplementaryPrivateUseArea-A")
265             .add("SupplementaryPrivateUseArea-B")
266             .add("SylotiNagri")
267             .add("Syriac")
268             .add("Tagalog")
269             .add("Tagbanwa")
270             .add("Tags")
271             .add("TaiLe")
272             .add("TaiTham")
273             .add("TaiViet")
274             .add("TaiXuanJingSymbols")
275             .add("Tamil")
276             .add("Telugu")
277             .add("Thaana")
278             .add("Thai")
279             .add("Tibetan")
280             .add("Tifinagh")
281             .add("TransportAndMapSymbols")
282             .add("Ugaritic")
283             .add("UnifiedCanadianAboriginalSyllabics")
284             .add("UnifiedCanadianAboriginalSyllabicsExtended")
285             .add("Vai")
286             .add("VariationSelectors")
287             .add("VariationSelectorsSupplement")
288             .add("VedicExtensions")
289             .add("VerticalForms")
290             .add("YiRadicals")
291             .add("YiSyllables")
292             .add("YijingHexagramSymbols").build();
293
294     private static final Map<String, DeviateKind> KEYWORD_TO_DEVIATE_MAP;
295     static {
296         final Builder<String, DeviateKind> keywordToDeviateMapBuilder = ImmutableMap.builder();
297         for (final DeviateKind deviate : DeviateKind.values()) {
298             keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
299         }
300         KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
301     }
302
303     private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
304         @Override
305         protected XPathFactory initialValue() {
306             return XPathFactory.newInstance();
307         }
308     };
309
310     private Utils() {
311         throw new UnsupportedOperationException();
312     }
313
314     /**
315      * Cleanup any resources attached to the current thread. Threads interacting with this class can cause thread-local
316      * caches to them. Invoke this method if you want to detach those resources.
317      */
318     public static void detachFromCurrentThread() {
319         XPATH_FACTORY.remove();
320     }
321
322     public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
323             final String value) {
324         final List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
325
326         // to detect if key contains duplicates
327         if ((new HashSet<>(keyTokens)).size() < keyTokens.size()) {
328             // FIXME: report all duplicate keys
329             throw new SourceException(ctx.getStatementSourceReference(), "Duplicate value in list key: %s", value);
330         }
331
332         final Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
333
334         for (final String keyToken : keyTokens) {
335
336             final SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
337                     Utils.qNameFromArgument(ctx, keyToken));
338             keyNodes.add(keyNode);
339         }
340
341         return keyNodes;
342     }
343
344     static Collection<SchemaNodeIdentifier.Relative> parseUniqueConstraintArgument(final StmtContext<?, ?, ?> ctx,
345             final String argumentValue) {
346         final Set<SchemaNodeIdentifier.Relative> uniqueConstraintNodes = new HashSet<>();
347         for (final String uniqueArgToken : SPACE_SPLITTER.split(argumentValue)) {
348             final SchemaNodeIdentifier nodeIdentifier = Utils.nodeIdentifierFromPath(ctx, uniqueArgToken);
349             SourceException.throwIf(nodeIdentifier.isAbsolute(), ctx.getStatementSourceReference(),
350                     "Unique statement argument '%s' contains schema node identifier '%s' "
351                             + "which is not in the descendant node identifier form.", argumentValue, uniqueArgToken);
352             uniqueConstraintNodes.add((SchemaNodeIdentifier.Relative) nodeIdentifier);
353         }
354         return ImmutableSet.copyOf(uniqueConstraintNodes);
355     }
356
357     private static String trimSingleLastSlashFromXPath(final String path) {
358         return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
359     }
360
361     static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
362         final XPath xPath = XPATH_FACTORY.get().newXPath();
363         xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
364
365         final String trimmed = trimSingleLastSlashFromXPath(path);
366         try {
367             // TODO: we could capture the result and expose its 'evaluate' method
368             xPath.compile(trimmed);
369         } catch (final XPathExpressionException e) {
370             LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
371         }
372
373         return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
374     }
375
376     public static QName trimPrefix(final QName identifier) {
377         final String prefixedLocalName = identifier.getLocalName();
378         final String[] namesParts = prefixedLocalName.split(":");
379
380         if (namesParts.length == 2) {
381             final String localName = namesParts[1];
382             return QName.create(identifier.getModule(), localName);
383         }
384
385         return identifier;
386     }
387
388     public static String trimPrefix(final String identifier) {
389         final List<String> namesParts = COLON_SPLITTER.splitToList(identifier);
390         if (namesParts.size() == 2) {
391             return namesParts.get(1);
392         }
393         return identifier;
394     }
395
396     /**
397      *
398      * Based on identifier read from source and collections of relevant prefixes and statement definitions mappings
399      * provided for actual phase, method resolves and returns valid QName for declared statement to be written.
400      * This applies to any declared statement, including unknown statements.
401      *
402      * @param prefixes - collection of all relevant prefix mappings supplied for actual parsing phase
403      * @param stmtDef - collection of all relevant statement definition mappings provided for actual parsing phase
404      * @param identifier - statement to parse from source
405      * @return valid QName for declared statement to be written
406      *
407      */
408     public static QName getValidStatementDefinition(final PrefixToModule prefixes,
409             final QNameToStatementDefinition stmtDef, final QName identifier) {
410         final StatementDefinition def = stmtDef.get(identifier);
411         if (def != null) {
412             return def.getStatementName();
413         }
414         if (prefixes == null) {
415             return null;
416         }
417
418         final String prefixedLocalName = identifier.getLocalName();
419         final String[] namesParts = prefixedLocalName.split(":");
420         if (namesParts.length != 2) {
421             return null;
422         }
423
424         final String prefix = namesParts[0];
425         final String localName = namesParts[1];
426         final QNameModule qNameModule = prefixes.get(prefix);
427         if (qNameModule == null) {
428             return null;
429         }
430
431         if (prefixes.isPreLinkageMap()) {
432             final StatementDefinition foundStmtDef = stmtDef.getByNamespaceAndLocalName(qNameModule.getNamespace(),
433                 localName);
434             return foundStmtDef != null ? foundStmtDef.getStatementName() : null;
435         }
436
437         final QName qName = QName.create(qNameModule, localName);
438         return stmtDef.get(qName) != null ? qName : null;
439     }
440
441     static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
442         // FIXME: is the path trimming really necessary??
443         final List<QName> qNames = new ArrayList<>();
444         for (final String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
445             try {
446                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
447                 qNames.add(qName);
448             } catch (final Exception e) {
449                 throw new IllegalArgumentException(
450                     String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
451             }
452         }
453
454         return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
455     }
456
457     public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
458         final StringBuilder sb = new StringBuilder();
459         List<TerminalNode> strings = context.STRING();
460         if (strings.isEmpty()) {
461             strings = Collections.singletonList(context.IDENTIFIER());
462         }
463         for (final TerminalNode stringNode : strings) {
464             final String str = stringNode.getText();
465             final char firstChar = str.charAt(0);
466             final char lastChar = str.charAt(str.length() - 1);
467             if (firstChar == '"' && lastChar == '"') {
468                 final String innerStr = str.substring(1, str.length() - 1);
469                 /*
470                  * Unescape escaped double quotes, tabs, new line and backslash
471                  * in the inner string and trim the result.
472                  */
473                 sb.append(innerStr.replace("\\\"", "\"").replace("\\\\", "\\").replace("\\n", "\n")
474                         .replace("\\t", "\t"));
475             } else if (firstChar == '\'' && lastChar == '\'') {
476                 /*
477                  * According to RFC6020 a single quote character cannot occur in
478                  * a single-quoted string, even when preceded by a backslash.
479                  */
480                 sb.append(str.substring(1, str.length() - 1));
481             } else {
482                 sb.append(str);
483             }
484         }
485         return sb.toString();
486     }
487
488     public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
489         if (Strings.isNullOrEmpty(value)) {
490             return ctx.getPublicDefinition().getStatementName();
491         }
492
493         String prefix;
494         QNameModule qNameModule = null;
495         String localName = null;
496
497         final String[] namesParts = value.split(":");
498         switch (namesParts.length) {
499         case 1:
500             localName = namesParts[0];
501             qNameModule = getRootModuleQName(ctx);
502             break;
503         default:
504             prefix = namesParts[0];
505             localName = namesParts[1];
506             qNameModule = getModuleQNameByPrefix(ctx, prefix);
507             // in case of unknown statement argument, we're not going to parse it
508             if (qNameModule == null
509                     && ctx.getPublicDefinition().getDeclaredRepresentationClass()
510                     .isAssignableFrom(UnknownStatementImpl.class)) {
511                 localName = value;
512                 qNameModule = getRootModuleQName(ctx);
513             }
514             if (qNameModule == null
515                     && ctx.getCopyHistory().getLastOperation() == CopyType.ADDED_BY_AUGMENTATION) {
516                 ctx = ctx.getOriginalCtx();
517                 qNameModule = getModuleQNameByPrefix(ctx, prefix);
518             }
519             break;
520         }
521
522         Preconditions.checkArgument(qNameModule != null,
523                 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
524                 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
525         final QNameModule resultQNameModule;
526         if (qNameModule.getRevision() == null) {
527             resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
528                 .intern();
529         } else {
530             resultQNameModule = qNameModule;
531         }
532
533         return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
534     }
535
536     public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
537         final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
538         final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
539
540         if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
541             final String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
542             return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
543         }
544         return qNameModule;
545     }
546
547     public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
548         if (ctx == null) {
549             return null;
550         }
551
552         final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
553         final QNameModule qNameModule;
554
555         if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
556             qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
557         } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
558             final String belongsToModuleName = firstAttributeOf(rootCtx.declaredSubstatements(),
559                 BelongsToStatement.class);
560             qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
561         } else {
562             qNameModule = null;
563         }
564
565         Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
566         if (qNameModule.getRevision() != null) {
567             return qNameModule;
568         }
569
570         return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
571     }
572
573     @Nullable
574     public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
575             final SchemaNodeIdentifier node) {
576         return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
577     }
578
579     public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
580         return stmtCtx != null && stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
581                 .isAssignableFrom(UnknownStatementImpl.class);
582     }
583
584     public static DeviateKind parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
585         return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
586                 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
587                 ctx.getStatementSourceReference());
588     }
589
590     public static Status parseStatus(final String value) {
591         switch (value) {
592         case "current":
593             return Status.CURRENT;
594         case "deprecated":
595             return Status.DEPRECATED;
596         case "obsolete":
597             return Status.OBSOLETE;
598         default:
599             LOG.warn("Invalid 'status' statement: {}", value);
600             return null;
601         }
602     }
603
604     public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
605         Date revision = null;
606         for (final StmtContext<?, ?, ?> subStmt : subStmts) {
607             if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
608                     .class)) {
609                 if (revision == null && subStmt.getStatementArgument() != null) {
610                     revision = (Date) subStmt.getStatementArgument();
611                 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
612                         (revision) > 0) {
613                     revision = (Date) subStmt.getStatementArgument();
614                 }
615             }
616         }
617         return revision;
618     }
619
620     public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
621         return (o instanceof ModuleIdentifier)
622                 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
623                         .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
624     }
625
626     /**
627      * Replaces illegal characters of QName by the name of the character (e.g.
628      * '?' is replaced by "QuestionMark" etc.).
629      *
630      * @param string
631      *            input String
632      * @return result String
633      */
634     public static String replaceIllegalCharsForQName(String string) {
635         string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
636         string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
637         string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
638         string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
639
640         return string;
641     }
642
643     public static String fixUnicodeScriptPattern(String rawPattern) {
644         for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
645             try {
646                 Pattern.compile(rawPattern);
647                 return rawPattern;
648             } catch(final PatternSyntaxException ex) {
649                 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
650                 if (ex.getMessage().contains("Unknown character script name")) {
651                     rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
652                 } else {
653                     return rawPattern;
654                 }
655             }
656         }
657
658         LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
659         return rawPattern;
660     }
661
662     private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
663         StringBuilder result = new StringBuilder(rawPattern);
664         final Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
665         if (matcher.find()) {
666             final String capturedGroup = matcher.group(1);
667             if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
668                 final int idx = rawPattern.indexOf("Is" + capturedGroup);
669                 result = result.replace(idx, idx + 2, "In");
670             }
671         }
672         return result.toString();
673     }
674
675     public static boolean belongsToTheSameModule(final QName targetStmtQName, final QName sourceStmtQName) {
676         if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
677             return true;
678         }
679         return false;
680     }
681
682     public static SourceIdentifier createSourceIdentifier(final RootStatementContext<?, ?, ?> root) {
683         final QNameModule qNameModule = root.getFromNamespace(ModuleCtxToModuleQName.class, root);
684         if (qNameModule != null) {
685             // creates SourceIdentifier for a module
686             return RevisionSourceIdentifier.create((String) root.getStatementArgument(),
687                 qNameModule.getFormattedRevision());
688         }
689
690         // creates SourceIdentifier for a submodule
691         final Date revision = Optional.ofNullable(Utils.getLatestRevision(root.declaredSubstatements()))
692                 .orElse(SimpleDateFormatUtil.DEFAULT_DATE_REV);
693         final String formattedRevision = SimpleDateFormatUtil.getRevisionFormat().format(revision);
694         return RevisionSourceIdentifier.create((String) root.getStatementArgument(), formattedRevision);
695     }
696 }