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}