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