Bug 6669: Mandatory nodes cannot be added to node from another module via augment
[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.ImmutableMap;
18 import com.google.common.collect.ImmutableMap.Builder;
19 import com.google.common.collect.ImmutableSet;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
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.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.meta.CopyType;
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         if (stmtDef.get(identifier) != null) {
411             return stmtDef.get(identifier).getStatementName();
412         } else {
413             final String prefixedLocalName = identifier.getLocalName();
414             final String[] namesParts = prefixedLocalName.split(":");
415
416             if (namesParts.length == 2) {
417                 final String prefix = namesParts[0];
418                 final String localName = namesParts[1];
419
420                 if (prefixes == null) {
421                     return null;
422                 }
423
424                 final QNameModule qNameModule = prefixes.get(prefix);
425                 if (qNameModule == null) {
426                     return null;
427                 }
428
429                 if (prefixes.isPreLinkageMap()) {
430                     final StatementDefinition foundStmtDef = stmtDef.getByNamespaceAndLocalName(qNameModule.getNamespace(),
431                             localName);
432                     return foundStmtDef != null ? foundStmtDef.getStatementName() : null;
433                 } else {
434                     final 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 (final String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
446             try {
447                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
448                 qNames.add(qName);
449             } catch (final 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         final StringBuilder sb = new StringBuilder();
460         List<TerminalNode> strings = context.STRING();
461         if (strings.isEmpty()) {
462             strings = Collections.singletonList(context.IDENTIFIER());
463         }
464         for (final TerminalNode stringNode : strings) {
465             final String str = stringNode.getText();
466             final char firstChar = str.charAt(0);
467             final char lastChar = str.charAt(str.length() - 1);
468             if (firstChar == '"' && lastChar == '"') {
469                 final String innerStr = str.substring(1, str.length() - 1);
470                 /*
471                  * Unescape escaped double quotes, tabs, new line and backslash
472                  * in the inner string and trim the result.
473                  */
474                 sb.append(innerStr.replace("\\\"", "\"").replace("\\\\", "\\").replace("\\n", "\n")
475                         .replace("\\t", "\t"));
476             } else if (firstChar == '\'' && lastChar == '\'') {
477                 /*
478                  * According to RFC6020 a single quote character cannot occur in
479                  * a single-quoted string, even when preceded by a backslash.
480                  */
481                 sb.append(str.substring(1, str.length() - 1));
482             } else {
483                 sb.append(str);
484             }
485         }
486         return sb.toString();
487     }
488
489     public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
490         if (Strings.isNullOrEmpty(value)) {
491             return ctx.getPublicDefinition().getStatementName();
492         }
493
494         String prefix;
495         QNameModule qNameModule = null;
496         String localName = null;
497
498         final String[] namesParts = value.split(":");
499         switch (namesParts.length) {
500         case 1:
501             localName = namesParts[0];
502             qNameModule = getRootModuleQName(ctx);
503             break;
504         default:
505             prefix = namesParts[0];
506             localName = namesParts[1];
507             qNameModule = getModuleQNameByPrefix(ctx, prefix);
508             // in case of unknown statement argument, we're not going to parse it
509             if (qNameModule == null
510                     && ctx.getPublicDefinition().getDeclaredRepresentationClass()
511                     .isAssignableFrom(UnknownStatementImpl.class)) {
512                 localName = value;
513                 qNameModule = getRootModuleQName(ctx);
514             }
515             if (qNameModule == null
516                     && ctx.getCopyHistory().getLastOperation() == CopyType.ADDED_BY_AUGMENTATION) {
517                 ctx = ctx.getOriginalCtx();
518                 qNameModule = getModuleQNameByPrefix(ctx, prefix);
519             }
520             break;
521         }
522
523         Preconditions.checkArgument(qNameModule != null,
524                 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
525                 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
526         final QNameModule resultQNameModule;
527         if (qNameModule.getRevision() == null) {
528             resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
529                 .intern();
530         } else {
531             resultQNameModule = qNameModule;
532         }
533
534         return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
535     }
536
537     public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
538         final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
539         final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
540
541         if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
542             final String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
543             return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
544         }
545         return qNameModule;
546     }
547
548     public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
549         if (ctx == null) {
550             return null;
551         }
552
553         final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
554         final QNameModule qNameModule;
555
556         if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
557             qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
558         } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
559             final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), 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         } else {
689             // creates SourceIdentifier for a submodule
690             final Date revision = Optional.fromNullable(Utils.getLatestRevision(root.declaredSubstatements()))
691                     .or(SimpleDateFormatUtil.DEFAULT_DATE_REV);
692             final String formattedRevision = SimpleDateFormatUtil.getRevisionFormat().format(revision);
693             return RevisionSourceIdentifier.create((String) root.getStatementArgument(),
694                     formattedRevision);
695         }
696     }
697 }