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.Collections;
23 import java.util.Date;
24 import java.util.HashSet;
25 import java.util.List;
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.Deviation;
41 import org.opendaylight.yangtools.yang.model.api.Deviation.Deviate;
42 import org.opendaylight.yangtools.yang.model.api.ModuleIdentifier;
43 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
44 import org.opendaylight.yangtools.yang.model.api.Status;
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.util.RevisionAwareXPathImpl;
52 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
53 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
54 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
55 import org.opendaylight.yangtools.yang.parser.spi.source.BelongsToPrefixToModuleName;
56 import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToModuleIdentifier;
57 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleQName;
58 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleIdentifierToModuleQName;
59 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToModuleQName;
60 import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
61 import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
62 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
63 import org.opendaylight.yangtools.yang.parser.stmt.reactor.StatementContextBase;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 public final class Utils {
68 private static final int UNICODE_SCRIPT_FIX_COUNTER = 30;
69 private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
70 private static final CharMatcher DOUBLE_QUOTE_MATCHER = CharMatcher.is('"');
71 private static final CharMatcher SINGLE_QUOTE_MATCHER = CharMatcher.is('\'');
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 Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
294 for (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 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 Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
331 for (String keyToken : keyTokens) {
333 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 (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 String prefixedLocalName = identifier.getLocalName();
362 String[] namesParts = prefixedLocalName.split(":");
364 if (namesParts.length == 2) {
365 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 String prefixedLocalName = identifier.getLocalName();
390 String[] namesParts = prefixedLocalName.split(":");
392 if (namesParts.length == 2) {
393 String prefix = namesParts[0];
394 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 (String nodeName : SLASH_SPLITTER.split(trimSingleLastSlashFromXPath(path))) {
409 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
411 } catch (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 StringBuilder sb = new StringBuilder();
422 List<TerminalNode> strings = context.STRING();
423 if (strings.isEmpty()) {
424 strings = Collections.singletonList(context.IDENTIFIER());
426 for (TerminalNode stringNode : strings) {
427 final String str = stringNode.getText();
428 char firstChar = str.charAt(0);
429 final CharMatcher quoteMatcher;
430 if (SINGLE_QUOTE_MATCHER.matches(firstChar)) {
431 quoteMatcher = SINGLE_QUOTE_MATCHER;
432 } else if (DOUBLE_QUOTE_MATCHER.matches(firstChar)) {
433 quoteMatcher = DOUBLE_QUOTE_MATCHER;
438 sb.append(quoteMatcher.removeFrom(str.substring(1, str.length() - 1)));
440 return sb.toString();
443 public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
444 if (Strings.isNullOrEmpty(value)) {
445 return ctx.getPublicDefinition().getStatementName();
449 QNameModule qNameModule = null;
450 String localName = null;
452 String[] namesParts = value.split(":");
453 switch (namesParts.length) {
455 localName = namesParts[0];
456 qNameModule = getRootModuleQName(ctx);
459 prefix = namesParts[0];
460 localName = namesParts[1];
461 qNameModule = getModuleQNameByPrefix(ctx, prefix);
462 // in case of unknown statement argument, we're not going to parse it
463 if (qNameModule == null
464 && ctx.getPublicDefinition().getDeclaredRepresentationClass()
465 .isAssignableFrom(UnknownStatementImpl.class)) {
467 qNameModule = getRootModuleQName(ctx);
469 if (qNameModule == null
470 && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
471 ctx = ctx.getOriginalCtx();
472 qNameModule = getModuleQNameByPrefix(ctx, prefix);
477 Preconditions.checkArgument(qNameModule != null,
478 "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
479 ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
480 final QNameModule resultQNameModule;
481 if (qNameModule.getRevision() == null) {
482 resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
485 resultQNameModule = qNameModule;
488 return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
491 public static QNameModule getModuleQNameByPrefix(final StmtContext<?, ?, ?> ctx, final String prefix) {
492 final ModuleIdentifier modId = ctx.getRoot().getFromNamespace(ImpPrefixToModuleIdentifier.class, prefix);
493 final QNameModule qNameModule = ctx.getFromNamespace(ModuleIdentifierToModuleQName.class, modId);
495 if (qNameModule == null && StmtContextUtils.producesDeclared(ctx.getRoot(), SubmoduleStatement.class)) {
496 String moduleName = ctx.getRoot().getFromNamespace(BelongsToPrefixToModuleName.class, prefix);
497 return ctx.getFromNamespace(ModuleNameToModuleQName.class, moduleName);
502 public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
507 final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
508 final QNameModule qNameModule;
510 if (StmtContextUtils.producesDeclared(rootCtx, ModuleStatement.class)) {
511 qNameModule = rootCtx.getFromNamespace(ModuleCtxToModuleQName.class, rootCtx);
512 } else if (StmtContextUtils.producesDeclared(rootCtx, SubmoduleStatement.class)) {
513 final String belongsToModuleName = firstAttributeOf(rootCtx.substatements(), BelongsToStatement.class);
514 qNameModule = rootCtx.getFromNamespace(ModuleNameToModuleQName.class, belongsToModuleName);
519 Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
520 if (qNameModule.getRevision() != null) {
524 return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
528 public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
529 final SchemaNodeIdentifier node) {
530 return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
533 public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
534 return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
535 .isAssignableFrom(UnknownStatementImpl.class);
538 public static Deviation.Deviate parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
539 return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
540 "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
541 ctx.getStatementSourceReference());
544 public static Status parseStatus(final String value) {
547 return Status.CURRENT;
549 return Status.DEPRECATED;
551 return Status.OBSOLETE;
553 LOG.warn("Invalid 'status' statement: {}", value);
558 public static Date getLatestRevision(final Iterable<? extends StmtContext<?, ?, ?>> subStmts) {
559 Date revision = null;
560 for (StmtContext<?, ?, ?> subStmt : subStmts) {
561 if (subStmt.getPublicDefinition().getDeclaredRepresentationClass().isAssignableFrom(RevisionStatement
563 if (revision == null && subStmt.getStatementArgument() != null) {
564 revision = (Date) subStmt.getStatementArgument();
565 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
567 revision = (Date) subStmt.getStatementArgument();
574 public static boolean isModuleIdentifierWithoutSpecifiedRevision(final Object o) {
575 return (o instanceof ModuleIdentifier)
576 && (((ModuleIdentifier) o).getRevision() == SimpleDateFormatUtil.DEFAULT_DATE_IMP || ((ModuleIdentifier) o)
577 .getRevision() == SimpleDateFormatUtil.DEFAULT_BELONGS_TO_DATE);
581 * Replaces illegal characters of QName by the name of the character (e.g.
582 * '?' is replaced by "QuestionMark" etc.).
586 * @return result String
588 public static String replaceIllegalCharsForQName(String string) {
589 string = LEFT_PARENTHESIS_MATCHER.replaceFrom(string, "LeftParenthesis");
590 string = RIGHT_PARENTHESIS_MATCHER.replaceFrom(string, "RightParenthesis");
591 string = AMPERSAND_MATCHER.replaceFrom(string, "Ampersand");
592 string = QUESTION_MARK_MATCHER.replaceFrom(string, "QuestionMark");
597 public static String fixUnicodeScriptPattern(String rawPattern) {
598 for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
600 Pattern.compile(rawPattern);
602 } catch(PatternSyntaxException ex) {
603 LOG.debug("Invalid regex pattern syntax in: {}", rawPattern, ex);
604 if (ex.getMessage().contains("Unknown character script name")) {
605 rawPattern = fixUnknownScripts(ex.getMessage(), rawPattern);
612 LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
616 private static String fixUnknownScripts(final String exMessage, final String rawPattern) {
617 StringBuilder result = new StringBuilder(rawPattern);
618 Matcher matcher = BETWEEN_CURLY_BRACES_PATTERN.matcher(exMessage);
619 if (matcher.find()) {
620 String capturedGroup = matcher.group(1);
621 if (JAVA_UNICODE_BLOCKS.contains(capturedGroup)) {
622 int idx = rawPattern.indexOf("Is" + capturedGroup);
623 result = result.replace(idx, idx + 2, "In");
626 return result.toString();