001package org.hl7.fhir.dstu3.utils;
002
003import java.math.BigDecimal;
004import java.util.Map;
005import java.util.Stack;
006
007import org.hl7.fhir.exceptions.FHIRException;
008import org.hl7.fhir.utilities.Utilities;
009
010import com.google.gson.*;
011
012
013/**
014 * This is created to get a json parser that can track line numbers... grr...
015 * 
016 * @author Grahame Grieve
017 *
018 */
019public class JsonTrackingParser {
020
021        public enum TokenType {
022                Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean;
023        }
024        
025        public class LocationData {
026                private int line;
027                private int col;
028                
029                protected LocationData(int line, int col) {
030                        super();
031                        this.line = line;
032                        this.col = col;
033                }
034                
035                public int getLine() {
036                        return line;
037                }
038                
039                public int getCol() {
040                        return col;
041                }
042                
043                public void newLine() {
044                        line++;
045                        col = 1;                
046                }
047
048                public LocationData copy() {
049                        return new LocationData(line, col);
050                }
051        }
052        
053        private class State {
054                private String name;
055                private boolean isProp;
056                protected State(String name, boolean isProp) {
057                        super();
058                        this.name = name;
059                        this.isProp = isProp;
060                }
061                public String getName() {
062                        return name;
063                }
064                public boolean isProp() {
065                        return isProp;
066                }
067        }
068        
069        private class Lexer {
070                private String source;
071                private int cursor;
072                private String peek;
073                private String value;
074                private TokenType type;
075                private Stack<State> states = new Stack<State>();
076                private LocationData lastLocationBWS;
077                private LocationData lastLocationAWS;
078                private LocationData location;
079                private StringBuilder b = new StringBuilder();
080                
081    public Lexer(String source) throws FHIRException {
082        this.source = source;
083        cursor = -1;
084        location = new LocationData(1, 1);  
085        start();
086    }
087    
088    private boolean more() {
089        return peek != null || cursor < source.length(); 
090    }
091    
092    private String getNext(int length) throws FHIRException {
093        String result = "";
094      if (peek != null) {
095        if (peek.length() > length) {
096                result = peek.substring(0, length);
097                peek = peek.substring(length);
098        } else {
099                result = peek;
100                peek = null;
101        }
102      }
103      if (result.length() < length) {
104        int len = length - result.length(); 
105        if (cursor > source.length() - len) 
106                throw error("Attempt to read past end of source");
107        result = result + source.substring(cursor+1, cursor+len+1);
108        cursor = cursor + len;
109      }
110       for (char ch : result.toCharArray())
111        if (ch == '\n')
112          location.newLine();
113        else
114          location.col++;
115      return result;
116    }
117    
118    private char getNextChar() throws FHIRException {
119      if (peek != null) {
120        char ch = peek.charAt(0);
121        peek = peek.length() == 1 ? null : peek.substring(1);
122        return ch;
123      } else {
124        cursor++;
125        if (cursor >= source.length())
126          return (char) 0;
127        char ch = source.charAt(cursor);
128        if (ch == '\n') {
129          location.newLine();
130        } else {
131          location.col++;
132        }
133        return ch;
134      }
135    }
136    
137    private void push(char ch){
138        peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek;
139    }
140    
141    private void parseWord(String word, char ch, TokenType type) throws FHIRException {
142      this.type = type;
143      value = ""+ch+getNext(word.length()-1);
144      if (!value.equals(word))
145        throw error("Syntax error in json reading special word "+word);
146    }
147    
148    private FHIRException error(String msg) {
149      return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])");
150    }
151    
152    private String path() {
153      if (states.empty())
154        return value;
155      else {
156        String result = "";
157        for (State s : states) 
158          result = result + '/'+ s.getName();
159        result = result + value;
160        return result;
161      }
162    }
163
164    public void start() throws FHIRException {
165//      char ch = getNextChar();
166//      if (ch = '\.uEF')
167//      begin
168//        // skip BOM
169//        getNextChar();
170//        getNextChar();
171//      end
172//      else
173//        push(ch);
174      next();
175    }
176    
177    public TokenType getType() {
178        return type;
179    }
180    
181    public String getValue() {
182        return value;
183    }
184
185
186    public LocationData getLastLocationBWS() {
187        return lastLocationBWS;
188    }
189
190    public LocationData getLastLocationAWS() {
191        return lastLocationAWS;
192    }
193
194    public void next() throws FHIRException {
195        lastLocationBWS = location.copy();
196        char ch;
197        do {
198                ch = getNextChar();
199        } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t'));
200        lastLocationAWS = location.copy();
201
202        if (!more()) {
203                type = TokenType.Eof;
204        } else {
205                switch (ch) {
206                case '{' : 
207                        type = TokenType.Open;
208                        break;
209                case '}' : 
210                        type = TokenType.Close;
211                        break;
212                case '"' :
213                        type = TokenType.String;
214                        b.setLength(0);
215                        do {
216                                ch = getNextChar();
217                                if (ch == '\\') {
218                                        ch = getNextChar();
219                                        switch (ch) {
220                                        case '"': b.append('"'); break;
221                                        case '\\': b.append('\\'); break;
222                                        case '/': b.append('/'); break;
223                                        case 'n': b.append('\n'); break;
224                                        case 'r': b.append('\r'); break;
225                                        case 't': b.append('\t'); break;
226                                        case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break;
227                                        default :
228                                                throw error("unknown escape sequence: \\"+ch);
229                                        }
230                                        ch = ' ';
231                                } else if (ch != '"')
232                                        b.append(ch);
233                        } while (more() && (ch != '"'));
234                        if (!more())
235                                throw error("premature termination of json stream during a string");
236                        value = b.toString();
237                        break;
238                case ':' : 
239                        type = TokenType.Colon;
240                        break;
241                case ',' : 
242                        type = TokenType.Comma;
243                        break;
244                case '[' : 
245                        type = TokenType.OpenArray;
246                        break;
247                case ']' : 
248                        type = TokenType.CloseArray;
249                        break;
250                case 't' : 
251                        parseWord("true", ch, TokenType.Boolean);
252                        break;
253                case 'f' : 
254                        parseWord("false", ch, TokenType.Boolean);
255                        break;
256                case 'n' : 
257                        parseWord("null", ch, TokenType.Null);
258                        break;
259                default:
260                        if ((ch >= '0' && ch <= '9') || ch == '-') {
261                                type = TokenType.Number;
262                                b.setLength(0);
263                                while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) {
264                                        b.append(ch);
265                                        ch = getNextChar();
266                                }
267                                value = b.toString();
268                                push(ch);
269                        } else
270                                throw error("Unexpected char '"+ch+"' in json stream");
271                }
272        }
273    }
274
275    public String consume(TokenType type) throws FHIRException {
276      if (this.type != type)
277        throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString());
278      String result = value;
279      next();
280      return result;
281    }
282
283        }
284
285        enum ItemType {
286          Object, String, Number, Boolean, Array, End, Eof, Null;
287        }
288        private Map<JsonElement, LocationData> map;
289  private Lexer lexer;
290  private ItemType itemType = ItemType.Object;
291  private String itemName;
292  private String itemValue;
293
294        public static JsonObject parse(String source, Map<JsonElement, LocationData> map) throws FHIRException {
295                JsonTrackingParser self = new JsonTrackingParser();
296                self.map = map;
297    return self.parse(source);
298        }
299
300        private JsonObject parse(String source) throws FHIRException {
301                lexer = new Lexer(source);
302                JsonObject result = new JsonObject();
303                LocationData loc = lexer.location.copy();
304    if (lexer.getType() == TokenType.Open) {
305      lexer.next();
306      lexer.states.push(new State("", false));
307    } 
308    else
309      throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString());
310
311    parseProperty();
312    readObject(result, true);
313                map.put(result, loc);
314    return result;
315        }
316
317        private void readObject(JsonObject obj, boolean root) throws FHIRException {
318                map.put(obj, lexer.location.copy());
319
320                while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) {
321                        if (obj.has(itemName))
322                                throw lexer.error("Duplicated property name: "+itemName);
323
324                        switch (itemType) {
325                        case Object:
326                                JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName);
327                                LocationData loc = lexer.location.copy();
328                                obj.add(itemName, child);
329                                next();
330                                readObject(child, false);
331                                map.put(obj, loc);
332                                break;
333                        case Boolean :
334                                JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue));
335                                obj.add(itemName, v);
336                                map.put(v, lexer.location.copy());
337                                break;
338                        case String:
339                                v = new JsonPrimitive(itemValue);
340                                obj.add(itemName, v);
341                                map.put(v, lexer.location.copy());
342                                break;
343                        case Number:
344                                v = new JsonPrimitive(new BigDecimal(itemValue));
345                                obj.add(itemName, v);
346                                map.put(v, lexer.location.copy());
347                                break;
348                        case Null:
349                                JsonNull n = new JsonNull();
350                                obj.add(itemName, n);
351                                map.put(n, lexer.location.copy());
352                                break;
353                        case Array:
354                                JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName);
355                                loc = lexer.location.copy();
356                                obj.add(itemName, arr);
357                                next();
358                                readArray(arr, false);
359                                map.put(arr, loc);
360                                break;
361                        case Eof : 
362                                throw lexer.error("Unexpected End of File");
363                        case End:
364                           // TODO: anything?\
365            break;
366                        }
367                        next();
368                }
369        }
370
371        private void readArray(JsonArray arr, boolean root) throws FHIRException {
372          while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) {
373            switch (itemType) {
374            case Object:
375                JsonObject obj  = new JsonObject(); // (arr.path+'['+inttostr(i)+']');
376                                LocationData loc = lexer.location.copy();
377                arr.add(obj);
378              next();
379              readObject(obj, false);
380                                map.put(obj, loc);
381              break;
382            case String:
383                JsonPrimitive v = new JsonPrimitive(itemValue);
384                                arr.add(v);
385                                map.put(v, lexer.location.copy());
386                                break;
387            case Number:
388                v = new JsonPrimitive(new BigDecimal(itemValue));
389                                arr.add(v);
390                                map.put(v, lexer.location.copy());
391                                break;
392            case Null :
393                JsonNull n = new JsonNull();
394                                arr.add(n);
395                                map.put(n, lexer.location.copy());
396                                break;
397            case Array:
398        JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']');
399                                loc = lexer.location.copy();
400                                arr.add(child);
401        next();
402              readArray(child, false);
403                                map.put(arr, loc);
404        break;
405            case Eof : 
406                throw lexer.error("Unexpected End of File");
407       case End:
408       case Boolean:
409         // TODO: anything?
410         break;
411            }
412            next();
413          }
414        }
415
416        private void next() throws FHIRException {
417                switch (itemType) {
418                case Object :
419                        lexer.consume(TokenType.Open);
420                        lexer.states.push(new State(itemName, false));
421                        if (lexer.getType() == TokenType.Close) {
422                                itemType = ItemType.End;
423                                lexer.next();
424                        } else
425                                parseProperty();
426                        break;
427                case Null:
428                case String:
429                case Number: 
430                case End: 
431                case Boolean :
432                        if (itemType == ItemType.End)
433                                lexer.states.pop();
434                        if (lexer.getType() == TokenType.Comma) {
435                                lexer.next();
436                                parseProperty();
437                        } else if (lexer.getType() == TokenType.Close) {
438                                itemType = ItemType.End;
439                                lexer.next();
440                        } else if (lexer.getType() == TokenType.CloseArray) {
441                                itemType = ItemType.End;
442                                lexer.next();
443                        } else if (lexer.getType() == TokenType.Eof) {
444                                itemType = ItemType.Eof;
445                        } else
446                                throw lexer.error("Unexpected JSON syntax");
447                        break;
448                case Array :
449                        lexer.next();
450                        lexer.states.push(new State(itemName+"[]", true));
451                        parseProperty();
452                        break;
453                case Eof :
454                        throw lexer.error("JSON Syntax Error - attempt to read past end of json stream");
455                default:
456                        throw lexer.error("not done yet (a): "+itemType.toString());
457                }
458        }
459
460        private void parseProperty() throws FHIRException {
461                if (!lexer.states.peek().isProp) {
462                        itemName = lexer.consume(TokenType.String);
463                        itemValue = null;
464                        lexer.consume(TokenType.Colon);
465                }
466                switch (lexer.getType()) {
467                case Null :
468                        itemType = ItemType.Null;
469                        itemValue = lexer.value;
470                        lexer.next();
471                        break;
472                case String :
473                        itemType = ItemType.String;
474                        itemValue = lexer.value;
475                        lexer.next();
476                        break;
477                case Boolean :
478                        itemType = ItemType.Boolean;
479                        itemValue = lexer.value;
480                        lexer.next();
481                        break;
482                case Number :
483                        itemType = ItemType.Number;
484                        itemValue = lexer.value;
485                        lexer.next();
486                        break;
487                case Open :
488                        itemType = ItemType.Object;
489                        break;
490                case OpenArray :
491                        itemType = ItemType.Array;
492                        break;
493                case CloseArray :
494                        itemType = ItemType.End;
495                        break;
496                        // case Close, , case Colon, case Comma, case OpenArray,       !
497                default:
498                        throw lexer.error("not done yet (b): "+lexer.getType().toString());
499                }
500        }
501}