All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.saxon.regex.REMatcher Maven / Gradle / Ivy

There is a newer version: 10.5
Show newest version
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2013 Saxonica Limited.
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Originally part of Apache's Jakarta project (downloaded January 2012),
 * this file has been extensively modified for integration into Saxon by
 * Michael Kay, Saxonica.
 */

package net.sf.saxon.regex;


import net.sf.saxon.tree.util.FastStringBuffer;
import net.sf.saxon.z.IntHashMap;
import net.sf.saxon.z.IntHashSet;
import net.sf.saxon.z.IntPredicate;
import net.sf.saxon.z.IntSet;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


/**
 * RE is an efficient, lightweight regular expression evaluator/matcher
 * class. Regular expressions are pattern descriptions which enable
 * sophisticated matching of strings.  In addition to being able to
 * match a string against a pattern, you can also extract parts of the
 * match.  This is especially useful in text parsing! Details on the
 * syntax of regular expression patterns are given below.
 * 

*

* To compile a regular expression (RE), you can simply construct an RE * matcher object from the string specification of the pattern, like this: *

*

 *  RE r = new RE("a*b");
 * 
*

*

* Once you have done this, you can call either of the RE.match methods to * perform matching on a String. For example: *

*

 *  boolean matched = r.match("aaaab");
 * 
*

* will cause the boolean matched to be set to true because the * pattern "a*b" matches the string "aaaab". *

*

* If you were interested in the number of a's which matched the * first part of our example expression, you could change the expression to * "(a*)b". Then when you compiled the expression and matched it against * something like "xaaaab", you would get results like this: *

*

 *  RE r = new RE("(a*)b");                  // Compile expression
 *  boolean matched = r.match("xaaaab");     // Match against "xaaaab"
 *
 *  String wholeExpr = r.getParen(0);        // wholeExpr will be 'aaaab'
 *  String insideParens = r.getParen(1);     // insideParens will be 'aaaa'
 *
 *  int startWholeExpr = r.getParenStart(0); // startWholeExpr will be index 1
 *  int endWholeExpr = r.getParenEnd(0);     // endWholeExpr will be index 6
 *  int lenWholeExpr = r.getParenLength(0);  // lenWholeExpr will be 5
 *
 *  int startInside = r.getParenStart(1);    // startInside will be index 1
 *  int endInside = r.getParenEnd(1);        // endInside will be index 5
 *  int lenInside = r.getParenLength(1);     // lenInside will be 4
 * 
*

* You can also refer to the contents of a parenthesized expression * within a regular expression itself. This is called a * 'backreference'. The first backreference in a regular expression is * denoted by \1, the second by \2 and so on. So the expression: *

*

 *  ([0-9]+)=\1
 * 
*

* will match any string of the form n=n (like 0=0 or 2=2). *

*

* The full regular expression syntax accepted by RE is as defined in the XSD 1.1 * specification, modified by the XPath 2.0 or 3.0 specifications. *

*

* Line terminators *
* A line terminator is a one- or two-character sequence that marks * the end of a line of the input character sequence. The following * are recognized as line terminators: *

    *
  • A newline (line feed) character ('\n'),
  • *
  • A carriage-return character followed immediately by a newline character ("\r\n"),
  • *
  • A standalone carriage-return character ('\r'),
  • *
  • A next-line character ('\u0085'),
  • *
  • A line-separator character ('\u2028'), or
  • *
  • A paragraph-separator character ('\u2029).
  • *
*

*

* RE runs programs compiled by the RECompiler class. But the RE * matcher class does not include the actual regular expression compiler * for reasons of efficiency. In fact, if you want to pre-compile one * or more regular expressions, the 'recompile' class can be invoked * from the command line to produce compiled output like this: *

*

 *    // Pre-compiled regular expression "a*b"
 *    char[] re1Instructions =
 *    {
 *        0x007c, 0x0000, 0x001a, 0x007c, 0x0000, 0x000d, 0x0041,
 *        0x0001, 0x0004, 0x0061, 0x007c, 0x0000, 0x0003, 0x0047,
 *        0x0000, 0xfff6, 0x007c, 0x0000, 0x0003, 0x004e, 0x0000,
 *        0x0003, 0x0041, 0x0001, 0x0004, 0x0062, 0x0045, 0x0000,
 *        0x0000,
 *    };
 *
 *
 *    REProgram re1 = new REProgram(re1Instructions);
 * 
*

* You can then construct a regular expression matcher (RE) object from * the pre-compiled expression re1 and thus avoid the overhead of * compiling the expression at runtime. If you require more dynamic * regular expressions, you can construct a single RECompiler object and * re-use it to compile each expression. Similarly, you can change the * program run by a given matcher object at any time. However, RE and * RECompiler are not threadsafe (for efficiency reasons, and because * requiring thread safety in this class is deemed to be a rare * requirement), so you will need to construct a separate compiler or * matcher object for each thread (unless you do thread synchronization * yourself). Once expression compiled into the REProgram object, REProgram * can be safely shared across multiple threads and RE objects. *

*


*

* * ISSUES: *

*

  • Not *all* possibilities are considered for greediness when backreferences * are involved (as POSIX suggests should be the case). The POSIX RE * "(ac*)c*d[ac]*\1", when matched against "acdacaa" should yield a match * of acdacaa where \1 is "a". This is not the case in this RE package, * and actually Perl doesn't go to this extent either! Until someone * actually complains about this, I'm not sure it's worth "fixing". * If it ever is fixed, test #137 in RETest.txt should be updated.
  • * *

    *

    This library is based on the Apache Jakarta regex library as downloaded * on 3 January 2012. Changes have been made to make the grammar and semantics conform to XSD * and XPath rules; these changes are listed in source code comments in the * RECompiler source code module.

    *
    * * @author Jonathan Locke * @author Tobias Schäfer * @author Michael Kay * @see RECompiler */ public class REMatcher { // Limits static final int MAX_PAREN = 16; // Number of paren pairs // State of current program REProgram program; // Compiled regular expression 'program' UnicodeString search; // The string being matched against int matchFlags; // Match behaviour flags int maxParen = MAX_PAREN; // Parenthesized subexpressions int parenCount; // Number of subexpressions matched (num open parens + 1) int[] startn; // Lazy-alloced array of sub-expression starts int[] endn; // Lazy-alloced array of sub-expression ends // Backreferences int[] startBackref; // Lazy-alloced array of backref starts int[] endBackref; // Lazy-alloced array of backref ends IntHashMap history; // Tracks progress of a match operation // Key is an integer offset in the source string // Value is a set of instructions that have visited this offset Operation[] instructions; boolean anchoredMatch; /** * Construct a matcher for a pre-compiled regular expression from program * (bytecode) data. * * @param program Compiled regular expression program * @see RECompiler */ public REMatcher(REProgram program) { setProgram(program); } /** * Sets the current regular expression program used by this matcher object. * * @param program Regular expression program compiled by RECompiler. * @see RECompiler * @see REProgram */ public void setProgram(REProgram program) { this.program = program; if (program != null && program.maxParens != -1) { this.instructions = program.instructions; this.maxParen = program.maxParens; } else { this.maxParen = MAX_PAREN; } } /** * Returns the current regular expression program in use by this matcher object. * * @return Regular expression program * @see #setProgram */ public REProgram getProgram() { return program; } /** * Returns the number of parenthesized subexpressions available after a successful match. * * @return Number of available parenthesized subexpressions */ public int getParenCount() { return parenCount; } /** * Gets the contents of a parenthesized subexpression after a successful match. * * @param which Nesting level of subexpression * @return String */ public UnicodeString getParen(int which) { int start; if (which < parenCount && (start = getParenStart(which)) >= 0) { return search.substring(start, getParenEnd(which)); } return null; } /** * Returns the start index of a given paren level. * * @param which Nesting level of subexpression * @return String index */ public final int getParenStart(int which) { if (which < startn.length) { return startn[which]; } return -1; } /** * Returns the end index of a given paren level. * * @param which Nesting level of subexpression * @return String index */ public final int getParenEnd(int which) { if (which < endn.length) { return endn[which]; } return -1; } /** * Returns the length of a given paren level. * * @param which Nesting level of subexpression * @return Number of characters in the parenthesized subexpression */ public final int getParenLength(int which) { if (which < startn.length) { return getParenEnd(which) - getParenStart(which); } return -1; } /** * Sets the start of a paren level * * @param which Which paren level * @param i Index in input array */ protected final void setParenStart(int which, int i) { while (which > startn.length - 1) { int[] s2 = new int[startn.length*2]; System.arraycopy(startn, 0, s2, 0, startn.length); Arrays.fill(s2, startn.length, s2.length, -1); startn = s2; } startn[which] = i; } /** * Sets the end of a paren level * * @param which Which paren level * @param i Index in input array */ protected final void setParenEnd(int which, int i) { while (which > endn.length - 1) { int[] e2 = new int[endn.length*2]; System.arraycopy(endn, 0, e2, 0, endn.length); Arrays.fill(e2, endn.length, e2.length, -1); endn = e2; } endn[which] = i; } /** * Throws an Error representing an internal error condition probably resulting * from a bug in the regular expression compiler (or possibly data corruption). * In practice, this should be very rare. * * @param s Error description */ protected void internalError(String s) throws Error { throw new Error("RE internal error: " + s); } /** * Try to match a string against a subset of nodes in the program * * @param firstNode Node to start at in program * @param lastNode Last valid node (used for matching a subexpression without * matching the rest of the program as well). * @param idx Starting position in character array * @return Final input array index if match succeeded. -1 if not. */ int matchNodes(int firstNode, int lastNode, int idx) { // TODO: all the tests seem to pass with the two-arg version of the method, and the 3-arg version seems // illogical: what is supposed to happen when node>=lastNode other than a program crash? return matchNodes(firstNode, idx); // // Loop while node is valid // int idxNew; // for (int node = firstNode; node < lastNode; ) { // Operation op = instructions[node]; // idxNew = op.exec(this, node, idx); // if (idxNew != -1) { // idx = idxNew; // } // switch (op.nextAction(idxNew)) { // case Operation.ACTION_RETURN: // return idxNew; // case Operation.ACTION_ADVANCE_TO_NEXT: // node = op.next; // continue; // case Operation.ACTION_ADVANCE_TO_FOLLOWING: // node++; // continue; // case Operation.ACTION_ADVANCE_TO_NEXT_NEXT: // node = instructions[op.next].next; // continue; // default: // internalError("Unknown action"); // } // break; // } // // // We should never end up here // internalError("Corrupt program"); // return -1; } /** * Try to match a string against a subset of nodes in the program. This version * has no lastNode argument (which hopefully saves a bit of stack space in the deep recursion) * * @param node Node to start at in program * @param idx Starting position in character array * @return Final input array index if match succeeded. -1 if not. */ int matchNodes(int node, int idx) { // Loop while node is valid int idxNew; while (true) { Operation op = instructions[node]; idxNew = op.exec(this, node, idx); if (idxNew != -1) { idx = idxNew; } //System.err.println("At char " + idxNew + " with instruction " + node); switch (op.nextAction(idxNew)) { case Operation.ACTION_RETURN: return idxNew; case Operation.ACTION_ADVANCE_TO_NEXT: node = op.next; continue; case Operation.ACTION_ADVANCE_TO_FOLLOWING: node++; continue; case Operation.ACTION_ADVANCE_TO_NEXT_NEXT: node = instructions[op.next].next; continue; default: internalError("Unknown action"); } break; } // We should never end up here internalError("Corrupt program"); return -1; } /** * Ask whether a particular node has previously visited a particular position * in the input string * * @param idx the position in the input string * @param node the instruction node * @return true if this is not the first visit by this instruction to this node */ boolean beenHereBefore(int idx, int node) { // TODO: this mechanism succeeds in its purpose of preventing an infinite number of matches // of zero-length strings, but it is incorrect: the state of the machine does not only depend // on the current instruction and the position in the input string, but also on the state of the stack. if (history == null) { history = new IntHashMap(Math.max(search.length(), 128)); } IntSet previousVisitors = history.get(idx); if (previousVisitors != null && previousVisitors.contains(node)) { return true; } else { if (previousVisitors == null) { previousVisitors = new IntHashSet(4); history.put(idx, previousVisitors); } previousVisitors.add(node); return false; } } /** * Match the current regular expression program against the current * input string, starting at index i of the input string. This method * is only meant for internal use. * * @param i The input string index to start matching at * @param anchored true if the regex must match all characters up to the end of the string * @return True if the input matched the expression */ protected boolean matchAt(int i, boolean anchored) { // Initialize start pointer, paren cache and paren count startn = new int[3]; startn[0] = startn[1] = startn[2] = -1; endn = new int[3]; endn[0] = endn[1] = endn[2] = -1; parenCount = 1; //history = new IntHashMap(search.length()); anchoredMatch = anchored; setParenStart(0, i); // Allocate backref arrays (unless optimizations indicate otherwise) if ((program.optimizationFlags & REProgram.OPT_HASBACKREFS) != 0) { startBackref = new int[maxParen]; endBackref = new int[maxParen]; } // Match against string int idx; if ((idx = matchNodes(0, i)) != -1) { setParenEnd(0, idx); return true; } // Didn't match parenCount = 0; return false; } /** * Tests whether the regex matches a string in its entirety, anchored * at both ends * @param search the string to be matched * @return true if the regex matches the whols string */ public boolean anchoredMatch(UnicodeString search) { this.search = search; return matchAt(0, true); } /** * Matches the current regular expression program against a character array, * starting at a given index. * * @param search String to match against * @param i Index to start searching at * @return True if string matched */ public boolean match(UnicodeString search, int i) { //System.err.println("Matching '" + search + "'"); // There is no compiled program to search with! if (program == null) { // This should be uncommon enough to be an error case rather // than an exception (which would have to be handled everywhere) internalError("No RE program to run!"); } // Save string to search this.search = search; // Can we optimize the search by looking for new lines? if ((program.optimizationFlags & REProgram.OPT_HASBOL) == REProgram.OPT_HASBOL) { // Non multi-line matching with BOL: Must match at '0' index if (!program.flags.isMultiLine()) { return i == 0 && matchAt(i, false); } // Multi-line matching with BOL: Seek to next line for (; !search.isEnd(i); i++) { // Skip if we are at the beginning of the line if (isNewline(i)) { continue; } // Match at the beginning of the line if (matchAt(i, false)) { return true; } // Skip to the end of line for (; !search.isEnd(i); i++) { if (isNewline(i)) { break; } } } return false; } // Can we optimize the search by looking for a prefix string? if (program.prefix == null) { if (program.initialCharClass != null) { // no prefix known; but the first character must match a predicate IntPredicate pred = program.initialCharClass; for (; !search.isEnd(i); i++) { if (pred.matches(search.charAt(i))) { if (matchAt(i, false)) { return true; } } } return false; } // Unprefixed matching must try for a match at each character for (; !search.isEnd(i - 1); i++) { // Try a match at index i if (matchAt(i, false)) { return true; } } return false; } else { // Prefix-anchored matching is possible UnicodeString prefix = program.prefix; for (; !search.isEnd(i + prefix.length() - 1); i++) { int j = i; int k = 0; if (program.flags.isCaseIndependent()) { do { // If there's a mismatch of any character in the prefix, give up } while (equalCaseBlind(search.charAt(j++), prefix.charAt(k++)) && k < prefix.length()); } else { do { // If there's a mismatch of any character in the prefix, give up } while ((search.charAt(j++) == prefix.charAt(k++)) && k < prefix.length()); } // See if the whole prefix string matched if (k == prefix.length()) { // We matched the full prefix at firstChar, so try it if (matchAt(i, false)) { return true; } } } return false; } } /** * Matches the current regular expression program against a String. * * @param search String to match against * @return True if string matched */ public boolean match(String search) { return match(UnicodeString.makeUnicodeString(search), 0); } /** * Splits a string into an array of strings on regular expression boundaries. * This function works the same way as the Perl function of the same name. * Given a regular expression of "[ab]+" and a string to split of * "xyzzyababbayyzabbbab123", the result would be the array of Strings * "[xyzzy, yyz, 123]". *

    *

    Please note that the first string in the resulting array may be an empty * string. This happens when the very first character of input string is * matched by the pattern. * * @param s String to split on this regular exression * @return Array of strings */ public List split(UnicodeString s) { // Create new vector List v = new ArrayList(); // Start at position 0 and search the whole string int pos = 0; int len = s.length(); // Try a match at each position while (pos < len && match(s, pos)) { // Get start of match int start = getParenStart(0); // Get end of match int newpos = getParenEnd(0); // Check if no progress was made if (newpos == pos) { v.add(s.substring(pos, start + 1)); newpos++; } else { v.add(s.substring(pos, start)); } // Move to new position pos = newpos; } // Push remainder even if it's empty UnicodeString remainder = s.substring(pos, len); v.add(remainder); // Return the list return v; } /** * Substitutes a string for this regular expression in another string. * This method works like the Perl function of the same name. * Given a regular expression of "a*b", a String to substituteIn of * "aaaabfooaaabgarplyaaabwackyb" and the substitution String "-", the * resulting String returned by subst would be "-foo-garply-wacky-". *

    * It is also possible to reference the contents of a parenthesized expression * with $0, $1, ... $9. A regular expression of "http://[\\.\\w\\-\\?/~_@&=%]+", * a String to substituteIn of "visit us: http://www.apache.org!" and the * substitution String "<a href=\"$0\">$0</a>", the resulting String * returned by subst would be * "visit us: <a href=\"http://www.apache.org\">http://www.apache.org</a>!". *

    * Note: $0 represents the whole match. * * @param in String to substitute within * @param replacement String to substitute for matches of this regular expression * @return The string substituteIn with zero or more occurrences of the current * regular expression replaced with the substitution String (if this regular * expression object doesn't match at any position, the original String is returned * unchanged). */ public CharSequence subst(UnicodeString in, UnicodeString replacement) { // String to return FastStringBuffer sb = new FastStringBuffer(in.length() * 2); // Start at position 0 and search the whole string int pos = 0; int len = in.length(); // Try a match at each position while (pos < len && match(in, pos)) { // Append chars from input string before match for (int i = pos; i < getParenStart(0); i++) { sb.appendWideChar(in.charAt(i)); } if (!program.flags.isLiteral()) { // Process references to captured substrings int maxCapture = getParenCount() - 1; for (int i = 0; i < replacement.length(); i++) { int ch = replacement.charAt(i); if (ch == '\\') { ch = replacement.charAt(++i); if (ch == '\\' || ch == '$') { sb.append((char) ch); } else { throw new RESyntaxException("Invalid escape in replacement string"); } } else if (ch == '$') { ch = replacement.charAt(++i); if (!(ch >= '0' && ch <= '9')) { throw new RESyntaxException("$ in replacement must be followed by a digit"); } int n = (ch - '0'); if (maxCapture <= 9) { if (maxCapture >= n) { UnicodeString captured = getParen(n); if (captured != null) { for (int j = 0; j < captured.length(); j++) { sb.appendWideChar(captured.charAt(j)); } } } else { // append a zero-length string (no-op) } } else { while (true) { if (i >= replacement.length()) { break; } ch = replacement.charAt(++i); if (ch >= '0' && ch <= '9') { int m = n * 10 + (ch - '0'); if (m > maxCapture) { i--; break; } else { n = m; } } else { i--; break; } } UnicodeString captured = getParen(n); for (int j = 0; j < captured.length(); j++) { sb.appendWideChar(captured.charAt(j)); } } } else { sb.appendWideChar(ch); } } } else { // Append substitution without processing backreferences for (int i = 0; i < replacement.length(); i++) { sb.appendWideChar(replacement.charAt(i)); } } // Move forward, skipping past match int newpos = getParenEnd(0); // We always want to make progress! if (newpos == pos) { newpos++; } // Try new position pos = newpos; } // If there's remaining input, append it for (int i = pos; i < len; i++) { sb.appendWideChar(in.charAt(i)); } // Return string buffer return sb.condense(); } /** * Test whether the character at a given position is a newline * * @param i the position of the character to be tested * @return true if character at i-th position in the search string is a newline */ boolean isNewline(int i) { return search.charAt(i) == '\n'; } /** * Compares two characters ignoring case. * * @param c1 first character to compare. * @param c2 second character to compare. * @return true the first character is equal to the second ignoring case. */ boolean equalCaseBlind(int c1, int c2) { if (c1 == c2) { return true; } for (int v : CaseVariants.getCaseVariants(c2)) { if (c1 == v) { return true; } } return false; } }





    © 2015 - 2024 Weber Informatics LLC | Privacy Policy