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                        }
364                        next();
365                }
366        }
367
368        private void readArray(JsonArray arr, boolean root) throws FHIRException {
369          while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) {
370            switch (itemType) {
371            case Object:
372                JsonObject obj  = new JsonObject(); // (arr.path+'['+inttostr(i)+']');
373                                LocationData loc = lexer.location.copy();
374                arr.add(obj);
375              next();
376              readObject(obj, false);
377                                map.put(obj, loc);
378              break;
379            case String:
380                JsonPrimitive v = new JsonPrimitive(itemValue);
381                                arr.add(v);
382                                map.put(v, lexer.location.copy());
383                                break;
384            case Number:
385                v = new JsonPrimitive(new BigDecimal(itemValue));
386                                arr.add(v);
387                                map.put(v, lexer.location.copy());
388                                break;
389            case Null :
390                JsonNull n = new JsonNull();
391                                arr.add(n);
392                                map.put(n, lexer.location.copy());
393                                break;
394            case Array:
395        JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']');
396                                loc = lexer.location.copy();
397                                arr.add(child);
398        next();
399              readArray(child, false);
400                                map.put(arr, loc);
401        break;
402            case Eof : 
403                throw lexer.error("Unexpected End of File");
404            }
405            next();
406          }
407        }
408
409        private void next() throws FHIRException {
410                switch (itemType) {
411                case Object :
412                        lexer.consume(TokenType.Open);
413                        lexer.states.push(new State(itemName, false));
414                        if (lexer.getType() == TokenType.Close) {
415                                itemType = ItemType.End;
416                                lexer.next();
417                        } else
418                                parseProperty();
419                        break;
420                case Null:
421                case String:
422                case Number: 
423                case End: 
424                case Boolean :
425                        if (itemType == ItemType.End)
426                                lexer.states.pop();
427                        if (lexer.getType() == TokenType.Comma) {
428                                lexer.next();
429                                parseProperty();
430                        } else if (lexer.getType() == TokenType.Close) {
431                                itemType = ItemType.End;
432                                lexer.next();
433                        } else if (lexer.getType() == TokenType.CloseArray) {
434                                itemType = ItemType.End;
435                                lexer.next();
436                        } else if (lexer.getType() == TokenType.Eof) {
437                                itemType = ItemType.Eof;
438                        } else
439                                throw lexer.error("Unexpected JSON syntax");
440                        break;
441                case Array :
442                        lexer.next();
443                        lexer.states.push(new State(itemName+"[]", true));
444                        parseProperty();
445                        break;
446                case Eof :
447                        throw lexer.error("JSON Syntax Error - attempt to read past end of json stream");
448                default:
449                        throw lexer.error("not done yet (a): "+itemType.toString());
450                }
451        }
452
453        private void parseProperty() throws FHIRException {
454                if (!lexer.states.peek().isProp) {
455                        itemName = lexer.consume(TokenType.String);
456                        itemValue = null;
457                        lexer.consume(TokenType.Colon);
458                }
459                switch (lexer.getType()) {
460                case Null :
461                        itemType = ItemType.Null;
462                        itemValue = lexer.value;
463                        lexer.next();
464                        break;
465                case String :
466                        itemType = ItemType.String;
467                        itemValue = lexer.value;
468                        lexer.next();
469                        break;
470                case Boolean :
471                        itemType = ItemType.Boolean;
472                        itemValue = lexer.value;
473                        lexer.next();
474                        break;
475                case Number :
476                        itemType = ItemType.Number;
477                        itemValue = lexer.value;
478                        lexer.next();
479                        break;
480                case Open :
481                        itemType = ItemType.Object;
482                        break;
483                case OpenArray :
484                        itemType = ItemType.Array;
485                        break;
486                case CloseArray :
487                        itemType = ItemType.End;
488                        break;
489                        // case Close, , case Colon, case Comma, case OpenArray,       !
490                default:
491                        throw lexer.error("not done yet (b): "+lexer.getType().toString());
492                }
493        }
494}