001package org.hl7.fhir.utilities.json.parser; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.List; 006import java.util.Stack; 007 008import org.hl7.fhir.utilities.Utilities; 009import org.hl7.fhir.utilities.json.model.JsonComment; 010import org.hl7.fhir.utilities.json.model.JsonElement; 011import org.hl7.fhir.utilities.json.model.JsonLocationData; 012 013public class JsonLexer { 014 public static class State { 015 private String name; 016 private boolean isProp; 017 public State(String name, boolean isProp) { 018 super(); 019 this.name = name; 020 this.isProp = isProp; 021 } 022 public String getName() { 023 return name; 024 } 025 public boolean isProp() { 026 return isProp; 027 } 028 } 029 030 public enum TokenType { 031 Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; 032 033 boolean isValueType() { 034 return this == Open || this == String || this == Number || this == OpenArray || this == Boolean || this == Null; 035 } 036 } 037 038 private String source; 039 private int cursor; 040 private String peek; 041 private String value; 042 private TokenType type; 043 private Stack<State> states = new Stack<State>(); 044 private JsonLocationData lastLocationBWS; 045 private JsonLocationData lastLocationAWS; 046 private JsonLocationData location; 047 private StringBuilder b = new StringBuilder(); 048 private boolean allowComments; 049 private boolean allowUnquotedStrings; 050 private List<JsonComment> comments = new ArrayList<>(); 051 private boolean isUnquoted; 052 053 public JsonLexer(String source, boolean allowComments, boolean allowUnquotedStrings) throws IOException { 054 this.source = source; 055 this.allowComments = allowComments; 056 this.allowUnquotedStrings = allowUnquotedStrings; 057 cursor = -1; 058 location = new JsonLocationData(1, 1); 059 start(); 060 } 061 062 private boolean more() { 063 return peek != null || cursor < source.length(); 064 } 065 066 private String getNext(int length) throws IOException { 067 String result = ""; 068 if (peek != null) { 069 if (peek.length() > length) { 070 result = peek.substring(0, length); 071 peek = peek.substring(length); 072 } else { 073 result = peek; 074 peek = null; 075 } 076 } 077 if (result.length() < length) { 078 int len = length - result.length(); 079 if (cursor > source.length() - len) 080 throw error("Attempt to read past end of source"); 081 result = result + source.substring(cursor+1, cursor+len+1); 082 cursor = cursor + len; 083 } 084 for (char ch : result.toCharArray()) 085 if (ch == '\n') 086 location.newLine(); 087 else 088 location.incCol(); 089 return result; 090 } 091 092 private char getNextChar() throws IOException { 093 char ch; 094 if (peek != null) { 095 ch = peek.charAt(0); 096 peek = peek.length() == 1 ? null : peek.substring(1); 097 } else { 098 cursor++; 099 if (cursor >= source.length()) { 100 ch = 0; 101 } else { 102 ch = source.charAt(cursor); 103 } 104 } 105 if (ch == '\n') { 106 location.newLine(); 107 } else { 108 location.incCol(); 109 } 110 return ch; 111 } 112 113 private void push(char ch){ 114 peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; 115 location.back(); 116 } 117 118 public IOException error(String msg) { 119 return new IOException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.getLine())+" (path=["+path()+"])"); 120 } 121 122 private String path() { 123 if (states.empty()) 124 return value; 125 else { 126 String result = ""; 127 for (State s : states) 128 result = result + '/'+ s.getName(); 129 result = result + value; 130 return result; 131 } 132 } 133 134 public void start() throws IOException { 135 // char ch = getNextChar(); 136 // if (ch = '\.uEF') 137 // begin 138 // // skip BOM 139 // getNextChar(); 140 // getNextChar(); 141 // end 142 // else 143 // push(ch); 144 next(); 145 } 146 147 public TokenType getType() { 148 return type; 149 } 150 151 public String getValue() { 152 return value; 153 } 154 155 156 public JsonLocationData getLastLocationBWS() { 157 return lastLocationBWS; 158 } 159 160 public JsonLocationData getLastLocationAWS() { 161 return lastLocationAWS; 162 } 163 164 public void next() throws IOException { 165 lastLocationBWS = location.copy(); 166 char ch; 167 do { 168 ch = getNextChar(); 169 if (allowComments && ch == '/') { 170 JsonLocationData start = location.prev(); 171 char ch1 = getNextChar(); 172 if (ch1 == '/') { 173 StringBuilder b = new StringBuilder(); 174 boolean first = true; 175 while (more() && !Utilities.charInSet(ch, '\r', '\n')) { 176 if (first) first = false; else b.append(ch); 177 ch = getNextChar(); 178 } 179 comments.add(new JsonComment(b.toString().trim(), start, location.prev())); 180 } else { 181 push(ch1); 182 } 183 } 184 } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); 185 lastLocationAWS = location.copy().prev(); 186 isUnquoted = false; 187 188 if (!more()) { 189 type = TokenType.Eof; 190 } else { 191 switch (ch) { 192 case '{' : 193 type = TokenType.Open; 194 break; 195 case '}' : 196 type = TokenType.Close; 197 break; 198 case '"' : 199 type = TokenType.String; 200 b.setLength(0); 201 do { 202 ch = getNextChar(); 203 if (ch == '\\') { 204 ch = getNextChar(); 205 switch (ch) { 206 case '"': b.append('"'); break; 207 case '\'': b.append('\''); break; 208 case '\\': b.append('\\'); break; 209 case '/': b.append('/'); break; 210 case 'n': b.append('\n'); break; 211 case 'r': b.append('\r'); break; 212 case 't': b.append('\t'); break; 213 case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; 214 default : 215 throw error("unknown escape sequence: \\"+ch); 216 } 217 ch = ' '; 218 } else if (ch != '"') 219 b.append(ch); 220 } while (more() && (ch != '"')); 221 if (!more()) 222 throw error("premature termination of json stream during a string"); 223 value = b.toString(); 224 break; 225 case ':' : 226 type = TokenType.Colon; 227 break; 228 case ',' : 229 type = TokenType.Comma; 230 break; 231 case '[' : 232 type = TokenType.OpenArray; 233 break; 234 case ']' : 235 type = TokenType.CloseArray; 236 break; 237 default: 238 if ((ch >= '0' && ch <= '9') || ch == '-') { 239 type = TokenType.Number; 240 b.setLength(0); 241 while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') || ch == '+' || ch == 'e' || ch == 'E') { 242 b.append(ch); 243 ch = getNextChar(); 244 } 245 value = b.toString(); 246 push(ch); 247 } else if (Utilities.isAlphabetic(ch) || (ch == '_')) { 248 type = TokenType.String; 249 isUnquoted = true; 250 b.setLength(0); 251 while (more() && (Utilities.isAlphabetic(ch) || Utilities.isDigit(ch) || Utilities.existsInList(ch, '_', '.', '-'))) { 252 b.append(ch); 253 ch = getNextChar(); 254 } 255 value = b.toString(); 256 push(ch); 257 if ("true".equals(value) || "false".equals(value)) { 258 this.type = TokenType.Boolean; 259 isUnquoted = false; 260 } else if ("null".equals(value)) { 261 this.type = TokenType.Null; 262 isUnquoted = false; 263 } else if (!allowUnquotedStrings) { 264 throw error("Unexpected token '"+value+"' in json stream"); 265 } 266 } 267 } 268 } 269 } 270 271 public String consume(TokenType type) throws IOException { 272 if (this.type != type) 273 throw error("JSON syntax error - found "+this.type.toString()+" expecting "+type.toString()); 274 String result = value; 275 next(); 276 return result; 277 } 278 279 public JsonLocationData getLocation() { 280 return location; 281 } 282 283 public Stack<State> getStates() { 284 return states; 285 } 286 287 public void takeComments(JsonElement child) { 288 if (!comments.isEmpty()) { 289 child.getComments().addAll(comments); 290 comments.clear(); 291 } 292 } 293 294 public boolean isUnquoted() { 295 return isUnquoted; 296 } 297 298 @Override 299 public String toString() { 300 return "JsonLexer [cursor=" + cursor + ", peek=" + peek + ", type=" + type + ", location=" + location.toString() + "]"; 301 } 302 303 304}