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