001/* 002 * Copyright (C) 2009-2011 Mathias Doenitz 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.parboiled.matchers; 018 019import org.parboiled.MatcherContext; 020import org.parboiled.Rule; 021import org.parboiled.buffers.InputBuffer; 022import org.parboiled.errors.GrammarException; 023 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027import java.util.TreeMap; 028 029import static org.parboiled.common.Preconditions.checkArgNotNull; 030 031/** 032 * A specialized FirstOfMatcher that handles FirstOf(string, string, ...) rules much faster that the regular 033 * FirstOfMatcher. If fast string matching is enabled this matcher uses a prebuilt character tree to efficiently 034 * determine whether the next input characters match the rule expression. 035 */ 036public class FirstOfStringsMatcher extends FirstOfMatcher { 037 038 // a node in the character tree 039 static class Record { 040 final char[] chars; // the sub characters of this node 041 final Record[] subs; // the sub records corresponding to the respective character 042 final boolean complete; // flag indicating that the path up to this record also constitutes a valid match 043 044 private Record(char[] chars, Record[] subs, boolean complete) { 045 this.chars = chars; 046 this.subs = subs; 047 this.complete = complete; 048 } 049 } 050 051 private final Record root; // the root of the character tree 052 public final char[][] strings; 053 054 public FirstOfStringsMatcher(Rule[] subRules, char[][] strings) { 055 super(checkArgNotNull(subRules, "subRules")); 056 verify(strings); 057 this.strings = strings; 058 root = createRecord(0, strings); 059 } 060 061 @Override 062 public boolean match(MatcherContext context) { 063 if (!context.fastStringMatching()) { 064 return super.match(context); 065 } 066 067 Record rec = root; 068 int ix = context.getCurrentIndex(); 069 InputBuffer buffer = context.getInputBuffer(); 070 char c = context.getCurrentChar(); 071 int endIx = -1; 072 073 loop: 074 while (true) { 075 char[] chars = rec.chars; 076 for (int i = 0; i < chars.length; i++) { 077 if (c == chars[i]) { 078 ix++; 079 rec = rec.subs[i]; 080 if (rec == null) { // success, we complected a tree path to a leave 081 endIx = ix; 082 break loop; 083 } 084 if (rec.complete) { // we completed a valid match path, but continue looking for a longer match 085 endIx = ix; 086 } 087 c = buffer.charAt(ix); 088 continue loop; 089 } 090 } 091 // we checked all sub branches of the current node, none matched, so we are done 092 break; 093 } 094 095 if (endIx == -1) return false; // we matched no complete path, so fail 096 097 context.advanceIndex(endIx - context.getCurrentIndex()); 098 context.createNode(); 099 return true; 100 } 101 102 static Record createRecord(int pos, char[][] strings) { 103 Map<Character, Set<char[]>> map = new TreeMap<Character, Set<char[]>>(); 104 boolean complete = false; 105 for (char[] s : strings) { 106 if (s.length == pos) complete = true; 107 if (s.length <= pos) continue; 108 char c = s[pos]; 109 Set<char[]> charStrings = map.get(c); 110 if (charStrings == null) { 111 charStrings = new HashSet<char[]>(); 112 map.put(c, charStrings); 113 } 114 charStrings.add(s); 115 } 116 117 if (map.isEmpty()) return null; 118 119 char[] chars = new char[map.size()]; 120 Record[] subs = new Record[map.size()]; 121 int i = 0; 122 for (Map.Entry<Character, Set<char[]>> entry : map.entrySet()) { 123 chars[i] = entry.getKey(); 124 subs[i++] = createRecord(pos + 1, entry.getValue().toArray(new char[entry.getValue().size()][])); 125 } 126 return new Record(chars, subs, complete); 127 } 128 129 // make sure that a string is no prefix of another string later in the array 130 // this would cause the second string to never match without fast-string-matching, 131 // but match in the fast implementation 132 133 private static void verify(char[][] strings) { 134 int length = strings.length; 135 for (int i = 0; i < length; i++) { 136 char[] a = strings[i]; 137 inner: 138 for (int j = i + 1; j < length; j++) { 139 char[] b = strings[j]; 140 if (b.length < a.length) continue; 141 for (int k = 0; k < a.length; k++) { 142 if (a[k] != b[k]) continue inner; 143 } 144 String sa = '"' + String.valueOf(a) + '"'; 145 String sb = '"' + String.valueOf(b) + '"'; 146 String msg = a.length == b.length ? sa + " is specified twice in a FirstOf(String...)" : sa + 147 " is a prefix of " + sb + " in a FirstOf(String...) and comes before " + 148 sb + ", which prevents " + sb + 149 " from ever matching! You should reverse the order of the two alternatives."; 150 throw new GrammarException(msg); 151 } 152 } 153 } 154}