Bug 6180 - Parser: Backslash double-quote in double-quoted string not recognized
[yangtools.git] / yang / yang-parser-impl / src / main / java / org / opendaylight / yangtools / yang / parser / stmt / rfc6020 / Utils.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
9
10 import static org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils.firstAttributeOf;
11
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;
27 import java.util.Map;
28 import java.util.Set;
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;
68
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()
81             .add("AegeanNumbers")
82             .add("AlchemicalSymbols")
83             .add("AlphabeticPresentationForms")
84             .add("AncientGreekMusicalNotation")
85             .add("AncientGreekNumbers")
86             .add("AncientSymbols")
87             .add("Arabic")
88             .add("ArabicPresentationForms-A")
89             .add("ArabicPresentationForms-B")
90             .add("ArabicSupplement")
91             .add("Armenian")
92             .add("Arrows")
93             .add("Avestan")
94             .add("Balinese")
95             .add("Bamum")
96             .add("BamumSupplement")
97             .add("BasicLatin")
98             .add("Batak")
99             .add("Bengali")
100             .add("BlockElements")
101             .add("Bopomofo")
102             .add("BopomofoExtended")
103             .add("BoxDrawing")
104             .add("Brahmi")
105             .add("BraillePatterns")
106             .add("Buginese")
107             .add("Buhid")
108             .add("ByzantineMusicalSymbols")
109             .add("Carian")
110             .add("Cham")
111             .add("Cherokee")
112             .add("CJKCompatibility")
113             .add("CJKCompatibilityForms")
114             .add("CJKCompatibilityIdeographs")
115             .add("CJKCompatibilityIdeographsSupplement")
116             .add("CJKRadicalsSupplement")
117             .add("CJKStrokes")
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")
130             .add("Coptic")
131             .add("CountingRodNumerals")
132             .add("Cuneiform")
133             .add("CuneiformNumbersandPunctuation")
134             .add("CurrencySymbols")
135             .add("CypriotSyllabary")
136             .add("Cyrillic")
137             .add("CyrillicExtended-A")
138             .add("CyrillicExtended-B")
139             .add("CyrillicSupplementary")
140             .add("Deseret")
141             .add("Devanagari")
142             .add("DevanagariExtended")
143             .add("Dingbats")
144             .add("DominoTiles")
145             .add("EgyptianHieroglyphs")
146             .add("Emoticons")
147             .add("EnclosedAlphanumericSupplement")
148             .add("EnclosedAlphanumerics")
149             .add("EnclosedCJKLettersandMonths")
150             .add("EnclosedIdeographicSupplement")
151             .add("Ethiopic")
152             .add("EthiopicExtended")
153             .add("EthiopicExtended-A")
154             .add("EthiopicSupplement")
155             .add("GeneralPunctuation")
156             .add("GeometricShapes")
157             .add("Georgian")
158             .add("GeorgianSupplement")
159             .add("Glagolitic")
160             .add("Gothic")
161             .add("GreekandCoptic")
162             .add("GreekExtended")
163             .add("Gujarati")
164             .add("Gurmukhi")
165             .add("HalfwidthandFullwidthForms")
166             .add("HangulCompatibilityJamo")
167             .add("HangulJamo")
168             .add("HangulJamoExtended-A")
169             .add("HangulJamoExtended-B")
170             .add("HangulSyllables")
171             .add("Hanunoo")
172             .add("Hebrew")
173             .add("HighPrivateUseSurrogates")
174             .add("HighSurrogates")
175             .add("Hiragana")
176             .add("IdeographicDescriptionCharacters")
177             .add("ImperialAramaic")
178             .add("InscriptionalPahlavi")
179             .add("InscriptionalParthian")
180             .add("IPAExtensions")
181             .add("Javanese")
182             .add("Kaithi")
183             .add("KanaSupplement")
184             .add("Kanbun")
185             .add("Kangxi Radicals")
186             .add("Kannada")
187             .add("Katakana")
188             .add("KatakanaPhoneticExtensions")
189             .add("KayahLi")
190             .add("Kharoshthi")
191             .add("Khmer")
192             .add("KhmerSymbols")
193             .add("Lao")
194             .add("Latin-1Supplement")
195             .add("LatinExtended-A")
196             .add("LatinExtendedAdditional")
197             .add("LatinExtended-B")
198             .add("LatinExtended-C")
199             .add("LatinExtended-D")
200             .add("Lepcha")
201             .add("LetterlikeSymbols")
202             .add("Limbu")
203             .add("LinearBIdeograms")
204             .add("LinearBSyllabary")
205             .add("Lisu")
206             .add("LowSurrogates")
207             .add("Lycian")
208             .add("Lydian")
209             .add("MahjongTiles")
210             .add("Malayalam")
211             .add("Mandaic")
212             .add("MathematicalAlphanumericSymbols")
213             .add("MathematicalOperators")
214             .add("MeeteiMayek")
215             .add("MiscellaneousMathematicalSymbols-A")
216             .add("MiscellaneousMathematicalSymbols-B")
217             .add("MiscellaneousSymbols")
218             .add("MiscellaneousSymbolsandArrows")
219             .add("MiscellaneousSymbolsAndPictographs")
220             .add("MiscellaneousTechnical")
221             .add("ModifierToneLetters")
222             .add("Mongolian")
223             .add("MusicalSymbols")
224             .add("Myanmar")
225             .add("MyanmarExtended-A")
226             .add("NewTaiLue")
227             .add("NKo")
228             .add("NumberForms")
229             .add("Ogham")
230             .add("OlChiki")
231             .add("OldItalic")
232             .add("OldPersian")
233             .add("OldSouthArabian")
234             .add("OldTurkic")
235             .add("OpticalCharacterRecognition")
236             .add("Oriya")
237             .add("Osmanya")
238             .add("Phags-pa")
239             .add("PhaistosDisc")
240             .add("Phoenician")
241             .add("PhoneticExtensions")
242             .add("PhoneticExtensionsSupplement")
243             .add("PlayingCards")
244             .add("PrivateUseArea")
245             .add("Rejang")
246             .add("RumiNumeralSymbols")
247             .add("Runic")
248             .add("Samaritan")
249             .add("Saurashtra")
250             .add("Shavian")
251             .add("Sinhala")
252             .add("SmallFormVariants")
253             .add("SpacingModifierLetters")
254             .add("Specials")
255             .add("Sundanese")
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")
263             .add("SylotiNagri")
264             .add("Syriac")
265             .add("Tagalog")
266             .add("Tagbanwa")
267             .add("Tags")
268             .add("TaiLe")
269             .add("TaiTham")
270             .add("TaiViet")
271             .add("TaiXuanJingSymbols")
272             .add("Tamil")
273             .add("Telugu")
274             .add("Thaana")
275             .add("Thai")
276             .add("Tibetan")
277             .add("Tifinagh")
278             .add("TransportAndMapSymbols")
279             .add("Ugaritic")
280             .add("UnifiedCanadianAboriginalSyllabics")
281             .add("UnifiedCanadianAboriginalSyllabicsExtended")
282             .add("Vai")
283             .add("VariationSelectors")
284             .add("VariationSelectorsSupplement")
285             .add("VedicExtensions")
286             .add("VerticalForms")
287             .add("YiRadicals")
288             .add("YiSyllables")
289             .add("YijingHexagramSymbols").build();
290
291     private static final Map<String, Deviate> KEYWORD_TO_DEVIATE_MAP;
292     static {
293         final Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
294         for (final Deviate deviate : Deviation.Deviate.values()) {
295             keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
296         }
297         KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
298     }
299
300     private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
301         @Override
302         protected XPathFactory initialValue() {
303             return XPathFactory.newInstance();
304         }
305     };
306
307     private Utils() {
308         throw new UnsupportedOperationException();
309     }
310
311     /**
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.
314      */
315     public static void detachFromCurrentThread() {
316         XPATH_FACTORY.remove();
317     }
318
319     public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
320             final String value) {
321         final List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
322
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);
327         }
328
329         final Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
330
331         for (final String keyToken : keyTokens) {
332
333             final SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
334                     Utils.qNameFromArgument(ctx, keyToken));
335             keyNodes.add(keyNode);
336         }
337
338         return keyNodes;
339     }
340
341     private static String trimSingleLastSlashFromXPath(final String path) {
342         return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
343     }
344
345     static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
346         final XPath xPath = XPATH_FACTORY.get().newXPath();
347         xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
348
349         final String trimmed = trimSingleLastSlashFromXPath(path);
350         try {
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);
355         }
356
357         return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
358     }
359
360     public static QName trimPrefix(final QName identifier) {
361         final String prefixedLocalName = identifier.getLocalName();
362         final String[] namesParts = prefixedLocalName.split(":");
363
364         if (namesParts.length == 2) {
365             final String localName = namesParts[1];
366             return QName.create(identifier.getModule(), localName);
367         }
368
369         return identifier;
370     }
371
372     /**
373      *
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.
377      *
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
382      *
383      */
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();
388         } else {
389             final String prefixedLocalName = identifier.getLocalName();
390             final String[] namesParts = prefixedLocalName.split(":");
391
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);
398                 }
399             }
400         }
401         return null;
402     }
403
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))) {
408             try {
409                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
410                 qNames.add(qName);
411             } catch (final Exception e) {
412                 throw new IllegalArgumentException(
413                     String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
414             }
415         }
416
417         return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
418     }
419
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());
425         }
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);
432                 /*
433                  * Unescape escaped double quotes, tabs, new line and backslash
434                  * in the inner string and trim the result.
435                  */
436                 sb.append(innerStr.replace("\\\"", "\"").replace("\\\\", "\\").replace("\\n", "\n")
437                         .replace("\\t", "\t"));
438             } else if (firstChar == '\'' && lastChar == '\'') {
439                 /*
440                  * According to RFC6020 a single quote character cannot occur in
441                  * a single-quoted string, even when preceded by a backslash.
442                  */
443                 sb.append(str.substring(1, str.length() - 1));
444             } else {
445                 sb.append(str);
446             }
447         }
448         return sb.toString();
449     }
450
451     public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
452         if (Strings.isNullOrEmpty(value)) {
453             return ctx.getPublicDefinition().getStatementName();
454         }
455
456         String prefix;
457         QNameModule qNameModule = null;
458         String localName = null;
459
460         final String[] namesParts = value.split(":");
461         switch (namesParts.length) {
462         case 1:
463             localName = namesParts[0];
464             qNameModule = getRootModuleQName(ctx);
465             break;
466         default:
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)) {
474                 localName = value;
475                 qNameModule = getRootModuleQName(ctx);
476             }
477             if (qNameModule == null
478                     && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
479                 ctx = ctx.getOriginalCtx();
480                 qNameModule = getModuleQNameByPrefix(ctx, prefix);
481             }
482             break;
483         }
484
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)
491                 .intern();
492         } else {
493             resultQNameModule = qNameModule;
494         }
495
496         return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
497     }
498
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);
502
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);
506         }
507         return qNameModule;
508     }
509
510     public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
511         if (ctx == null) {
512             return null;
513         }
514
515         final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
516         final QNameModule qNameModule;
517
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);
523         } else {
524             qNameModule = null;
525         }
526
527         Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
528         if (qNameModule.getRevision() != null) {
529             return qNameModule;
530         }
531
532         return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
533     }
534
535     @Nullable
536     public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
537             final SchemaNodeIdentifier node) {
538         return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
539     }
540
541     public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
542         return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
543                 .isAssignableFrom(UnknownStatementImpl.class);
544     }
545
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());
550     }
551
552     public static Status parseStatus(final String value) {
553         switch (value) {
554         case "current":
555             return Status.CURRENT;
556         case "deprecated":
557             return Status.DEPRECATED;
558         case "obsolete":
559             return Status.OBSOLETE;
560         default:
561             LOG.warn("Invalid 'status' statement: {}", value);
562             return null;
563         }
564     }
565
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
570                     .class)) {
571                 if (revision == null && subStmt.getStatementArgument() != null) {
572                     revision = (Date) subStmt.getStatementArgument();
573                 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
574                         (revision) > 0) {
575                     revision = (Date) subStmt.getStatementArgument();
576                 }
577             }
578         }
579         return revision;
580     }
581
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);
586     }
587
588     /**
589      * Replaces illegal characters of QName by the name of the character (e.g.
590      * '?' is replaced by "QuestionMark" etc.).
591      *
592      * @param string
593      *            input String
594      * @return result String
595      */
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");
601
602         return string;
603     }
604
605     public static String fixUnicodeScriptPattern(String rawPattern) {
606         for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
607             try {
608                 Pattern.compile(rawPattern);
609                 return 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);
614                 } else {
615                     return rawPattern;
616                 }
617             }
618         }
619
620         LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
621         return rawPattern;
622     }
623
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");
632             }
633         }
634         return result.toString();
635     }
636
637     public static boolean belongsToTheSameModule(final QName targetStmtQName, final QName sourceStmtQName) {
638         if (targetStmtQName.getModule().equals(sourceStmtQName.getModule())) {
639             return true;
640         }
641         return false;
642     }
643
644     public static boolean isPresenceContainer(final StatementContextBase<?, ?, ?> targetCtx) {
645         if (!targetCtx.getPublicDefinition().equals(Rfc6020Mapping.CONTAINER)) {
646             return false;
647         }
648
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)) {
653                 return true;
654             }
655         }
656
657         return false;
658     }
659 }