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.Date;
25 import java.util.HashSet;
26 import java.util.List;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.regex.PatternSyntaxException;
32 import javax.annotation.Nullable;
33 import javax.xml.xpath.XPath;
34 import javax.xml.xpath.XPathExpressionException;
35 import javax.xml.xpath.XPathFactory;
36 import org.antlr.v4.runtime.tree.TerminalNode;
37 import org.opendaylight.yangtools.antlrv4.code.gen.YangStatementParser;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.common.QNameModule;
40 import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
41 import org.opendaylight.yangtools.yang.model.api.Deviation;
42 import org.opendaylight.yangtools.yang.model.api.Deviation.Deviate;
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.stmt.BelongsToStatement;
48 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleStatement;
49 import org.opendaylight.yangtools.yang.model.api.stmt.RevisionStatement;
50 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
51 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Relative;
52 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleStatement;
53 import org.opendaylight.yangtools.yang.model.util.RevisionAwareXPathImpl;
54 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
55 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
56 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
57 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
58 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
60 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
61 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
62 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
63 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
64 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
65 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
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 Pattern PATH_ABS = Pattern.compile("/[^/].*");
79 private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
80 private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
82 .add("AlchemicalSymbols")
83 .add("AlphabeticPresentationForms")
84 .add("AncientGreekMusicalNotation")
85 .add("AncientGreekNumbers")
86 .add("AncientSymbols")
88 .add("ArabicPresentationForms-A")
89 .add("ArabicPresentationForms-B")
90 .add("ArabicSupplement")
96 .add("BamumSupplement")
100 .add("BlockElements")
102 .add("BopomofoExtended")
105 .add("BraillePatterns")
108 .add("ByzantineMusicalSymbols")
112 .add("CJKCompatibility")
113 .add("CJKCompatibilityForms")
114 .add("CJKCompatibilityIdeographs")
115 .add("CJKCompatibilityIdeographsSupplement")
116 .add("CJKRadicalsSupplement")
118 .add("CJKSymbolsandPunctuation")
119 .add("CJKUnifiedIdeographs")
120 .add("CJKUnifiedIdeographsExtensionA")
121 .add("CJKUnifiedIdeographsExtensionB")
122 .add("CJKUnifiedIdeographsExtensionC")
123 .add("CJKUnifiedIdeographsExtensionD")
124 .add("CombiningDiacriticalMarks")
125 .add("CombiningDiacriticalMarksSupplement")
126 .add("CombiningHalfMarks")
127 .add("CombiningDiacriticalMarksforSymbols")
128 .add("CommonIndicNumberForms")
129 .add("ControlPictures")
131 .add("CountingRodNumerals")
133 .add("CuneiformNumbersandPunctuation")
134 .add("CurrencySymbols")
135 .add("CypriotSyllabary")
137 .add("CyrillicExtended-A")
138 .add("CyrillicExtended-B")
139 .add("CyrillicSupplementary")
142 .add("DevanagariExtended")
145 .add("EgyptianHieroglyphs")
147 .add("EnclosedAlphanumericSupplement")
148 .add("EnclosedAlphanumerics")
149 .add("EnclosedCJKLettersandMonths")
150 .add("EnclosedIdeographicSupplement")
152 .add("EthiopicExtended")
153 .add("EthiopicExtended-A")
154 .add("EthiopicSupplement")
155 .add("GeneralPunctuation")
156 .add("GeometricShapes")
158 .add("GeorgianSupplement")
161 .add("GreekandCoptic")
162 .add("GreekExtended")
165 .add("HalfwidthandFullwidthForms")
166 .add("HangulCompatibilityJamo")
168 .add("HangulJamoExtended-A")
169 .add("HangulJamoExtended-B")
170 .add("HangulSyllables")
173 .add("HighPrivateUseSurrogates")
174 .add("HighSurrogates")
176 .add("IdeographicDescriptionCharacters")
177 .add("ImperialAramaic")
178 .add("InscriptionalPahlavi")
179 .add("InscriptionalParthian")
180 .add("IPAExtensions")
183 .add("KanaSupplement")
185 .add("Kangxi Radicals")
188 .add("KatakanaPhoneticExtensions")
194 .add("Latin-1Supplement")
195 .add("LatinExtended-A")
196 .add("LatinExtendedAdditional")
197 .add("LatinExtended-B")
198 .add("LatinExtended-C")
199 .add("LatinExtended-D")
201 .add("LetterlikeSymbols")
203 .add("LinearBIdeograms")
204 .add("LinearBSyllabary")
206 .add("LowSurrogates")
212 .add("MathematicalAlphanumericSymbols")
213 .add("MathematicalOperators")
215 .add("MiscellaneousMathematicalSymbols-A")
216 .add("MiscellaneousMathematicalSymbols-B")
217 .add("MiscellaneousSymbols")
218 .add("MiscellaneousSymbolsandArrows")
219 .add("MiscellaneousSymbolsAndPictographs")
220 .add("MiscellaneousTechnical")
221 .add("ModifierToneLetters")
223 .add("MusicalSymbols")
225 .add("MyanmarExtended-A")
233 .add("OldSouthArabian")
235 .add("OpticalCharacterRecognition")
241 .add("PhoneticExtensions")
242 .add("PhoneticExtensionsSupplement")
244 .add("PrivateUseArea")
246 .add("RumiNumeralSymbols")
252 .add("SmallFormVariants")
253 .add("SpacingModifierLetters")
256 .add("SuperscriptsandSubscripts")
257 .add("SupplementalArrows-A")
258 .add("SupplementalArrows-B")
259 .add("SupplementalMathematicalOperators")
260 .add("SupplementalPunctuation")
261 .add("SupplementaryPrivateUseArea-A")
262 .add("SupplementaryPrivateUseArea-B")
271 .add("TaiXuanJingSymbols")
278 .add("TransportAndMapSymbols")
280 .add("UnifiedCanadianAboriginalSyllabics")
281 .add("UnifiedCanadianAboriginalSyllabicsExtended")
283 .add("VariationSelectors")
284 .add("VariationSelectorsSupplement")
285 .add("VedicExtensions")
286 .add("VerticalForms")
289 .add("YijingHexagramSymbols").build();
291 private static final Map<String, Deviate> KEYWORD_TO_DEVIATE_MAP;
293 final Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
294 for (final Deviate deviate : Deviation.Deviate.values()) {
295 keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
297 KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
300 private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
302 protected XPathFactory initialValue() {
303 return XPathFactory.newInstance();
308 throw new UnsupportedOperationException();
312 * Cleanup any resources attached to the current thread. Threads interacting with this class can cause thread-local
313 * caches to them. Invoke this method if you want to detach those resources.
315 public static void detachFromCurrentThread() {
316 XPATH_FACTORY.remove();
319 public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
320 final String value) {
321 final List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
323 // to detect if key contains duplicates
324 if ((new HashSet<>(keyTokens)).size() < keyTokens.size()) {
325 // FIXME: report all duplicate keys
326 throw new SourceException(ctx.getStatementSourceReference(), "Duplicate value in list key: %s", value);
329 final Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
331 for (final String keyToken : keyTokens) {
333 final SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
334 Utils.qNameFromArgument(ctx, keyToken));
335 keyNodes.add(keyNode);
341 private static String trimSingleLastSlashFromXPath(final String path) {
342 return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
345 static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
346 final XPath xPath = XPATH_FACTORY.get().newXPath();
347 xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
349 final String trimmed = trimSingleLastSlashFromXPath(path);
351 // TODO: we could capture the result and expose its 'evaluate' method
352 xPath.compile(trimmed);
353 } catch (final XPathExpressionException e) {
354 LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
357 return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
360 public static QName trimPrefix(final QName identifier) {
361 final String prefixedLocalName = identifier.getLocalName();
362 final String[] namesParts = prefixedLocalName.split(":");
364 if (namesParts.length == 2) {
365 final String localName = namesParts[1];
366 return QName.create(identifier.getModule(), localName);
374 * Based on identifier read from source and collections of relevant prefixes and statement definitions mappings
375 * provided for actual phase, method resolves and returns valid QName for declared statement to be written.
376 * This applies to any declared statement, including unknown statements.
378 * @param prefixes - collection of all relevant prefix mappings supplied for actual parsing phase
379 * @param stmtDef - collection of all relevant statement definition mappings provided for actual parsing phase
380 * @param identifier - statement to parse from source
381 * @return valid QName for declared statement to be written
384 public static QName getValidStatementDefinition(final PrefixToModule prefixes, final QNameToStatementDefinition
385 stmtDef, final QName identifier) {
386 if (stmtDef.get(identifier) != null) {
387 return stmtDef.get(identifier).getStatementName();
389 final String prefixedLocalName = identifier.getLocalName();
390 final String[] namesParts = prefixedLocalName.split(":");
392 if (namesParts.length == 2) {
393 final String prefix = namesParts[0];
394 final String localName = namesParts[1];
395 if (prefixes != null && prefixes.get(prefix) != null
396 && stmtDef.get(QName.create(prefixes.get(prefix), localName)) != null) {
397 return QName.create(prefixes.get(prefix), localName);
404 static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
405 // FIXME: is the path trimming really necessary??
406 final List<QName> qNames = new ArrayList<>();
407 for (final String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
409 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
411 } catch (final Exception e) {
412 throw new IllegalArgumentException(
413 String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
417 return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
420 public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
421 final StringBuilder sb = new StringBuilder();
422 List<TerminalNode> strings = context.STRING();
423 if (strings.isEmpty()) {
424 strings = Arrays.asList(context.IDENTIFIER());
426 for (final TerminalNode stringNode : strings) {
427 final String str = stringNode.getText();
428 final char firstChar = str.charAt(0);
429 final char lastChar = str.charAt(str.length() - 1);
430 if (firstChar == '"' && lastChar == '"') {
431 final String innerStr = str.substring(1, str.length() - 1);
433 * Unescape escaped double quotes, tabs, new line and backslash
434 * in the inner string and trim the result.
436 sb.append(innerStr.replace("\\\"", "\"").replace("\\\\", "\\").replace("\\n", "\n")
437 .replace("\\t", "\t"));
438 } else if (firstChar == '\'' && lastChar == '\'') {
440 * According to RFC6020 a single quote character cannot occur in
441 * a single-quoted string, even when preceded by a backslash.
443 sb.append(str.substring(1, str.length() - 1));
448 return sb.toString();
451 public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
452 if (Strings.isNullOrEmpty(value)) {
453 return ctx.getPublicDefinition().getStatementName();
457 QNameModule qNameModule = null;
458 String localName = null;
460 final String[] namesParts = value.split(":");
461 switch (namesParts.length) {
463 localName = namesParts[0];
464 qNameModule = getRootModuleQName(ctx);
467 prefix = namesParts[0];
468 localName = namesParts[1];
469 qNameModule = getModuleQNameByPrefix(ctx, prefix);
470 // in case of unknown statement argument, we're not going to parse it
471 if (qNameModule == null
472 && ctx.getPublicDefinition().getDeclaredRepresentationClass()
473 .isAssignableFrom(UnknownStatementImpl.class)) {
475 qNameModule = getRootModuleQName(ctx);
477 if (qNameModule == null
478 && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
479 ctx = ctx.getOriginalCtx();
480 qNameModule = getModuleQNameByPrefix(ctx, prefix);
485 Preconditions.checkArgument(qNameModule != null,
486 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
487 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
488 final QNameModule resultQNameModule;
489 if (qNameModule.getRevision() == null) {
490 resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
493 resultQNameModule = qNameModule;
496 return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
499 public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
500 final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
501 final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
503 if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
504 final String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
505 return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
510 public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
515 final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
516 final QNameModule qNameModule;
518 if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
519 qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
520 } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
521 final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), BelongsToStatement.class);
522 qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
527 Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
528 if (qNameModule.getRevision() != null) {
532 return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
536 public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
537 final SchemaNodeIdentifier node) {
538 return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
541 public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
542 return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
543 .isAssignableFrom(UnknownStatementImpl.class);
546 public static Deviation.Deviate parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
547 return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
548 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
549 ctx.getStatementSourceReference());
552 public static Status parseStatus(final String value) {
555 return Status.CURRENT;
557 return Status.DEPRECATED;
559 return Status.OBSOLETE;
561 LOG.warn("Invalid 'status' statement: {}", value);
566 public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
567 Date revision = null;
568 for (final StmtContext<?, ?, ?> subStmt : subStmts) {
569 if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
571 if (revision == null && subStmt.getStatementArgument() != null) {
572 revision = (Date) subStmt.getStatementArgument();
573 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
575 revision = (Date) subStmt.getStatementArgument();
582 public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
583 return (o instanceof ModuleIdentifier)
584 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
585 .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
589 * Replaces illegal characters of QName by the name of the character (e.g.
590 * '?' is replaced by "QuestionMark" etc.).
594 * @return result String
596 public static String replaceIllegalCharsForQName(String string) {
597 string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
598 string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
599 string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
600 string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
605 public static String fixUnicodeScriptPattern(String rawPattern) {
606 for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
608 Pattern.compile(rawPattern);
610 } catch(final PatternSyntaxException ex) {
611 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
612 if (ex.getMessage().contains("Unknown character script name")) {
613 rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
620 LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
624 private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
625 StringBuilder result = new StringBuilder(rawPattern);
626 final Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
627 if (matcher.find()) {
628 final String capturedGroup = matcher.group(1);
629 if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
630 final int idx = rawPattern.indexOf("Is" + capturedGroup);
631 result = result.replace(idx, idx + 2, "In");
634 return result.toString();
637 public static boolean belongsToTheSameModule(final QName targetStmtQName, final QName sourceStmtQName) {
638 if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
644 public static boolean isPresenceContainer(final StatementContextBase<?, ?, ?> targetCtx) {
645 if (!targetCtx.getPublicDefinition().equals(Rfc6020Mapping.CONTAINER)) {
649 final List<StatementContextBase<?, ?, ?>> targetSubStatements = new ImmutableList.Builder<StatementContextBase<?, ?, ?>>()
650 .addAll(targetCtx.declaredSubstatements()).addAll(targetCtx.effectiveSubstatements()).build();
651 for (final StatementContextBase<?, ?, ?> subStatement : targetSubStatements) {
652 if (subStatement.getPublicDefinition().equals(Rfc6020Mapping.PRESENCE)) {