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