2 * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
10 import static org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils.firstAttributeOf;
12 import com.google.common.base.CharMatcher;
13 import com.google.common.base.Preconditions;
14 import com.google.common.base.Splitter;
15 import com.google.common.base.Strings;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.ImmutableMap.Builder;
18 import com.google.common.collect.ImmutableSet;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashSet;
24 import java.util.List;
26 import java.util.Optional;
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.stmt.BelongsToStatement;
45 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleStatement;
46 import org.opendaylight.yangtools.yang.model.api.stmt.RevisionStatement;
47 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
48 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Relative;
49 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleStatement;
50 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
51 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
52 import org.opendaylight.yangtools.yang.model.util.RevisionAwareXPathImpl;
53 import org.opendaylight.yangtools.yang.parser.spi.meta.CopyType;
54 import org.opendaylight.yangtools.yang.parser.spi.meta.InferenceException;
55 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
56 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
57 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
58 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
60 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
61 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
62 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
63 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
64 import org.opendaylight.yangtools.yang.parser.stmt.reactor.RootStatementContext;
65 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
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 LEFT_PARENTHESIS_MATCHER = CharMatcher.is('(');
73 private static final CharMatcher RIGHT_PARENTHESIS_MATCHER = CharMatcher.is(')');
74 private static final CharMatcher AMPERSAND_MATCHER = CharMatcher.is('&');
75 private static final CharMatcher QUESTION_MARK_MATCHER = CharMatcher.is('?');
76 private static final Splitter SLASH_SPLITTER = Splitter.on('/').omitEmptyStrings().trimResults();
77 private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
78 private static final Splitter COLON_SPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
79 private static final Pattern PATH_ABS = Pattern.compile("/[^/].*");
80 private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
81 private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
83 .add("AlchemicalSymbols")
84 .add("AlphabeticPresentationForms")
85 .add("AncientGreekMusicalNotation")
86 .add("AncientGreekNumbers")
87 .add("AncientSymbols")
89 .add("ArabicPresentationForms-A")
90 .add("ArabicPresentationForms-B")
91 .add("ArabicSupplement")
97 .add("BamumSupplement")
101 .add("BlockElements")
103 .add("BopomofoExtended")
106 .add("BraillePatterns")
109 .add("ByzantineMusicalSymbols")
113 .add("CJKCompatibility")
114 .add("CJKCompatibilityForms")
115 .add("CJKCompatibilityIdeographs")
116 .add("CJKCompatibilityIdeographsSupplement")
117 .add("CJKRadicalsSupplement")
119 .add("CJKSymbolsandPunctuation")
120 .add("CJKUnifiedIdeographs")
121 .add("CJKUnifiedIdeographsExtensionA")
122 .add("CJKUnifiedIdeographsExtensionB")
123 .add("CJKUnifiedIdeographsExtensionC")
124 .add("CJKUnifiedIdeographsExtensionD")
125 .add("CombiningDiacriticalMarks")
126 .add("CombiningDiacriticalMarksSupplement")
127 .add("CombiningHalfMarks")
128 .add("CombiningDiacriticalMarksforSymbols")
129 .add("CommonIndicNumberForms")
130 .add("ControlPictures")
132 .add("CountingRodNumerals")
134 .add("CuneiformNumbersandPunctuation")
135 .add("CurrencySymbols")
136 .add("CypriotSyllabary")
138 .add("CyrillicExtended-A")
139 .add("CyrillicExtended-B")
140 .add("CyrillicSupplementary")
143 .add("DevanagariExtended")
146 .add("EgyptianHieroglyphs")
148 .add("EnclosedAlphanumericSupplement")
149 .add("EnclosedAlphanumerics")
150 .add("EnclosedCJKLettersandMonths")
151 .add("EnclosedIdeographicSupplement")
153 .add("EthiopicExtended")
154 .add("EthiopicExtended-A")
155 .add("EthiopicSupplement")
156 .add("GeneralPunctuation")
157 .add("GeometricShapes")
159 .add("GeorgianSupplement")
162 .add("GreekandCoptic")
163 .add("GreekExtended")
166 .add("HalfwidthandFullwidthForms")
167 .add("HangulCompatibilityJamo")
169 .add("HangulJamoExtended-A")
170 .add("HangulJamoExtended-B")
171 .add("HangulSyllables")
174 .add("HighPrivateUseSurrogates")
175 .add("HighSurrogates")
177 .add("IdeographicDescriptionCharacters")
178 .add("ImperialAramaic")
179 .add("InscriptionalPahlavi")
180 .add("InscriptionalParthian")
181 .add("IPAExtensions")
184 .add("KanaSupplement")
186 .add("Kangxi Radicals")
189 .add("KatakanaPhoneticExtensions")
195 .add("Latin-1Supplement")
196 .add("LatinExtended-A")
197 .add("LatinExtendedAdditional")
198 .add("LatinExtended-B")
199 .add("LatinExtended-C")
200 .add("LatinExtended-D")
202 .add("LetterlikeSymbols")
204 .add("LinearBIdeograms")
205 .add("LinearBSyllabary")
207 .add("LowSurrogates")
213 .add("MathematicalAlphanumericSymbols")
214 .add("MathematicalOperators")
216 .add("MiscellaneousMathematicalSymbols-A")
217 .add("MiscellaneousMathematicalSymbols-B")
218 .add("MiscellaneousSymbols")
219 .add("MiscellaneousSymbolsandArrows")
220 .add("MiscellaneousSymbolsAndPictographs")
221 .add("MiscellaneousTechnical")
222 .add("ModifierToneLetters")
224 .add("MusicalSymbols")
226 .add("MyanmarExtended-A")
234 .add("OldSouthArabian")
236 .add("OpticalCharacterRecognition")
242 .add("PhoneticExtensions")
243 .add("PhoneticExtensionsSupplement")
245 .add("PrivateUseArea")
247 .add("RumiNumeralSymbols")
253 .add("SmallFormVariants")
254 .add("SpacingModifierLetters")
257 .add("SuperscriptsandSubscripts")
258 .add("SupplementalArrows-A")
259 .add("SupplementalArrows-B")
260 .add("SupplementalMathematicalOperators")
261 .add("SupplementalPunctuation")
262 .add("SupplementaryPrivateUseArea-A")
263 .add("SupplementaryPrivateUseArea-B")
272 .add("TaiXuanJingSymbols")
279 .add("TransportAndMapSymbols")
281 .add("UnifiedCanadianAboriginalSyllabics")
282 .add("UnifiedCanadianAboriginalSyllabicsExtended")
284 .add("VariationSelectors")
285 .add("VariationSelectorsSupplement")
286 .add("VedicExtensions")
287 .add("VerticalForms")
290 .add("YijingHexagramSymbols").build();
292 private static final Map<String, DeviateKind> KEYWORD_TO_DEVIATE_MAP;
294 final Builder<String, DeviateKind> keywordToDeviateMapBuilder = ImmutableMap.builder();
295 for (final DeviateKind deviate : DeviateKind.values()) {
296 keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
298 KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
301 private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
303 protected XPathFactory initialValue() {
304 return XPathFactory.newInstance();
309 throw new UnsupportedOperationException();
313 * Cleanup any resources attached to the current thread. Threads interacting with this class can cause thread-local
314 * caches to them. Invoke this method if you want to detach those resources.
316 public static void detachFromCurrentThread() {
317 XPATH_FACTORY.remove();
320 public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
321 final String value) {
322 final List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
324 // to detect if key contains duplicates
325 if ((new HashSet<>(keyTokens)).size() < keyTokens.size()) {
326 // FIXME: report all duplicate keys
327 throw new SourceException(ctx.getStatementSourceReference(), "Duplicate value in list key: %s", value);
330 final Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
332 for (final String keyToken : keyTokens) {
334 final SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
335 Utils.qNameFromArgument(ctx, keyToken));
336 keyNodes.add(keyNode);
342 static Collection<SchemaNodeIdentifier.Relative> parseUniqueConstraintArgument(final StmtContext<?, ?, ?> ctx,
343 final String argumentValue) {
344 final Set<SchemaNodeIdentifier.Relative> uniqueConstraintNodes = new HashSet<>();
345 for (final String uniqueArgToken : SPACE_SPLITTER.split(argumentValue)) {
346 final SchemaNodeIdentifier nodeIdentifier = Utils.nodeIdentifierFromPath(ctx, uniqueArgToken);
347 SourceException.throwIf(nodeIdentifier.isAbsolute(), ctx.getStatementSourceReference(),
348 "Unique statement argument '%s' contains schema node identifier '%s' "
349 + "which is not in the descendant node identifier form.", argumentValue, uniqueArgToken);
350 uniqueConstraintNodes.add((SchemaNodeIdentifier.Relative) nodeIdentifier);
352 return ImmutableSet.copyOf(uniqueConstraintNodes);
355 private static String trimSingleLastSlashFromXPath(final String path) {
356 return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
359 static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
360 final XPath xPath = XPATH_FACTORY.get().newXPath();
361 xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
363 final String trimmed = trimSingleLastSlashFromXPath(path);
365 // TODO: we could capture the result and expose its 'evaluate' method
366 xPath.compile(trimmed);
367 } catch (final XPathExpressionException e) {
368 LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
371 return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
374 public static QName trimPrefix(final QName identifier) {
375 final String prefixedLocalName = identifier.getLocalName();
376 final String[] namesParts = prefixedLocalName.split(":");
378 if (namesParts.length == 2) {
379 final String localName = namesParts[1];
380 return QName.create(identifier.getModule(), localName);
386 public static String trimPrefix(final String identifier) {
387 final List<String> namesParts = COLON_SPLITTER.splitToList(identifier);
388 if (namesParts.size() == 2) {
389 return namesParts.get(1);
394 static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
395 // FIXME: is the path trimming really necessary??
396 final List<QName> qNames = new ArrayList<>();
397 for (final String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
399 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
401 } catch (final RuntimeException e) {
402 throw new SourceException(ctx.getStatementSourceReference(), e,
403 "Failed to parse node '%s' in path '%s'", nodeName, path);
407 return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
410 public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
411 final StringBuilder sb = new StringBuilder();
412 List<TerminalNode> strings = context.STRING();
413 if (strings.isEmpty()) {
414 strings = Collections.singletonList(context.IDENTIFIER());
416 for (final TerminalNode stringNode : strings) {
417 final String str = stringNode.getText();
418 final char firstChar = str.charAt(0);
419 final char lastChar = str.charAt(str.length() - 1);
420 if (firstChar == '"' && lastChar == '"') {
421 final String innerStr = str.substring(1, str.length() - 1);
423 * Unescape escaped double quotes, tabs, new line and backslash
424 * in the inner string and trim the result.
426 sb.append(innerStr.replace("\\\"", "\"").replace("\\\\", "\\").replace("\\n", "\n")
427 .replace("\\t", "\t"));
428 } else if (firstChar == '\'' && lastChar == '\'') {
430 * According to RFC6020 a single quote character cannot occur in
431 * a single-quoted string, even when preceded by a backslash.
433 sb.append(str.substring(1, str.length() - 1));
438 return sb.toString();
441 public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
442 if (Strings.isNullOrEmpty(value)) {
443 return ctx.getPublicDefinition().getStatementName();
447 QNameModule qNameModule = null;
448 String localName = null;
450 final String[] namesParts = value.split(":");
451 switch (namesParts.length) {
453 localName = namesParts[0];
454 qNameModule = getRootModuleQName(ctx);
457 prefix = namesParts[0];
458 localName = namesParts[1];
459 qNameModule = getModuleQNameByPrefix(ctx, prefix);
460 // in case of unknown statement argument, we're not going to parse it
461 if (qNameModule == null
462 && ctx.getPublicDefinition().getDeclaredRepresentationClass()
463 .isAssignableFrom(UnknownStatementImpl.class)) {
465 qNameModule = getRootModuleQName(ctx);
467 if (qNameModule == null
468 && ctx.getCopyHistory().getLastOperation() == CopyType.ADDED_BY_AUGMENTATION) {
469 ctx = ctx.getOriginalCtx();
470 qNameModule = getModuleQNameByPrefix(ctx, prefix);
475 qNameModule = InferenceException.throwIfNull(qNameModule, ctx.getStatementSourceReference(),
476 "Cannot resolve QNameModule for '%s'", value);
478 final QNameModule resultQNameModule;
479 if (qNameModule.getRevision() == null) {
480 resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
483 resultQNameModule = qNameModule;
486 return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
489 public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
490 final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
491 final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
493 if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
494 final String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
495 return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
500 public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
505 final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
506 final QNameModule qNameModule;
508 if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
509 qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
510 } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
511 final String belongsToModuleName = firstAttributeOf(rootCtx.declaredSubstatements(),
512 BelongsToStatement.class);
513 qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
518 Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
519 if (qNameModule.getRevision() != null) {
523 return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
527 public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
528 final SchemaNodeIdentifier node) {
529 return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
532 public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
533 return stmtCtx != null && stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
534 .isAssignableFrom(UnknownStatementImpl.class);
537 public static DeviateKind parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
538 return SourceException.throwIfNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
539 ctx.getStatementSourceReference(), "String '%s' is not valid deviate argument", deviateKeyword);
542 public static Status parseStatus(final String value) {
545 return Status.CURRENT;
547 return Status.DEPRECATED;
549 return Status.OBSOLETE;
551 LOG.warn("Invalid 'status' statement: {}", value);
556 public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
557 Date revision = null;
558 for (final StmtContext<?, ?, ?> subStmt : subStmts) {
559 if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
561 if (revision == null && subStmt.getStatementArgument() != null) {
562 revision = (Date) subStmt.getStatementArgument();
563 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
565 revision = (Date) subStmt.getStatementArgument();
572 public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
573 return (o instanceof ModuleIdentifier)
574 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
575 .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
579 * Replaces illegal characters of QName by the name of the character (e.g.
580 * '?' is replaced by "QuestionMark" etc.).
584 * @return result String
586 public static String replaceIllegalCharsForQName(String string) {
587 string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
588 string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
589 string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
590 string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
595 public static String fixUnicodeScriptPattern(String rawPattern) {
596 for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
598 Pattern.compile(rawPattern);
600 } catch(final PatternSyntaxException ex) {
601 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
602 if (ex.getMessage().contains("Unknown character script name")) {
603 rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
610 LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
614 private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
615 StringBuilder result = new StringBuilder(rawPattern);
616 final Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
617 if (matcher.find()) {
618 final String capturedGroup = matcher.group(1);
619 if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
620 final int idx = rawPattern.indexOf("Is" + capturedGroup);
621 result = result.replace(idx, idx + 2, "In");
624 return result.toString();
627 public static boolean belongsToTheSameModule(final QName targetStmtQName, final QName sourceStmtQName) {
628 if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
634 public static SourceIdentifier createSourceIdentifier(final RootStatementContext<?, ?, ?> root) {
635 final QNameModule qNameModule = root.getFromNamespace(ModuleCtxToModuleQName.class, root);
636 if (qNameModule != null) {
637 // creates SourceIdentifier for a module
638 return RevisionSourceIdentifier.create((String) root.getStatementArgument(),
639 qNameModule.getFormattedRevision());
642 // creates SourceIdentifier for a submodule
643 final Date revision = Optional.ofNullable(Utils.getLatestRevision(root.declaredSubstatements()))
644 .orElse(SimpleDateFormatUtil.DEFAULT_DATE_REV);
645 final String formattedRevision = SimpleDateFormatUtil.getRevisionFormat().format(revision);
646 return RevisionSourceIdentifier.create((String) root.getStatementArgument(), formattedRevision);