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.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.Arrays;
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;
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.Deviation;
43 import org.opendaylight.yangtools.yang.model.api.Deviation.Deviate;
44 import org.opendaylight.yangtools.yang.model.api.ModuleIdentifier;
45 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
46 import org.opendaylight.yangtools.yang.model.api.Rfc6020Mapping;
47 import org.opendaylight.yangtools.yang.model.api.Status;
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.util.RevisionAwareXPathImpl;
55 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
56 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
57 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
58 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
60 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
61 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
62 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
63 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
64 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
65 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
66 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
70 public final class Utils {
71 private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
72 private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
73 private static final CharMatcher DOUBLE_QUOTE_MATCHER = CharMatcher.is('"');
74 private static final CharMatcher SINGLE_QUOTE_MATCHER = CharMatcher.is('\'');
75 private static final CharMatcher LEFT_PARENTHESIS_MATCHER = CharMatcher.is('(');
76 private static final CharMatcher RIGHT_PARENTHESIS_MATCHER = CharMatcher.is(')');
77 private static final CharMatcher AMPERSAND_MATCHER = CharMatcher.is('&');
78 private static final CharMatcher QUESTION_MARK_MATCHER = CharMatcher.is('?');
79 private static final Splitter SLASH_SPLITTER = Splitter.on('/').omitEmptyStrings().trimResults();
80 private static final Splitter SPACE_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()
85 .add("AlchemicalSymbols")
86 .add("AlphabeticPresentationForms")
87 .add("AncientGreekMusicalNotation")
88 .add("AncientGreekNumbers")
89 .add("AncientSymbols")
91 .add("ArabicPresentationForms-A")
92 .add("ArabicPresentationForms-B")
93 .add("ArabicSupplement")
99 .add("BamumSupplement")
103 .add("BlockElements")
105 .add("BopomofoExtended")
108 .add("BraillePatterns")
111 .add("ByzantineMusicalSymbols")
115 .add("CJKCompatibility")
116 .add("CJKCompatibilityForms")
117 .add("CJKCompatibilityIdeographs")
118 .add("CJKCompatibilityIdeographsSupplement")
119 .add("CJKRadicalsSupplement")
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")
134 .add("CountingRodNumerals")
136 .add("CuneiformNumbersandPunctuation")
137 .add("CurrencySymbols")
138 .add("CypriotSyllabary")
140 .add("CyrillicExtended-A")
141 .add("CyrillicExtended-B")
142 .add("CyrillicSupplementary")
145 .add("DevanagariExtended")
148 .add("EgyptianHieroglyphs")
150 .add("EnclosedAlphanumericSupplement")
151 .add("EnclosedAlphanumerics")
152 .add("EnclosedCJKLettersandMonths")
153 .add("EnclosedIdeographicSupplement")
155 .add("EthiopicExtended")
156 .add("EthiopicExtended-A")
157 .add("EthiopicSupplement")
158 .add("GeneralPunctuation")
159 .add("GeometricShapes")
161 .add("GeorgianSupplement")
164 .add("GreekandCoptic")
165 .add("GreekExtended")
168 .add("HalfwidthandFullwidthForms")
169 .add("HangulCompatibilityJamo")
171 .add("HangulJamoExtended-A")
172 .add("HangulJamoExtended-B")
173 .add("HangulSyllables")
176 .add("HighPrivateUseSurrogates")
177 .add("HighSurrogates")
179 .add("IdeographicDescriptionCharacters")
180 .add("ImperialAramaic")
181 .add("InscriptionalPahlavi")
182 .add("InscriptionalParthian")
183 .add("IPAExtensions")
186 .add("KanaSupplement")
188 .add("Kangxi Radicals")
191 .add("KatakanaPhoneticExtensions")
197 .add("Latin-1Supplement")
198 .add("LatinExtended-A")
199 .add("LatinExtendedAdditional")
200 .add("LatinExtended-B")
201 .add("LatinExtended-C")
202 .add("LatinExtended-D")
204 .add("LetterlikeSymbols")
206 .add("LinearBIdeograms")
207 .add("LinearBSyllabary")
209 .add("LowSurrogates")
215 .add("MathematicalAlphanumericSymbols")
216 .add("MathematicalOperators")
218 .add("MiscellaneousMathematicalSymbols-A")
219 .add("MiscellaneousMathematicalSymbols-B")
220 .add("MiscellaneousSymbols")
221 .add("MiscellaneousSymbolsandArrows")
222 .add("MiscellaneousSymbolsAndPictographs")
223 .add("MiscellaneousTechnical")
224 .add("ModifierToneLetters")
226 .add("MusicalSymbols")
228 .add("MyanmarExtended-A")
236 .add("OldSouthArabian")
238 .add("OpticalCharacterRecognition")
244 .add("PhoneticExtensions")
245 .add("PhoneticExtensionsSupplement")
247 .add("PrivateUseArea")
249 .add("RumiNumeralSymbols")
255 .add("SmallFormVariants")
256 .add("SpacingModifierLetters")
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")
274 .add("TaiXuanJingSymbols")
281 .add("TransportAndMapSymbols")
283 .add("UnifiedCanadianAboriginalSyllabics")
284 .add("UnifiedCanadianAboriginalSyllabicsExtended")
286 .add("VariationSelectors")
287 .add("VariationSelectorsSupplement")
288 .add("VedicExtensions")
289 .add("VerticalForms")
292 .add("YijingHexagramSymbols").build();
294 private static final Map<String, Deviate> KEYWORD_TO_DEVIATE_MAP;
296 Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
297 for (Deviate deviate : Deviation.Deviate.values()) {
298 keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
300 KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
303 private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
305 protected XPathFactory initialValue() {
306 return XPathFactory.newInstance();
311 throw new UnsupportedOperationException();
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.
318 public static void detachFromCurrentThread() {
319 XPATH_FACTORY.remove();
322 public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
323 final String value) {
324 List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
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);
332 Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
334 for (String keyToken : keyTokens) {
336 SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
337 Utils.qNameFromArgument(ctx, keyToken));
338 keyNodes.add(keyNode);
344 private static String trimSingleLastSlashFromXPath(final String path) {
345 return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
348 static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
349 final XPath xPath = XPATH_FACTORY.get().newXPath();
350 xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
352 final String trimmed = trimSingleLastSlashFromXPath(path);
354 // TODO: we could capture the result and expose its 'evaluate' method
355 xPath.compile(trimmed);
356 } catch (XPathExpressionException e) {
357 LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
360 return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
363 public static QName trimPrefix(final QName identifier) {
364 String prefixedLocalName = identifier.getLocalName();
365 String[] namesParts = prefixedLocalName.split(":");
367 if (namesParts.length == 2) {
368 String localName = namesParts[1];
369 return QName.create(identifier.getModule(), localName);
377 * Based on identifier read from source and collections of relevant prefixes and statement definitions mappings
378 * provided for actual phase, method resolves and returns valid QName for declared statement to be written.
379 * This applies to any declared statement, including unknown statements.
381 * @param prefixes - collection of all relevant prefix mappings supplied for actual parsing phase
382 * @param stmtDef - collection of all relevant statement definition mappings provided for actual parsing phase
383 * @param identifier - statement to parse from source
384 * @return valid QName for declared statement to be written
387 public static QName getValidStatementDefinition(final PrefixToModule prefixes, final QNameToStatementDefinition
388 stmtDef, final QName identifier) {
389 if (stmtDef.get(identifier) != null) {
390 return stmtDef.get(identifier).getStatementName();
392 String prefixedLocalName = identifier.getLocalName();
393 String[] namesParts = prefixedLocalName.split(":");
395 if (namesParts.length == 2) {
396 String prefix = namesParts[0];
397 String localName = namesParts[1];
398 if (prefixes != null && prefixes.get(prefix) != null
399 && stmtDef.get(QName.create(prefixes.get(prefix), localName)) != null) {
400 return QName.create(prefixes.get(prefix), localName);
407 static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
408 // FIXME: is the path trimming really necessary??
409 final List<QName> qNames = new ArrayList<>();
410 for (String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
412 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
414 } catch (Exception e) {
415 throw new IllegalArgumentException(
416 String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
420 return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
423 public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
424 StringBuilder sb = new StringBuilder();
425 List<TerminalNode> strings = context.STRING();
426 if (strings.isEmpty()) {
427 strings = Collections.singletonList(context.IDENTIFIER());
429 for (TerminalNode stringNode : strings) {
430 final String str = stringNode.getText();
431 char firstChar = str.charAt(0);
432 final CharMatcher quoteMatcher;
433 if (SINGLE_QUOTE_MATCHER.matches(firstChar)) {
434 quoteMatcher = SINGLE_QUOTE_MATCHER;
435 } else if (DOUBLE_QUOTE_MATCHER.matches(firstChar)) {
436 quoteMatcher = DOUBLE_QUOTE_MATCHER;
441 sb.append(quoteMatcher.removeFrom(str.substring(1, str.length() - 1)));
443 return sb.toString();
446 public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
447 if (Strings.isNullOrEmpty(value)) {
448 return ctx.getPublicDefinition().getStatementName();
452 QNameModule qNameModule = null;
453 String localName = null;
455 String[] namesParts = value.split(":");
456 switch (namesParts.length) {
458 localName = namesParts[0];
459 qNameModule = getRootModuleQName(ctx);
462 prefix = namesParts[0];
463 localName = namesParts[1];
464 qNameModule = getModuleQNameByPrefix(ctx, prefix);
465 // in case of unknown statement argument, we're not going to parse it
466 if (qNameModule == null
467 && ctx.getPublicDefinition().getDeclaredRepresentationClass()
468 .isAssignableFrom(UnknownStatementImpl.class)) {
470 qNameModule = getRootModuleQName(ctx);
472 if (qNameModule == null
473 && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
474 ctx = ctx.getOriginalCtx();
475 qNameModule = getModuleQNameByPrefix(ctx, prefix);
480 Preconditions.checkArgument(qNameModule != null,
481 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
482 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
483 final QNameModule resultQNameModule;
484 if (qNameModule.getRevision() == null) {
485 resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
488 resultQNameModule = qNameModule;
491 return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
494 public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
495 final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
496 final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
498 if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
499 String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
500 return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
505 public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
510 final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
511 final QNameModule qNameModule;
513 if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
514 qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
515 } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
516 final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), BelongsToStatement.class);
517 qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
522 Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
523 if (qNameModule.getRevision() != null) {
527 return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
531 public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
532 final SchemaNodeIdentifier node) {
533 return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
536 public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
537 return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
538 .isAssignableFrom(UnknownStatementImpl.class);
541 public static Deviation.Deviate parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
542 return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
543 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
544 ctx.getStatementSourceReference());
547 public static Status parseStatus(final String value) {
550 return Status.CURRENT;
552 return Status.DEPRECATED;
554 return Status.OBSOLETE;
556 LOG.warn("Invalid 'status' statement: {}", value);
561 public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
562 Date revision = null;
563 for (StmtContext<?, ?, ?> subStmt : subStmts) {
564 if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
566 if (revision == null && subStmt.getStatementArgument() != null) {
567 revision = (Date) subStmt.getStatementArgument();
568 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
570 revision = (Date) subStmt.getStatementArgument();
577 public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
578 return (o instanceof ModuleIdentifier)
579 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
580 .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
584 * Replaces illegal characters of QName by the name of the character (e.g.
585 * '?' is replaced by "QuestionMark" etc.).
589 * @return result String
591 public static String replaceIllegalCharsForQName(String string) {
592 string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
593 string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
594 string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
595 string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
600 public static String fixUnicodeScriptPattern(String rawPattern) {
601 for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
603 Pattern.compile(rawPattern);
605 } catch(PatternSyntaxException ex) {
606 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
607 if (ex.getMessage().contains("Unknown character script name")) {
608 rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
615 LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
619 private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
620 StringBuilder result = new StringBuilder(rawPattern);
621 Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
622 if (matcher.find()) {
623 String capturedGroup = matcher.group(1);
624 if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
625 int idx = rawPattern.indexOf("Is" + capturedGroup);
626 result = result.replace(idx, idx + 2, "In");
629 return result.toString();
632 public static boolean belongsToTheSameModule(QName targetStmtQName, QName sourceStmtQName) {
633 if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
639 public static boolean isPresenceContainer(StatementContextBase<?, ?, ?> targetCtx) {
640 if (!targetCtx.getPublicDefinition().equals(Rfc6020Mapping.CONTAINER)) {
644 final List<StatementContextBase<?, ?, ?>> targetSubStatements = new ImmutableList.Builder<StatementContextBase<?, ?, ?>>()
645 .addAll(targetCtx.declaredSubstatements()).addAll(targetCtx.effectiveSubstatements()).build();
646 for (final StatementContextBase<?, ?, ?> subStatement : targetSubStatements) {
647 if (subStatement.getPublicDefinition().equals(Rfc6020Mapping.PRESENCE)) {