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