Bug 6022: Deviation statement is not fully available in the YANG parser output
[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.ImmutableList;
17 import com.google.common.collect.ImmutableMap;
18 import com.google.common.collect.ImmutableMap.Builder;
19 import com.google.common.collect.ImmutableSet;
20 import com.google.common.collect.Iterables;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Date;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.regex.PatternSyntaxException;
32 import javax.annotation.Nullable;
33 import javax.xml.xpath.XPath;
34 import javax.xml.xpath.XPathExpressionException;
35 import javax.xml.xpath.XPathFactory;
36 import org.antlr.v4.runtime.tree.TerminalNode;
37 import org.opendaylight.yangtools.antlrv4.code.gen.YangStatementParser;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.common.QNameModule;
40 import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
41 import org.opendaylight.yangtools.yang.model.api.DeviateKind;
42 import org.opendaylight.yangtools.yang.model.api.ModuleIdentifier;
43 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
44 import org.opendaylight.yangtools.yang.model.api.Rfc6020Mapping;
45 import org.opendaylight.yangtools.yang.model.api.Status;
46 import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
47 import org.opendaylight.yangtools.yang.model.api.stmt.BelongsToStatement;
48 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleStatement;
49 import org.opendaylight.yangtools.yang.model.api.stmt.RevisionStatement;
50 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
51 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Relative;
52 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleStatement;
53 import org.opendaylight.yangtools.yang.model.util.RevisionAwareXPathImpl;
54 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
55 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
56 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
57 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
58 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
60 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
61 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
62 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
63 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
64 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
65 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 public final class Utils {
70     private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
71     private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
72     private static final CharMatcher DOUBLE_QUOTE_MATCHER = CharMatcher.is('"');
73     private static final CharMatcher SINGLE_QUOTE_MATCHER = CharMatcher.is('\'');
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         Builder<String, DeviateKind> keywordToDeviateMapBuilder = ImmutableMap.builder();
297         for (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         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         Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
333
334         for (String keyToken : keyTokens) {
335
336             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 (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 (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         String prefixedLocalName = identifier.getLocalName();
378         String[] namesParts = prefixedLocalName.split(":");
379
380         if (namesParts.length == 2) {
381             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         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         if (stmtDef.get(identifier) != null) {
411             return stmtDef.get(identifier).getStatementName();
412         } else {
413             String prefixedLocalName = identifier.getLocalName();
414             String[] namesParts = prefixedLocalName.split(":");
415
416             if (namesParts.length == 2) {
417                 String prefix = namesParts[0];
418                 String localName = namesParts[1];
419
420                 if (prefixes == null) {
421                     return null;
422                 }
423
424                 QNameModule qNameModule = prefixes.get(prefix);
425                 if (qNameModule == null) {
426                     return null;
427                 }
428
429                 if (prefixes.isPreLinkageMap()) {
430                     StatementDefinition foundStmtDef = stmtDef.getByNamespaceAndLocalName(qNameModule.getNamespace(),
431                             localName);
432                     return foundStmtDef != null ? foundStmtDef.getStatementName() : null;
433                 } else {
434                     QName qName = QName.create(qNameModule, localName);
435                     return stmtDef.get(qName) != null ? qName : null;
436                 }
437             }
438         }
439         return null;
440     }
441
442     static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
443         // FIXME: is the path trimming really necessary??
444         final List<QName> qNames = new ArrayList<>();
445         for (String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
446             try {
447                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
448                 qNames.add(qName);
449             } catch (Exception e) {
450                 throw new IllegalArgumentException(
451                     String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
452             }
453         }
454
455         return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
456     }
457
458     public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
459         StringBuilder sb = new StringBuilder();
460         List<TerminalNode> strings = context.STRING();
461         if (strings.isEmpty()) {
462             strings = Collections.singletonList(context.IDENTIFIER());
463         }
464         for (TerminalNode stringNode : strings) {
465             final String str = stringNode.getText();
466             char firstChar = str.charAt(0);
467             final CharMatcher quoteMatcher;
468             if (SINGLE_QUOTE_MATCHER.matches(firstChar)) {
469                 quoteMatcher = SINGLE_QUOTE_MATCHER;
470             } else if (DOUBLE_QUOTE_MATCHER.matches(firstChar)) {
471                 quoteMatcher = DOUBLE_QUOTE_MATCHER;
472             } else {
473                 sb.append(str);
474                 continue;
475             }
476             sb.append(quoteMatcher.removeFrom(str.substring(1, str.length() - 1)));
477         }
478         return sb.toString();
479     }
480
481     public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
482         if (Strings.isNullOrEmpty(value)) {
483             return ctx.getPublicDefinition().getStatementName();
484         }
485
486         String prefix;
487         QNameModule qNameModule = null;
488         String localName = null;
489
490         String[] namesParts = value.split(":");
491         switch (namesParts.length) {
492         case 1:
493             localName = namesParts[0];
494             qNameModule = getRootModuleQName(ctx);
495             break;
496         default:
497             prefix = namesParts[0];
498             localName = namesParts[1];
499             qNameModule = getModuleQNameByPrefix(ctx, prefix);
500             // in case of unknown statement argument, we're not going to parse it
501             if (qNameModule == null
502                     && ctx.getPublicDefinition().getDeclaredRepresentationClass()
503                     .isAssignableFrom(UnknownStatementImpl.class)) {
504                 localName = value;
505                 qNameModule = getRootModuleQName(ctx);
506             }
507             if (qNameModule == null
508                     && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
509                 ctx = ctx.getOriginalCtx();
510                 qNameModule = getModuleQNameByPrefix(ctx, prefix);
511             }
512             break;
513         }
514
515         Preconditions.checkArgument(qNameModule != null,
516                 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
517                 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
518         final QNameModule resultQNameModule;
519         if (qNameModule.getRevision() == null) {
520             resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
521                 .intern();
522         } else {
523             resultQNameModule = qNameModule;
524         }
525
526         return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
527     }
528
529     public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
530         final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
531         final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
532
533         if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
534             String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
535             return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
536         }
537         return qNameModule;
538     }
539
540     public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
541         if (ctx == null) {
542             return null;
543         }
544
545         final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
546         final QNameModule qNameModule;
547
548         if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
549             qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
550         } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
551             final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), BelongsToStatement.class);
552             qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
553         } else {
554             qNameModule = null;
555         }
556
557         Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
558         if (qNameModule.getRevision() != null) {
559             return qNameModule;
560         }
561
562         return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
563     }
564
565     @Nullable
566     public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
567             final SchemaNodeIdentifier node) {
568         return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
569     }
570
571     public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
572         return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
573                 .isAssignableFrom(UnknownStatementImpl.class);
574     }
575
576     public static DeviateKind parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
577         return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
578                 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
579                 ctx.getStatementSourceReference());
580     }
581
582     public static Status parseStatus(final String value) {
583         switch (value) {
584         case "current":
585             return Status.CURRENT;
586         case "deprecated":
587             return Status.DEPRECATED;
588         case "obsolete":
589             return Status.OBSOLETE;
590         default:
591             LOG.warn("Invalid 'status' statement: {}", value);
592             return null;
593         }
594     }
595
596     public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
597         Date revision = null;
598         for (StmtContext<?, ?, ?> subStmt : subStmts) {
599             if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
600                     .class)) {
601                 if (revision == null && subStmt.getStatementArgument() != null) {
602                     revision = (Date) subStmt.getStatementArgument();
603                 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
604                         (revision) > 0) {
605                     revision = (Date) subStmt.getStatementArgument();
606                 }
607             }
608         }
609         return revision;
610     }
611
612     public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
613         return (o instanceof ModuleIdentifier)
614                 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
615                         .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
616     }
617
618     /**
619      * Replaces illegal characters of QName by the name of the character (e.g.
620      * '?' is replaced by "QuestionMark" etc.).
621      *
622      * @param string
623      *            input String
624      * @return result String
625      */
626     public static String replaceIllegalCharsForQName(String string) {
627         string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
628         string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
629         string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
630         string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
631
632         return string;
633     }
634
635     public static String fixUnicodeScriptPattern(String rawPattern) {
636         for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
637             try {
638                 Pattern.compile(rawPattern);
639                 return rawPattern;
640             } catch(PatternSyntaxException ex) {
641                 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
642                 if (ex.getMessage().contains("Unknown character script name")) {
643                     rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
644                 } else {
645                     return rawPattern;
646                 }
647             }
648         }
649
650         LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
651         return rawPattern;
652     }
653
654     private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
655         StringBuilder result = new StringBuilder(rawPattern);
656         Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
657         if (matcher.find()) {
658             String capturedGroup = matcher.group(1);
659             if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
660                 int idx = rawPattern.indexOf("Is" + capturedGroup);
661                 result = result.replace(idx, idx + 2, "In");
662             }
663         }
664         return result.toString();
665     }
666
667     public static boolean belongsToTheSameModule(QName targetStmtQName, QName sourceStmtQName) {
668         if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
669             return true;
670         }
671         return false;
672     }
673
674     public static boolean isPresenceContainer(StatementContextBase<?, ?, ?> targetCtx) {
675         if (!targetCtx.getPublicDefinition().equals(Rfc6020Mapping.CONTAINER)) {
676             return false;
677         }
678
679         final List<StatementContextBase<?, ?, ?>> targetSubStatements = new ImmutableList.Builder<StatementContextBase<?, ?, ?>>()
680                 .addAll(targetCtx.declaredSubstatements()).addAll(targetCtx.effectiveSubstatements()).build();
681         for (final StatementContextBase<?, ?, ?> subStatement : targetSubStatements) {
682             if (subStatement.getPublicDefinition().equals(Rfc6020Mapping.PRESENCE)) {
683                 return true;
684             }
685         }
686
687         return false;
688     }
689 }