Rule.java

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

  17. package org.apache.commons.codec.language.bm;

  18. import java.io.InputStream;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collections;
  22. import java.util.Comparator;
  23. import java.util.EnumMap;
  24. import java.util.HashMap;
  25. import java.util.HashSet;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Scanner;
  29. import java.util.Set;
  30. import java.util.regex.Matcher;
  31. import java.util.regex.Pattern;

  32. import org.apache.commons.codec.language.bm.Languages.LanguageSet;

  33. /**
  34.  * A phoneme rule.
  35.  * <p>
  36.  * Rules have a pattern, left context, right context, output phoneme, set of languages for which they apply
  37.  * and a logical flag indicating if all languages must be in play. A rule matches if:
  38.  * <ul>
  39.  * <li>the pattern matches at the current position</li>
  40.  * <li>the string up until the beginning of the pattern matches the left context</li>
  41.  * <li>the string from the end of the pattern matches the right context</li>
  42.  * <li>logical is ALL and all languages are in scope; or</li>
  43.  * <li>logical is any other value and at least one language is in scope</li>
  44.  * </ul>
  45.  * <p>
  46.  * Rules are typically generated by parsing rules resources. In normal use, there will be no need for the user
  47.  * to explicitly construct their own.
  48.  * <p>
  49.  * Rules are immutable and thread-safe.
  50.  * <p>
  51.  * <b>Rules resources</b>
  52.  * <p>
  53.  * Rules are typically loaded from resource files. These are UTF-8 encoded text files. They are systematically
  54.  * named following the pattern:
  55.  * <blockquote>org/apache/commons/codec/language/bm/${NameType#getName}_${RuleType#getName}_${language}.txt</blockquote>
  56.  * <p>
  57.  * The format of these resources is the following:
  58.  * <ul>
  59.  * <li><b>Rules:</b> whitespace separated, double-quoted strings. There should be 4 columns to each row, and these
  60.  * will be interpreted as:
  61.  * <ol>
  62.  * <li>pattern</li>
  63.  * <li>left context</li>
  64.  * <li>right context</li>
  65.  * <li>phoneme</li>
  66.  * </ol>
  67.  * </li>
  68.  * <li><b>End-of-line comments:</b> Any occurrence of '//' will cause all text following on that line to be discarded
  69.  * as a comment.</li>
  70.  * <li><b>Multi-line comments:</b> Any line starting with '/*' will start multi-line commenting mode. This will skip
  71.  * all content until a line ending in '*' and '/' is found.</li>
  72.  * <li><b>Blank lines:</b> All blank lines will be skipped.</li>
  73.  * </ul>
  74.  *
  75.  * @since 1.6
  76.  * @version $Id: Rule.java 1760691 2016-09-14 12:14:26Z jochen $
  77.  */
  78. public class Rule {

  79.     public static final class Phoneme implements PhonemeExpr {
  80.         public static final Comparator<Phoneme> COMPARATOR = new Comparator<Phoneme>() {
  81.             @Override
  82.             public int compare(final Phoneme o1, final Phoneme o2) {
  83.                 for (int i = 0; i < o1.phonemeText.length(); i++) {
  84.                     if (i >= o2.phonemeText.length()) {
  85.                         return +1;
  86.                     }
  87.                     final int c = o1.phonemeText.charAt(i) - o2.phonemeText.charAt(i);
  88.                     if (c != 0) {
  89.                         return c;
  90.                     }
  91.                 }

  92.                 if (o1.phonemeText.length() < o2.phonemeText.length()) {
  93.                     return -1;
  94.                 }

  95.                 return 0;
  96.             }
  97.         };

  98.         private final StringBuilder phonemeText;
  99.         private final Languages.LanguageSet languages;

  100.         public Phoneme(final CharSequence phonemeText, final Languages.LanguageSet languages) {
  101.             this.phonemeText = new StringBuilder(phonemeText);
  102.             this.languages = languages;
  103.         }

  104.         public Phoneme(final Phoneme phonemeLeft, final Phoneme phonemeRight) {
  105.             this(phonemeLeft.phonemeText, phonemeLeft.languages);
  106.             this.phonemeText.append(phonemeRight.phonemeText);
  107.         }

  108.         public Phoneme(final Phoneme phonemeLeft, final Phoneme phonemeRight, final Languages.LanguageSet languages) {
  109.             this(phonemeLeft.phonemeText, languages);
  110.             this.phonemeText.append(phonemeRight.phonemeText);
  111.         }

  112.         public Phoneme append(final CharSequence str) {
  113.             this.phonemeText.append(str);
  114.             return this;
  115.         }

  116.         public Languages.LanguageSet getLanguages() {
  117.             return this.languages;
  118.         }

  119.         @Override
  120.         public Iterable<Phoneme> getPhonemes() {
  121.             return Collections.singleton(this);
  122.         }

  123.         public CharSequence getPhonemeText() {
  124.             return this.phonemeText;
  125.         }

  126.         /**
  127.          * Deprecated since 1.9.
  128.          *
  129.          * @param right the Phoneme to join
  130.          * @return a new Phoneme
  131.          * @deprecated since 1.9
  132.          */
  133.         @Deprecated
  134.         public Phoneme join(final Phoneme right) {
  135.             return new Phoneme(this.phonemeText.toString() + right.phonemeText.toString(),
  136.                                this.languages.restrictTo(right.languages));
  137.         }

  138.         /**
  139.          * Returns a new Phoneme with the same text but a union of its
  140.          * current language set and the given one.
  141.          *
  142.          * @param lang the language set to merge
  143.          * @return a new Phoneme
  144.          */
  145.         public Phoneme mergeWithLanguage(final LanguageSet lang) {
  146.           return new Phoneme(this.phonemeText.toString(), this.languages.merge(lang));
  147.         }

  148.         @Override
  149.         public String toString() {
  150.           return phonemeText.toString() + "[" + languages + "]";
  151.         }
  152.     }

  153.     public interface PhonemeExpr {
  154.         Iterable<Phoneme> getPhonemes();
  155.     }

  156.     public static final class PhonemeList implements PhonemeExpr {
  157.         private final List<Phoneme> phonemes;

  158.         public PhonemeList(final List<Phoneme> phonemes) {
  159.             this.phonemes = phonemes;
  160.         }

  161.         @Override
  162.         public List<Phoneme> getPhonemes() {
  163.             return this.phonemes;
  164.         }
  165.     }

  166.     /**
  167.      * A minimal wrapper around the functionality of Pattern that we use, to allow for alternate implementations.
  168.      */
  169.     public interface RPattern {
  170.         boolean isMatch(CharSequence input);
  171.     }

  172.     public static final RPattern ALL_STRINGS_RMATCHER = new RPattern() {
  173.         @Override
  174.         public boolean isMatch(final CharSequence input) {
  175.             return true;
  176.         }
  177.     };

  178.     public static final String ALL = "ALL";

  179.     private static final String DOUBLE_QUOTE = "\"";

  180.     private static final String HASH_INCLUDE = "#include";

  181.     private static final Map<NameType, Map<RuleType, Map<String, Map<String, List<Rule>>>>> RULES =
  182.             new EnumMap<NameType, Map<RuleType, Map<String, Map<String, List<Rule>>>>>(NameType.class);

  183.     static {
  184.         for (final NameType s : NameType.values()) {
  185.             final Map<RuleType, Map<String, Map<String, List<Rule>>>> rts =
  186.                     new EnumMap<RuleType, Map<String, Map<String, List<Rule>>>>(RuleType.class);

  187.             for (final RuleType rt : RuleType.values()) {
  188.                 final Map<String, Map<String, List<Rule>>> rs = new HashMap<String, Map<String, List<Rule>>>();

  189.                 final Languages ls = Languages.getInstance(s);
  190.                 for (final String l : ls.getLanguages()) {
  191.                     final Scanner scanner = createScanner(s, rt, l);
  192.                     try {
  193.                         rs.put(l, parseRules(scanner, createResourceName(s, rt, l)));
  194.                     } catch (final IllegalStateException e) {
  195.                         throw new IllegalStateException("Problem processing " + createResourceName(s, rt, l), e);
  196.                     } finally {
  197.                         scanner.close();
  198.                     }
  199.                 }
  200.                 if (!rt.equals(RuleType.RULES)) {
  201.                     final Scanner scanner = createScanner(s, rt, "common");
  202.                     try {
  203.                         rs.put("common", parseRules(scanner, createResourceName(s, rt, "common")));
  204.                     } finally {
  205.                         scanner.close();
  206.                     }
  207.                 }

  208.                 rts.put(rt, Collections.unmodifiableMap(rs));
  209.             }

  210.             RULES.put(s, Collections.unmodifiableMap(rts));
  211.         }
  212.     }

  213.     private static boolean contains(final CharSequence chars, final char input) {
  214.         for (int i = 0; i < chars.length(); i++) {
  215.             if (chars.charAt(i) == input) {
  216.                 return true;
  217.             }
  218.         }
  219.         return false;
  220.     }

  221.     private static String createResourceName(final NameType nameType, final RuleType rt, final String lang) {
  222.         return String.format("org/apache/commons/codec/language/bm/%s_%s_%s.txt",
  223.                              nameType.getName(), rt.getName(), lang);
  224.     }

  225.     private static Scanner createScanner(final NameType nameType, final RuleType rt, final String lang) {
  226.         final String resName = createResourceName(nameType, rt, lang);
  227.         final InputStream rulesIS = Languages.class.getClassLoader().getResourceAsStream(resName);

  228.         if (rulesIS == null) {
  229.             throw new IllegalArgumentException("Unable to load resource: " + resName);
  230.         }

  231.         return new Scanner(rulesIS, ResourceConstants.ENCODING);
  232.     }

  233.     private static Scanner createScanner(final String lang) {
  234.         final String resName = String.format("org/apache/commons/codec/language/bm/%s.txt", lang);
  235.         final InputStream rulesIS = Languages.class.getClassLoader().getResourceAsStream(resName);

  236.         if (rulesIS == null) {
  237.             throw new IllegalArgumentException("Unable to load resource: " + resName);
  238.         }

  239.         return new Scanner(rulesIS, ResourceConstants.ENCODING);
  240.     }

  241.     private static boolean endsWith(final CharSequence input, final CharSequence suffix) {
  242.         if (suffix.length() > input.length()) {
  243.             return false;
  244.         }
  245.         for (int i = input.length() - 1, j = suffix.length() - 1; j >= 0; i--, j--) {
  246.             if (input.charAt(i) != suffix.charAt(j)) {
  247.                 return false;
  248.             }
  249.         }
  250.         return true;
  251.     }

  252.     /**
  253.      * Gets rules for a combination of name type, rule type and languages.
  254.      *
  255.      * @param nameType
  256.      *            the NameType to consider
  257.      * @param rt
  258.      *            the RuleType to consider
  259.      * @param langs
  260.      *            the set of languages to consider
  261.      * @return a list of Rules that apply
  262.      */
  263.     public static List<Rule> getInstance(final NameType nameType, final RuleType rt,
  264.                                          final Languages.LanguageSet langs) {
  265.         final Map<String, List<Rule>> ruleMap = getInstanceMap(nameType, rt, langs);
  266.         final List<Rule> allRules = new ArrayList<Rule>();
  267.         for (final List<Rule> rules : ruleMap.values()) {
  268.             allRules.addAll(rules);
  269.         }
  270.         return allRules;
  271.     }

  272.     /**
  273.      * Gets rules for a combination of name type, rule type and a single language.
  274.      *
  275.      * @param nameType
  276.      *            the NameType to consider
  277.      * @param rt
  278.      *            the RuleType to consider
  279.      * @param lang
  280.      *            the language to consider
  281.      * @return a list of Rules that apply
  282.      */
  283.     public static List<Rule> getInstance(final NameType nameType, final RuleType rt, final String lang) {
  284.         return getInstance(nameType, rt, LanguageSet.from(new HashSet<String>(Arrays.asList(lang))));
  285.     }

  286.     /**
  287.      * Gets rules for a combination of name type, rule type and languages.
  288.      *
  289.      * @param nameType
  290.      *            the NameType to consider
  291.      * @param rt
  292.      *            the RuleType to consider
  293.      * @param langs
  294.      *            the set of languages to consider
  295.      * @return a map containing all Rules that apply, grouped by the first character of the rule pattern
  296.      * @since 1.9
  297.      */
  298.     public static Map<String, List<Rule>> getInstanceMap(final NameType nameType, final RuleType rt,
  299.                                                          final Languages.LanguageSet langs) {
  300.         return langs.isSingleton() ? getInstanceMap(nameType, rt, langs.getAny()) :
  301.                                      getInstanceMap(nameType, rt, Languages.ANY);
  302.     }

  303.     /**
  304.      * Gets rules for a combination of name type, rule type and a single language.
  305.      *
  306.      * @param nameType
  307.      *            the NameType to consider
  308.      * @param rt
  309.      *            the RuleType to consider
  310.      * @param lang
  311.      *            the language to consider
  312.      * @return a map containing all Rules that apply, grouped by the first character of the rule pattern
  313.      * @since 1.9
  314.      */
  315.     public static Map<String, List<Rule>> getInstanceMap(final NameType nameType, final RuleType rt,
  316.                                                          final String lang) {
  317.         final Map<String, List<Rule>> rules = RULES.get(nameType).get(rt).get(lang);

  318.         if (rules == null) {
  319.             throw new IllegalArgumentException(String.format("No rules found for %s, %s, %s.",
  320.                                                nameType.getName(), rt.getName(), lang));
  321.         }

  322.         return rules;
  323.     }

  324.     private static Phoneme parsePhoneme(final String ph) {
  325.         final int open = ph.indexOf("[");
  326.         if (open >= 0) {
  327.             if (!ph.endsWith("]")) {
  328.                 throw new IllegalArgumentException("Phoneme expression contains a '[' but does not end in ']'");
  329.             }
  330.             final String before = ph.substring(0, open);
  331.             final String in = ph.substring(open + 1, ph.length() - 1);
  332.             final Set<String> langs = new HashSet<String>(Arrays.asList(in.split("[+]")));

  333.             return new Phoneme(before, Languages.LanguageSet.from(langs));
  334.         }
  335.         return new Phoneme(ph, Languages.ANY_LANGUAGE);
  336.     }

  337.     private static PhonemeExpr parsePhonemeExpr(final String ph) {
  338.         if (ph.startsWith("(")) { // we have a bracketed list of options
  339.             if (!ph.endsWith(")")) {
  340.                 throw new IllegalArgumentException("Phoneme starts with '(' so must end with ')'");
  341.             }

  342.             final List<Phoneme> phs = new ArrayList<Phoneme>();
  343.             final String body = ph.substring(1, ph.length() - 1);
  344.             for (final String part : body.split("[|]")) {
  345.                 phs.add(parsePhoneme(part));
  346.             }
  347.             if (body.startsWith("|") || body.endsWith("|")) {
  348.                 phs.add(new Phoneme("", Languages.ANY_LANGUAGE));
  349.             }

  350.             return new PhonemeList(phs);
  351.         }
  352.         return parsePhoneme(ph);
  353.     }

  354.     private static Map<String, List<Rule>> parseRules(final Scanner scanner, final String location) {
  355.         final Map<String, List<Rule>> lines = new HashMap<String, List<Rule>>();
  356.         int currentLine = 0;

  357.         boolean inMultilineComment = false;
  358.         while (scanner.hasNextLine()) {
  359.             currentLine++;
  360.             final String rawLine = scanner.nextLine();
  361.             String line = rawLine;

  362.             if (inMultilineComment) {
  363.                 if (line.endsWith(ResourceConstants.EXT_CMT_END)) {
  364.                     inMultilineComment = false;
  365.                 }
  366.             } else {
  367.                 if (line.startsWith(ResourceConstants.EXT_CMT_START)) {
  368.                     inMultilineComment = true;
  369.                 } else {
  370.                     // discard comments
  371.                     final int cmtI = line.indexOf(ResourceConstants.CMT);
  372.                     if (cmtI >= 0) {
  373.                         line = line.substring(0, cmtI);
  374.                     }

  375.                     // trim leading-trailing whitespace
  376.                     line = line.trim();

  377.                     if (line.length() == 0) {
  378.                         continue; // empty lines can be safely skipped
  379.                     }

  380.                     if (line.startsWith(HASH_INCLUDE)) {
  381.                         // include statement
  382.                         final String incl = line.substring(HASH_INCLUDE.length()).trim();
  383.                         if (incl.contains(" ")) {
  384.                             throw new IllegalArgumentException("Malformed import statement '" + rawLine + "' in " +
  385.                                                                location);
  386.                         }
  387.                         final Scanner hashIncludeScanner = createScanner(incl);
  388.                         try {
  389.                             lines.putAll(parseRules(hashIncludeScanner, location + "->" + incl));
  390.                         } finally {
  391.                             hashIncludeScanner.close();
  392.                         }
  393.                     } else {
  394.                         // rule
  395.                         final String[] parts = line.split("\\s+");
  396.                         if (parts.length != 4) {
  397.                             throw new IllegalArgumentException("Malformed rule statement split into " + parts.length +
  398.                                                                " parts: " + rawLine + " in " + location);
  399.                         }
  400.                         try {
  401.                             final String pat = stripQuotes(parts[0]);
  402.                             final String lCon = stripQuotes(parts[1]);
  403.                             final String rCon = stripQuotes(parts[2]);
  404.                             final PhonemeExpr ph = parsePhonemeExpr(stripQuotes(parts[3]));
  405.                             final int cLine = currentLine;
  406.                             final Rule r = new Rule(pat, lCon, rCon, ph) {
  407.                                 private final int myLine = cLine;
  408.                                 private final String loc = location;

  409.                                 @Override
  410.                                 public String toString() {
  411.                                     final StringBuilder sb = new StringBuilder();
  412.                                     sb.append("Rule");
  413.                                     sb.append("{line=").append(myLine);
  414.                                     sb.append(", loc='").append(loc).append('\'');
  415.                                     sb.append(", pat='").append(pat).append('\'');
  416.                                     sb.append(", lcon='").append(lCon).append('\'');
  417.                                     sb.append(", rcon='").append(rCon).append('\'');
  418.                                     sb.append('}');
  419.                                     return sb.toString();
  420.                                 }
  421.                             };
  422.                             final String patternKey = r.pattern.substring(0,1);
  423.                             List<Rule> rules = lines.get(patternKey);
  424.                             if (rules == null) {
  425.                                 rules = new ArrayList<Rule>();
  426.                                 lines.put(patternKey, rules);
  427.                             }
  428.                             rules.add(r);
  429.                         } catch (final IllegalArgumentException e) {
  430.                             throw new IllegalStateException("Problem parsing line '" + currentLine + "' in " +
  431.                                                             location, e);
  432.                         }
  433.                     }
  434.                 }
  435.             }
  436.         }

  437.         return lines;
  438.     }

  439.     /**
  440.      * Attempts to compile the regex into direct string ops, falling back to Pattern and Matcher in the worst case.
  441.      *
  442.      * @param regex
  443.      *            the regular expression to compile
  444.      * @return an RPattern that will match this regex
  445.      */
  446.     private static RPattern pattern(final String regex) {
  447.         final boolean startsWith = regex.startsWith("^");
  448.         final boolean endsWith = regex.endsWith("$");
  449.         final String content = regex.substring(startsWith ? 1 : 0, endsWith ? regex.length() - 1 : regex.length());
  450.         final boolean boxes = content.contains("[");

  451.         if (!boxes) {
  452.             if (startsWith && endsWith) {
  453.                 // exact match
  454.                 if (content.length() == 0) {
  455.                     // empty
  456.                     return new RPattern() {
  457.                         @Override
  458.                         public boolean isMatch(final CharSequence input) {
  459.                             return input.length() == 0;
  460.                         }
  461.                     };
  462.                 }
  463.                 return new RPattern() {
  464.                     @Override
  465.                     public boolean isMatch(final CharSequence input) {
  466.                         return input.equals(content);
  467.                     }
  468.                 };
  469.             } else if ((startsWith || endsWith) && content.length() == 0) {
  470.                 // matches every string
  471.                 return ALL_STRINGS_RMATCHER;
  472.             } else if (startsWith) {
  473.                 // matches from start
  474.                 return new RPattern() {
  475.                     @Override
  476.                     public boolean isMatch(final CharSequence input) {
  477.                         return startsWith(input, content);
  478.                     }
  479.                 };
  480.             } else if (endsWith) {
  481.                 // matches from start
  482.                 return new RPattern() {
  483.                     @Override
  484.                     public boolean isMatch(final CharSequence input) {
  485.                         return endsWith(input, content);
  486.                     }
  487.                 };
  488.             }
  489.         } else {
  490.             final boolean startsWithBox = content.startsWith("[");
  491.             final boolean endsWithBox = content.endsWith("]");

  492.             if (startsWithBox && endsWithBox) {
  493.                 String boxContent = content.substring(1, content.length() - 1);
  494.                 if (!boxContent.contains("[")) {
  495.                     // box containing alternatives
  496.                     final boolean negate = boxContent.startsWith("^");
  497.                     if (negate) {
  498.                         boxContent = boxContent.substring(1);
  499.                     }
  500.                     final String bContent = boxContent;
  501.                     final boolean shouldMatch = !negate;

  502.                     if (startsWith && endsWith) {
  503.                         // exact match
  504.                         return new RPattern() {
  505.                             @Override
  506.                             public boolean isMatch(final CharSequence input) {
  507.                                 return input.length() == 1 && contains(bContent, input.charAt(0)) == shouldMatch;
  508.                             }
  509.                         };
  510.                     } else if (startsWith) {
  511.                         // first char
  512.                         return new RPattern() {
  513.                             @Override
  514.                             public boolean isMatch(final CharSequence input) {
  515.                                 return input.length() > 0 && contains(bContent, input.charAt(0)) == shouldMatch;
  516.                             }
  517.                         };
  518.                     } else if (endsWith) {
  519.                         // last char
  520.                         return new RPattern() {
  521.                             @Override
  522.                             public boolean isMatch(final CharSequence input) {
  523.                                 return input.length() > 0 &&
  524.                                        contains(bContent, input.charAt(input.length() - 1)) == shouldMatch;
  525.                             }
  526.                         };
  527.                     }
  528.                 }
  529.             }
  530.         }

  531.         return new RPattern() {
  532.             Pattern pattern = Pattern.compile(regex);

  533.             @Override
  534.             public boolean isMatch(final CharSequence input) {
  535.                 final Matcher matcher = pattern.matcher(input);
  536.                 return matcher.find();
  537.             }
  538.         };
  539.     }

  540.     private static boolean startsWith(final CharSequence input, final CharSequence prefix) {
  541.         if (prefix.length() > input.length()) {
  542.             return false;
  543.         }
  544.         for (int i = 0; i < prefix.length(); i++) {
  545.             if (input.charAt(i) != prefix.charAt(i)) {
  546.                 return false;
  547.             }
  548.         }
  549.         return true;
  550.     }

  551.     private static String stripQuotes(String str) {
  552.         if (str.startsWith(DOUBLE_QUOTE)) {
  553.             str = str.substring(1);
  554.         }

  555.         if (str.endsWith(DOUBLE_QUOTE)) {
  556.             str = str.substring(0, str.length() - 1);
  557.         }

  558.         return str;
  559.     }

  560.     private final RPattern lContext;

  561.     private final String pattern;

  562.     private final PhonemeExpr phoneme;

  563.     private final RPattern rContext;

  564.     /**
  565.      * Creates a new rule.
  566.      *
  567.      * @param pattern
  568.      *            the pattern
  569.      * @param lContext
  570.      *            the left context
  571.      * @param rContext
  572.      *            the right context
  573.      * @param phoneme
  574.      *            the resulting phoneme
  575.      */
  576.     public Rule(final String pattern, final String lContext, final String rContext, final PhonemeExpr phoneme) {
  577.         this.pattern = pattern;
  578.         this.lContext = pattern(lContext + "$");
  579.         this.rContext = pattern("^" + rContext);
  580.         this.phoneme = phoneme;
  581.     }

  582.     /**
  583.      * Gets the left context. This is a regular expression that must match to the left of the pattern.
  584.      *
  585.      * @return the left context Pattern
  586.      */
  587.     public RPattern getLContext() {
  588.         return this.lContext;
  589.     }

  590.     /**
  591.      * Gets the pattern. This is a string-literal that must exactly match.
  592.      *
  593.      * @return the pattern
  594.      */
  595.     public String getPattern() {
  596.         return this.pattern;
  597.     }

  598.     /**
  599.      * Gets the phoneme. If the rule matches, this is the phoneme associated with the pattern match.
  600.      *
  601.      * @return the phoneme
  602.      */
  603.     public PhonemeExpr getPhoneme() {
  604.         return this.phoneme;
  605.     }

  606.     /**
  607.      * Gets the right context. This is a regular expression that must match to the right of the pattern.
  608.      *
  609.      * @return the right context Pattern
  610.      */
  611.     public RPattern getRContext() {
  612.         return this.rContext;
  613.     }

  614.     /**
  615.      * Decides if the pattern and context match the input starting at a position. It is a match if the
  616.      * <code>lContext</code> matches <code>input</code> up to <code>i</code>, <code>pattern</code> matches at i and
  617.      * <code>rContext</code> matches from the end of the match of <code>pattern</code> to the end of <code>input</code>.
  618.      *
  619.      * @param input
  620.      *            the input String
  621.      * @param i
  622.      *            the int position within the input
  623.      * @return true if the pattern and left/right context match, false otherwise
  624.      */
  625.     public boolean patternAndContextMatches(final CharSequence input, final int i) {
  626.         if (i < 0) {
  627.             throw new IndexOutOfBoundsException("Can not match pattern at negative indexes");
  628.         }

  629.         final int patternLength = this.pattern.length();
  630.         final int ipl = i + patternLength;

  631.         if (ipl > input.length()) {
  632.             // not enough room for the pattern to match
  633.             return false;
  634.         }

  635.         // evaluate the pattern, left context and right context
  636.         // fail early if any of the evaluations is not successful
  637.         if (!input.subSequence(i, ipl).equals(this.pattern)) {
  638.             return false;
  639.         } else if (!this.rContext.isMatch(input.subSequence(ipl, input.length()))) {
  640.             return false;
  641.         }
  642.         return this.lContext.isMatch(input.subSequence(0, i));
  643.     }
  644. }