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}