Bug 4079: Unable to compile pattern defined in module
[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 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;
25 import java.util.Map;
26 import java.util.Set;
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;
65
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()
80             .add("AegeanNumbers")
81             .add("AlchemicalSymbols")
82             .add("AlphabeticPresentationForms")
83             .add("AncientGreekMusicalNotation")
84             .add("AncientGreekNumbers")
85             .add("AncientSymbols")
86             .add("Arabic")
87             .add("ArabicPresentationForms-A")
88             .add("ArabicPresentationForms-B")
89             .add("ArabicSupplement")
90             .add("Armenian")
91             .add("Arrows")
92             .add("Avestan")
93             .add("Balinese")
94             .add("Bamum")
95             .add("BamumSupplement")
96             .add("BasicLatin")
97             .add("Batak")
98             .add("Bengali")
99             .add("BlockElements")
100             .add("Bopomofo")
101             .add("BopomofoExtended")
102             .add("BoxDrawing")
103             .add("Brahmi")
104             .add("BraillePatterns")
105             .add("Buginese")
106             .add("Buhid")
107             .add("ByzantineMusicalSymbols")
108             .add("Carian")
109             .add("Cham")
110             .add("Cherokee")
111             .add("CJKCompatibility")
112             .add("CJKCompatibilityForms")
113             .add("CJKCompatibilityIdeographs")
114             .add("CJKCompatibilityIdeographsSupplement")
115             .add("CJKRadicalsSupplement")
116             .add("CJKStrokes")
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")
129             .add("Coptic")
130             .add("CountingRodNumerals")
131             .add("Cuneiform")
132             .add("CuneiformNumbersandPunctuation")
133             .add("CurrencySymbols")
134             .add("CypriotSyllabary")
135             .add("Cyrillic")
136             .add("CyrillicExtended-A")
137             .add("CyrillicExtended-B")
138             .add("CyrillicSupplementary")
139             .add("Deseret")
140             .add("Devanagari")
141             .add("DevanagariExtended")
142             .add("Dingbats")
143             .add("DominoTiles")
144             .add("EgyptianHieroglyphs")
145             .add("Emoticons")
146             .add("EnclosedAlphanumericSupplement")
147             .add("EnclosedAlphanumerics")
148             .add("EnclosedCJKLettersandMonths")
149             .add("EnclosedIdeographicSupplement")
150             .add("Ethiopic")
151             .add("EthiopicExtended")
152             .add("EthiopicExtended-A")
153             .add("EthiopicSupplement")
154             .add("GeneralPunctuation")
155             .add("GeometricShapes")
156             .add("Georgian")
157             .add("GeorgianSupplement")
158             .add("Glagolitic")
159             .add("Gothic")
160             .add("GreekandCoptic")
161             .add("GreekExtended")
162             .add("Gujarati")
163             .add("Gurmukhi")
164             .add("HalfwidthandFullwidthForms")
165             .add("HangulCompatibilityJamo")
166             .add("HangulJamo")
167             .add("HangulJamoExtended-A")
168             .add("HangulJamoExtended-B")
169             .add("HangulSyllables")
170             .add("Hanunoo")
171             .add("Hebrew")
172             .add("HighPrivateUseSurrogates")
173             .add("HighSurrogates")
174             .add("Hiragana")
175             .add("IdeographicDescriptionCharacters")
176             .add("ImperialAramaic")
177             .add("InscriptionalPahlavi")
178             .add("InscriptionalParthian")
179             .add("IPAExtensions")
180             .add("Javanese")
181             .add("Kaithi")
182             .add("KanaSupplement")
183             .add("Kanbun")
184             .add("Kangxi Radicals")
185             .add("Kannada")
186             .add("Katakana")
187             .add("KatakanaPhoneticExtensions")
188             .add("KayahLi")
189             .add("Kharoshthi")
190             .add("Khmer")
191             .add("KhmerSymbols")
192             .add("Lao")
193             .add("Latin-1Supplement")
194             .add("LatinExtended-A")
195             .add("LatinExtendedAdditional")
196             .add("LatinExtended-B")
197             .add("LatinExtended-C")
198             .add("LatinExtended-D")
199             .add("Lepcha")
200             .add("LetterlikeSymbols")
201             .add("Limbu")
202             .add("LinearBIdeograms")
203             .add("LinearBSyllabary")
204             .add("Lisu")
205             .add("LowSurrogates")
206             .add("Lycian")
207             .add("Lydian")
208             .add("MahjongTiles")
209             .add("Malayalam")
210             .add("Mandaic")
211             .add("MathematicalAlphanumericSymbols")
212             .add("MathematicalOperators")
213             .add("MeeteiMayek")
214             .add("MiscellaneousMathematicalSymbols-A")
215             .add("MiscellaneousMathematicalSymbols-B")
216             .add("MiscellaneousSymbols")
217             .add("MiscellaneousSymbolsandArrows")
218             .add("MiscellaneousSymbolsAndPictographs")
219             .add("MiscellaneousTechnical")
220             .add("ModifierToneLetters")
221             .add("Mongolian")
222             .add("MusicalSymbols")
223             .add("Myanmar")
224             .add("MyanmarExtended-A")
225             .add("NewTaiLue")
226             .add("NKo")
227             .add("NumberForms")
228             .add("Ogham")
229             .add("OlChiki")
230             .add("OldItalic")
231             .add("OldPersian")
232             .add("OldSouthArabian")
233             .add("OldTurkic")
234             .add("OpticalCharacterRecognition")
235             .add("Oriya")
236             .add("Osmanya")
237             .add("Phags-pa")
238             .add("PhaistosDisc")
239             .add("Phoenician")
240             .add("PhoneticExtensions")
241             .add("PhoneticExtensionsSupplement")
242             .add("PlayingCards")
243             .add("PrivateUseArea")
244             .add("Rejang")
245             .add("RumiNumeralSymbols")
246             .add("Runic")
247             .add("Samaritan")
248             .add("Saurashtra")
249             .add("Shavian")
250             .add("Sinhala")
251             .add("SmallFormVariants")
252             .add("SpacingModifierLetters")
253             .add("Specials")
254             .add("Sundanese")
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")
262             .add("SylotiNagri")
263             .add("Syriac")
264             .add("Tagalog")
265             .add("Tagbanwa")
266             .add("Tags")
267             .add("TaiLe")
268             .add("TaiTham")
269             .add("TaiViet")
270             .add("TaiXuanJingSymbols")
271             .add("Tamil")
272             .add("Telugu")
273             .add("Thaana")
274             .add("Thai")
275             .add("Tibetan")
276             .add("Tifinagh")
277             .add("TransportAndMapSymbols")
278             .add("Ugaritic")
279             .add("UnifiedCanadianAboriginalSyllabics")
280             .add("UnifiedCanadianAboriginalSyllabicsExtended")
281             .add("Vai")
282             .add("VariationSelectors")
283             .add("VariationSelectorsSupplement")
284             .add("VedicExtensions")
285             .add("VerticalForms")
286             .add("YiRadicals")
287             .add("YiSyllables")
288             .add("YijingHexagramSymbols").build();
289
290     private static final Map<String, Deviate> KEYWORD_TO_DEVIATE_MAP;
291     static {
292         Builder<String, Deviate> keywordToDeviateMapBuilder = ImmutableMap.builder();
293         for (Deviate deviate : Deviation.Deviate.values()) {
294             keywordToDeviateMapBuilder.put(deviate.getKeyword(), deviate);
295         }
296         KEYWORD_TO_DEVIATE_MAP = keywordToDeviateMapBuilder.build();
297     }
298
299     private static final ThreadLocal<XPathFactory> XPATH_FACTORY = new ThreadLocal<XPathFactory>() {
300         @Override
301         protected XPathFactory initialValue() {
302             return XPathFactory.newInstance();
303         }
304     };
305
306     private Utils() {
307         throw new UnsupportedOperationException();
308     }
309
310     /**
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.
313      */
314     public static void detachFromCurrentThread() {
315         XPATH_FACTORY.remove();
316     }
317
318     public static Collection<SchemaNodeIdentifier.Relative> transformKeysStringToKeyNodes(final StmtContext<?, ?, ?> ctx,
319             final String value) {
320         List<String> keyTokens = SPACE_SPLITTER.splitToList(value);
321
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);
326         }
327
328         Set<SchemaNodeIdentifier.Relative> keyNodes = new HashSet<>();
329
330         for (String keyToken : keyTokens) {
331
332             SchemaNodeIdentifier.Relative keyNode = (Relative) SchemaNodeIdentifier.Relative.create(false,
333                     Utils.qNameFromArgument(ctx, keyToken));
334             keyNodes.add(keyNode);
335         }
336
337         return keyNodes;
338     }
339
340     private static String trimSingleLastSlashFromXPath(final String path) {
341         return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
342     }
343
344     static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
345         final XPath xPath = XPATH_FACTORY.get().newXPath();
346         xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
347
348         final String trimmed = trimSingleLastSlashFromXPath(path);
349         try {
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);
354         }
355
356         return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
357     }
358
359     public static QName trimPrefix(final QName identifier) {
360         String prefixedLocalName = identifier.getLocalName();
361         String[] namesParts = prefixedLocalName.split(":");
362
363         if (namesParts.length == 2) {
364             String localName = namesParts[1];
365             return QName.create(identifier.getModule(), localName);
366         }
367
368         return identifier;
369     }
370
371     /**
372      *
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.
376      *
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
381      *
382      */
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();
387         } else {
388             String prefixedLocalName = identifier.getLocalName();
389             String[] namesParts = prefixedLocalName.split(":");
390
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);
397                 }
398             }
399         }
400         return null;
401     }
402
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))) {
407             try {
408                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
409                 qNames.add(qName);
410             } catch (Exception e) {
411                 throw new IllegalArgumentException(
412                     String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
413             }
414         }
415
416         return SchemaNodeIdentifier.create(qNames, PATH_ABS.matcher(path).matches());
417     }
418
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());
424         }
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;
433             } else {
434                 sb.append(str);
435                 continue;
436             }
437             sb.append(quoteMatcher.removeFrom(str.substring(1, str.length() - 1)));
438         }
439         return sb.toString();
440     }
441
442     public static QName qNameFromArgument(StmtContext<?, ?, ?> ctx, final String value) {
443         if (Strings.isNullOrEmpty(value)) {
444             return ctx.getPublicDefinition().getStatementName();
445         }
446
447         String prefix;
448         QNameModule qNameModule = null;
449         String localName = null;
450
451         String[] namesParts = value.split(":");
452         switch (namesParts.length) {
453         case 1:
454             localName = namesParts[0];
455             qNameModule = getRootModuleQName(ctx);
456             break;
457         default:
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)) {
465                 localName = value;
466                 qNameModule = getRootModuleQName(ctx);
467             }
468             if (qNameModule == null
469                     && Iterables.getLast(ctx.getCopyHistory()) == StmtContext.TypeOfCopy.ADDED_BY_AUGMENTATION) {
470                 ctx = ctx.getOriginalCtx();
471                 qNameModule = getModuleQNameByPrefix(ctx, prefix);
472             }
473             break;
474         }
475
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)
482                 .intern();
483         } else {
484             resultQNameModule = qNameModule;
485         }
486
487         return ctx.getFromNamespace(QNameCacheNamespace.class, QName.create(resultQNameModule, localName));
488     }
489
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);
493
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);
497         }
498         return qNameModule;
499     }
500
501     public static QNameModule getRootModuleQName(final StmtContext<?, ?, ?> ctx) {
502         if (ctx == null) {
503             return null;
504         }
505
506         final StmtContext<?, ?, ?> rootCtx = ctx.getRoot();
507         final QNameModule qNameModule;
508
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);
514         } else {
515             qNameModule = null;
516         }
517
518         Preconditions.checkArgument(qNameModule != null, "Failed to look up root QNameModule for %s", ctx);
519         if (qNameModule.getRevision() != null) {
520             return qNameModule;
521         }
522
523         return QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV).intern();
524     }
525
526     @Nullable
527     public static StatementContextBase<?, ?, ?> findNode(final StmtContext<?, ?, ?> rootStmtCtx,
528             final SchemaNodeIdentifier node) {
529         return (StatementContextBase<?, ?, ?>) rootStmtCtx.getFromNamespace(SchemaNodeIdentifierBuildNamespace.class, node);
530     }
531
532     public static boolean isUnknownNode(final StmtContext<?, ?, ?> stmtCtx) {
533         return stmtCtx.getPublicDefinition().getDeclaredRepresentationClass()
534                 .isAssignableFrom(UnknownStatementImpl.class);
535     }
536
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());
541     }
542
543     public static Status parseStatus(final String value) {
544         switch (value) {
545         case "current":
546             return Status.CURRENT;
547         case "deprecated":
548             return Status.DEPRECATED;
549         case "obsolete":
550             return Status.OBSOLETE;
551         default:
552             LOG.warn("Invalid 'status' statement: {}", value);
553             return null;
554         }
555     }
556
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
561                     .class)) {
562                 if (revision == null && subStmt.getStatementArgument() != null) {
563                     revision = (Date) subStmt.getStatementArgument();
564                 } else if (subStmt.getStatementArgument() != null && ((Date) subStmt.getStatementArgument()).compareTo
565                         (revision) > 0) {
566                     revision = (Date) subStmt.getStatementArgument();
567                 }
568             }
569         }
570         return revision;
571     }
572
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);
577     }
578
579     /**
580      * Replaces illegal characters of QName by the name of the character (e.g.
581      * '?' is replaced by "QuestionMark" etc.).
582      *
583      * @param string
584      *            input String
585      * @return result String
586      */
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");
592
593         return string;
594     }
595
596     public static String fixUnicodeScriptPattern(String rawPattern) {
597         for (int i = 0; i < UNICODE_SCRIPT_FIX_COUNTER; i++) {
598             try {
599                 Pattern.compile(rawPattern);
600                 return 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);
605                 } else {
606                     return rawPattern;
607                 }
608             }
609         }
610
611         LOG.warn("Regex pattern could not be fixed: {}", rawPattern);
612         return rawPattern;
613     }
614
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");
623             }
624         }
625         return result.toString();
626     }
627 }