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