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 }