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