001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.search;
003    
004    import static org.openstreetmap.josm.tools.I18n.marktr;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    import static org.openstreetmap.josm.tools.Utils.equal;
007    
008    import java.io.IOException;
009    import java.io.Reader;
010    import java.util.Arrays;
011    import java.util.List;
012    
013    import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
014    
015    public class PushbackTokenizer {
016    
017        public static class Range {
018            private final long start;
019            private final long end;
020    
021            public Range(long start, long end) {
022                this.start = start;
023                this.end = end;
024            }
025    
026            public long getStart() {
027                return start;
028            }
029    
030            public long getEnd() {
031                return end;
032            }
033        }
034    
035        private final Reader search;
036    
037        private Token currentToken;
038        private String currentText;
039        private Long currentNumber;
040        private Long currentRange;
041        private int c;
042        private boolean isRange;
043    
044        public PushbackTokenizer(Reader search) {
045            this.search = search;
046            getChar();
047        }
048    
049        public enum Token {
050            NOT(marktr("<not>")), OR(marktr("<or>")), XOR(marktr("<xor>")), LEFT_PARENT(marktr("<left parent>")),
051            RIGHT_PARENT(marktr("<right parent>")), COLON(marktr("<colon>")), EQUALS(marktr("<equals>")),
052            KEY(marktr("<key>")), QUESTION_MARK(marktr("<question mark>")),
053            EOF(marktr("<end-of-file>"));
054    
055            private Token(String name) {
056                this.name = name;
057            }
058    
059            private final String name;
060    
061            @Override
062            public String toString() {
063                return tr(name);
064            }
065        }
066    
067    
068        private void getChar() {
069            try {
070                c = search.read();
071            } catch (IOException e) {
072                throw new RuntimeException(e.getMessage(), e);
073            }
074        }
075    
076        private static final List<Character> specialChars = Arrays.asList(new Character[] {'"', ':', '(', ')', '|', '^', '=', '?'});
077        private static final List<Character> specialCharsQuoted = Arrays.asList(new Character[] {'"'});
078    
079        private String getString(boolean quoted) {
080            List<Character> sChars = quoted ? specialCharsQuoted : specialChars;
081            StringBuilder s = new StringBuilder();
082            boolean escape = false;
083            while (c != -1 && (escape || (!sChars.contains((char)c) && (quoted || !Character.isWhitespace(c))))) {
084                if (c == '\\' && !escape) {
085                    escape = true;
086                } else {
087                    s.append((char)c);
088                    escape = false;
089                }
090                getChar();
091            }
092            return s.toString();
093        }
094    
095        private String getString() {
096            return getString(false);
097        }
098    
099        /**
100         * The token returned is <code>null</code> or starts with an identifier character:
101         * - for an '-'. This will be the only character
102         * : for an key. The value is the next token
103         * | for "OR"
104         * ^ for "XOR"
105         * ' ' for anything else.
106         * @return The next token in the stream.
107         */
108        public Token nextToken() {
109            if (currentToken != null) {
110                Token result = currentToken;
111                currentToken = null;
112                return result;
113            }
114    
115            while (Character.isWhitespace(c)) {
116                getChar();
117            }
118            switch (c) {
119            case -1:
120                getChar();
121                return Token.EOF;
122            case ':':
123                getChar();
124                return Token.COLON;
125            case '=':
126                getChar();
127                return Token.EQUALS;
128            case '(':
129                getChar();
130                return Token.LEFT_PARENT;
131            case ')':
132                getChar();
133                return Token.RIGHT_PARENT;
134            case '|':
135                getChar();
136                return Token.OR;
137            case '^':
138                getChar();
139                return Token.XOR;
140            case '&':
141                getChar();
142                return nextToken();
143            case '?':
144                getChar();
145                return Token.QUESTION_MARK;
146            case '"':
147                getChar();
148                currentText = getString(true);
149                getChar();
150                return Token.KEY;
151            default:
152                String prefix = "";
153                if (c == '-') {
154                    getChar();
155                    if (!Character.isDigit(c))
156                        return Token.NOT;
157                    prefix = "-";
158                }
159                currentText = prefix + getString();
160                if ("or".equalsIgnoreCase(currentText))
161                    return Token.OR;
162                else if ("xor".equalsIgnoreCase(currentText))
163                    return Token.XOR;
164                else if ("and".equalsIgnoreCase(currentText))
165                    return nextToken();
166                // try parsing number
167                try {
168                    currentNumber = Long.parseLong(currentText);
169                } catch (NumberFormatException e) {
170                    currentNumber = null;
171                }
172                // if text contains "-", try parsing a range
173                int pos = currentText.indexOf('-', 1);
174                isRange = pos > 0;
175                if (isRange) {
176                    try {
177                        currentNumber = Long.parseLong(currentText.substring(0, pos));
178                    } catch (NumberFormatException e) {
179                        currentNumber = null;
180                    }
181                    try {
182                        currentRange = Long.parseLong(currentText.substring(pos + 1));
183                    } catch (NumberFormatException e) {
184                        currentRange = null;
185                        }
186                    } else {
187                        currentRange = null;
188                    }
189                return Token.KEY;
190            }
191        }
192    
193        public boolean readIfEqual(Token token) {
194            Token nextTok = nextToken();
195            if (equal(nextTok, token))
196                return true;
197            currentToken = nextTok;
198            return false;
199        }
200    
201        public String readTextOrNumber() {
202            Token nextTok = nextToken();
203            if (nextTok == Token.KEY)
204                return currentText;
205            currentToken = nextTok;
206            return null;
207        }
208    
209        public long readNumber(String errorMessage) throws ParseError {
210            if ((nextToken() == Token.KEY) && (currentNumber != null))
211                return currentNumber;
212            else
213                throw new ParseError(errorMessage);
214        }
215    
216        public long getReadNumber() {
217            return (currentNumber != null) ? currentNumber : 0;
218        }
219    
220        public Range readRange(String errorMessage) throws ParseError {
221            if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
222                throw new ParseError(errorMessage);
223            } else if (!isRange && currentNumber != null) {
224                if (currentNumber >= 0) {
225                    return new Range(currentNumber, currentNumber);
226                } else {
227                    return new Range(0, Math.abs(currentNumber));
228                }
229            } else if (isRange && currentRange == null) {
230                return new Range(currentNumber, Integer.MAX_VALUE);
231            } else if (currentNumber != null && currentRange != null) {
232                return new Range(currentNumber, currentRange);
233            } else {
234                throw new ParseError(errorMessage);
235            }
236        }
237    
238        public String getText() {
239            return currentText;
240        }
241    }