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;
11 import com.google.common.base.CharMatcher;
12 import com.google.common.base.Preconditions;
13 import com.google.common.base.Splitter;
14 import com.google.common.base.Strings;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.common.collect.ImmutableMap.Builder;
17 import com.google.common.collect.ImmutableSet;
18 import com.google.common.collect.Iterables;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Date;
23 import java.util.HashSet;
24 import java.util.List;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import java.util.regex.PatternSyntaxException;
30 import javax.annotation.Nullable;
31 import javax.xml.xpath.XPath;
32 import javax.xml.xpath.XPathExpressionException;
33 import javax.xml.xpath.XPathFactory;
34 import org.antlr.v4.runtime.tree.TerminalNode;
35 import org.opendaylight.yangtools.antlrv4.code.gen.YangStatementParser;
36 import org.opendaylight.yangtools.yang.common.QName;
37 import org.opendaylight.yangtools.yang.common.QNameModule;
38 import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
39 import org.opendaylight.yangtools.yang.model.api.Deviation;
40 import org.opendaylight.yangtools.yang.model.api.Deviation.Deviate;
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.util.RevisionAwareXPathImpl;
51 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
52 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
53 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
54 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
55 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
56 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
57 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
58 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
59 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
60 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
61 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
62 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
66 public final class Utils {
67 private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
68 private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
69 private static final CharMatcher DOUBLE_QUOTE_MATCHER = CharMatcher.is('"');
70 private static final CharMatcher SINGLE_QUOTE_MATCHER = CharMatcher.is('\'');
71 private static final CharMatcher LEFT_PARENTHESIS_MATCHER = CharMatcher.is('(');
72 private static final CharMatcher RIGHT_PARENTHESIS_MATCHER = CharMatcher.is(')');
73 private static final CharMatcher AMPERSAND_MATCHER = CharMatcher.is('&');
74 private static final CharMatcher QUESTION_MARK_MATCHER = CharMatcher.is('?');
75 private static final Splitter SLASH_SPLITTER = Splitter.on('/').omitEmptyStrings().trimResults();
76 private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
77 private static final Pattern PATH_ABS = Pattern.compile("/[^/].*");
78 private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
79 private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
81 .add("AlchemicalSymbols")
82 .add("AlphabeticPresentationForms")
83 .add("AncientGreekMusicalNotation")
84 .add("AncientGreekNumbers")
85 .add("AncientSymbols")
87 .add("ArabicPresentationForms-A")
88 .add("ArabicPresentationForms-B")
89 .add("ArabicSupplement")
95 .add("BamumSupplement")
101 .add("BopomofoExtended")
104 .add("BraillePatterns")
107 .add("ByzantineMusicalSymbols")
111 .add("CJKCompatibility")
112 .add("CJKCompatibilityForms")
113 .add("CJKCompatibilityIdeographs")
114 .add("CJKCompatibilityIdeographsSupplement")
115 .add("CJKRadicalsSupplement")
117 .add("CJKSymbolsandPunctuation")
118 .add("CJKUnifiedIdeographs")
119 .add("CJKUnifiedIdeographsExtensionA")
120 .add("CJKUnifiedIdeographsExtensionB")
121 .add("CJKUnifiedIdeographsExtensionC")
122 .add("CJKUnifiedIdeographsExtensionD")
123 .add("CombiningDiacriticalMarks")
124 .add("CombiningDiacriticalMarksSupplement")
125 .add("CombiningHalfMarks")
126 .add("CombiningDiacriticalMarksforSymbols")
127 .add("CommonIndicNumberForms")
128 .add("ControlPictures")
130 .add("CountingRodNumerals")
132 .add("CuneiformNumbersandPunctuation")
133 .add("CurrencySymbols")
134 .add("CypriotSyllabary")
136 .add("CyrillicExtended-A")
137 .add("CyrillicExtended-B")
138 .add("CyrillicSupplementary")
141 .add("DevanagariExtended")
144 .add("EgyptianHieroglyphs")
146 .add("EnclosedAlphanumericSupplement")
147 .add("EnclosedAlphanumerics")
148 .add("EnclosedCJKLettersandMonths")
149 .add("EnclosedIdeographicSupplement")
151 .add("EthiopicExtended")
152 .add("EthiopicExtended-A")
153 .add("EthiopicSupplement")
154 .add("GeneralPunctuation")
155 .add("GeometricShapes")
157 .add("GeorgianSupplement")
160 .add("GreekandCoptic")
161 .add("GreekExtended")
164 .add("HalfwidthandFullwidthForms")
165 .add("HangulCompatibilityJamo")
167 .add("HangulJamoExtended-A")
168 .add("HangulJamoExtended-B")
169 .add("HangulSyllables")
172 .add("HighPrivateUseSurrogates")
173 .add("HighSurrogates")
175 .add("IdeographicDescriptionCharacters")
176 .add("ImperialAramaic")
177 .add("InscriptionalPahlavi")
178 .add("InscriptionalParthian")
179 .add("IPAExtensions")
182 .add("KanaSupplement")
184 .add("Kangxi Radicals")
187 .add("KatakanaPhoneticExtensions")
193 .add("Latin-1Supplement")
194 .add("LatinExtended-A")
195 .add("LatinExtendedAdditional")
196 .add("LatinExtended-B")
197 .add("LatinExtended-C")
198 .add("LatinExtended-D")
200 .add("LetterlikeSymbols")
202 .add("LinearBIdeograms")
203 .add("LinearBSyllabary")
205 .add("LowSurrogates")
211 .add("MathematicalAlphanumericSymbols")
212 .add("MathematicalOperators")
214 .add("MiscellaneousMathematicalSymbols-A")
215 .add("MiscellaneousMathematicalSymbols-B")
216 .add("MiscellaneousSymbols")
217 .add("MiscellaneousSymbolsandArrows")
218 .add("MiscellaneousSymbolsAndPictographs")
219 .add("MiscellaneousTechnical")
220 .add("ModifierToneLetters")
222 .add("MusicalSymbols")
224 .add("MyanmarExtended-A")
232 .add("OldSouthArabian")
234 .add("OpticalCharacterRecognition")
240 .add("PhoneticExtensions")
241 .add("PhoneticExtensionsSupplement")
243 .add("PrivateUseArea")
245 .add("RumiNumeralSymbols")
251 .add("SmallFormVariants")
252 .add("SpacingModifierLetters")
255 .add("SuperscriptsandSubscripts")
256 .add("SupplementalArrows-A")
257 .add("SupplementalArrows-B")
258 .add("SupplementalMathematicalOperators")
259 .add("SupplementalPunctuation")
260 .add("SupplementaryPrivateUseArea-A")
261 .add("SupplementaryPrivateUseArea-B")
270 .add("TaiXuanJingSymbols")
277 .add("TransportAndMapSymbols")
279 .add("UnifiedCanadianAboriginalSyllabics")
280 .add("UnifiedCanadianAboriginalSyllabicsExtended")
282 .add("VariationSelectors")
283 .add("VariationSelectorsSupplement")
284 .add("VedicExtensions")
285 .add("VerticalForms")
288 .add("YijingHexagramSymbols").build();
290 private static final Map<String, Deviate> KEYWORD_TO_DEVIATE_MAP;
292 Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
293 for (Deviate deviate : Deviation.Deviate.values()) {
294 keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
296 KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
299 private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
301 protected XPathFactory initialValue() {
302 return XPathFactory.newInstance();
307 throw new UnsupportedOperationException();
311 * Cleanup any resources attached to the current thread. Threads interacting with this class can cause thread-local
312 * caches to them. Invoke this method if you want to detach those resources.
314 public static void detachFromCurrentThread() {
315 XPATH_FACTORY.remove();
318 public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
319 final String value) {
320 List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
322 // to detect if key contains duplicates
323 if ((new HashSet<>(keyTokens)).size() < keyTokens.size()) {
324 // FIXME: report all duplicate keys
325 throw new SourceException(ctx.getStatementSourceReference(), "Duplicate value in list key: %s", value);
328 Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
330 for (String keyToken : keyTokens) {
332 SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
333 Utils.qNameFromArgument(ctx, keyToken));
334 keyNodes.add(keyNode);
340 private static String trimSingleLastSlashFromXPath(final String path) {
341 return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
344 static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
345 final XPath xPath = XPATH_FACTORY.get().newXPath();
346 xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
348 final String trimmed = trimSingleLastSlashFromXPath(path);
350 // TODO: we could capture the result and expose its 'evaluate' method
351 xPath.compile(trimmed);
352 } catch (XPathExpressionException e) {
353 LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
356 return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
359 public static QName trimPrefix(final QName identifier) {
360 String prefixedLocalName = identifier.getLocalName();
361 String[] namesParts = prefixedLocalName.split(":");
363 if (namesParts.length == 2) {
364 String localName = namesParts[1];
365 return QName.create(identifier.getModule(), localName);
373 * Based on identifier read from source and collections of relevant prefixes and statement definitions mappings
374 * provided for actual phase, method resolves and returns valid QName for declared statement to be written.
375 * This applies to any declared statement, including unknown statements.
377 * @param prefixes - collection of all relevant prefix mappings supplied for actual parsing phase
378 * @param stmtDef - collection of all relevant statement definition mappings provided for actual parsing phase
379 * @param identifier - statement to parse from source
380 * @return valid QName for declared statement to be written
383 public static QName getValidStatementDefinition(final PrefixToModule prefixes, final QNameToStatementDefinition
384 stmtDef, final QName identifier) {
385 if (stmtDef.get(identifier) != null) {
386 return stmtDef.get(identifier).getStatementName();
388 String prefixedLocalName = identifier.getLocalName();
389 String[] namesParts = prefixedLocalName.split(":");
391 if (namesParts.length == 2) {
392 String prefix = namesParts[0];
393 String localName = namesParts[1];
394 if (prefixes != null && prefixes.get(prefix) != null
395 && stmtDef.get(QName.create(prefixes.get(prefix), localName)) != null) {
396 return QName.create(prefixes.get(prefix), localName);
403 static SchemaNodeIdentifier nodeIdentifierFromPath(final StmtContext<?, ?, ?> ctx, final String path) {
404 // FIXME: is the path trimming really necessary??
405 final List<QName> qNames = new ArrayList<>();
406 for (String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
408 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
410 } catch (Exception e) {
411 throw new IllegalArgumentException(
412 String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
416 return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
419 public static String stringFromStringContext(final YangStatementParser.ArgumentContext context) {
420 StringBuilder sb = new StringBuilder();
421 List<TerminalNode> strings = context.STRING();
422 if (strings.isEmpty()) {
423 strings = Arrays.asList(context.IDENTIFIER());
425 for (TerminalNode stringNode : strings) {
426 final String str = stringNode.getText();
427 char firstChar = str.charAt(0);
428 final CharMatcher quoteMatcher;
429 if (SINGLE_QUOTE_MATCHER.matches(firstChar)) {
430 quoteMatcher = SINGLE_QUOTE_MATCHER;
431 } else if (DOUBLE_QUOTE_MATCHER.matches(firstChar)) {
432 quoteMatcher = DOUBLE_QUOTE_MATCHER;
437 sb.append(quoteMatcher.removeFrom(str.substring(1, str.length() - 1)));
439 return sb.toString();
442 public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
443 if (Strings.isNullOrEmpty(value)) {
444 return ctx.getPublicDefinition().getStatementName();
448 QNameModule qNameModule = null;
449 String localName = null;
451 String[] namesParts = value.split(":");
452 switch (namesParts.length) {
454 localName = namesParts[0];
455 qNameModule = getRootModuleQName(ctx);
458 prefix = namesParts[0];
459 localName = namesParts[1];
460 qNameModule = getModuleQNameByPrefix(ctx, prefix);
461 // in case of unknown statement argument, we're not going to parse it
462 if (qNameModule == null
463 && ctx.getPublicDefinition().getDeclaredRepresentationClass()
464 .isAssignableFrom(UnknownStatementImpl.class)) {
466 qNameModule = getRootModuleQName(ctx);
468 if (qNameModule == null
469 && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
470 ctx = ctx.getOriginalCtx();
471 qNameModule = getModuleQNameByPrefix(ctx, prefix);
476 Preconditions.checkArgument(qNameModule != null,
477 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
478 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
479 final QNameModule resultQNameModule;
480 if (qNameModule.getRevision() == null) {
481 resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
484 resultQNameModule = qNameModule;
487 return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
490 public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
491 final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
492 final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
494 if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
495 String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
496 return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
501 public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
506 final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
507 final QNameModule qNameModule;
509 if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
510 qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
511 } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
512 final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), 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.getPublicDefinition().getDeclaredRepresentationClass()
534 .isAssignableFrom(UnknownStatementImpl.class);
537 public static Deviation.Deviate parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
538 return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
539 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
540 ctx.getStatementSourceReference());
543 public static Status parseStatus(final String value) {
546 return Status.CURRENT;
548 return Status.DEPRECATED;
550 return Status.OBSOLETE;
552 LOG.warn("Invalid 'status' statement: {}", value);
557 public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
558 Date revision = null;
559 for (StmtContext<?, ?, ?> subStmt : subStmts) {
560 if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
562 if (revision == null && subStmt.getStatementArgument() != null) {
563 revision = (Date) subStmt.getStatementArgument();
564 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
566 revision = (Date) subStmt.getStatementArgument();
573 public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
574 return (o instanceof ModuleIdentifier)
575 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
576 .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
580 * Replaces illegal characters of QName by the name of the character (e.g.
581 * '?' is replaced by "QuestionMark" etc.).
585 * @return result String
587 public static String replaceIllegalCharsForQName(String string) {
588 string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
589 string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
590 string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
591 string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
596 public static String fixUnicodeScriptPattern(String rawPattern) {
597 for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
599 Pattern.compile(rawPattern);
601 } catch(PatternSyntaxException ex) {
602 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
603 if (ex.getMessage().contains("Unknown character script name")) {
604 rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
611 LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
615 private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
616 StringBuilder result = new StringBuilder(rawPattern);
617 Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
618 if (matcher.find()) {
619 String capturedGroup = matcher.group(1);
620 if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
621 int idx = rawPattern.indexOf("Is" + capturedGroup);
622 result = result.replace(idx, idx + 2, "In");
625 return result.toString();