001package org.hl7.fhir.utilities.json;
002
003import java.io.File;
004
005/*-
006 * #%L
007 * org.hl7.fhir.utilities
008 * %%
009 * Copyright (C) 2014 - 2019 Health Level 7
010 * %%
011 * Licensed under the Apache License, Version 2.0 (the "License");
012 * you may not use this file except in compliance with the License.
013 * You may obtain a copy of the License at
014 * 
015 *      http://www.apache.org/licenses/LICENSE-2.0
016 * 
017 * Unless required by applicable law or agreed to in writing, software
018 * distributed under the License is distributed on an "AS IS" BASIS,
019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 * See the License for the specific language governing permissions and
021 * limitations under the License.
022 * #L%
023 */
024
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.math.BigDecimal;
029import java.util.Map;
030import java.util.Stack;
031
032import org.hl7.fhir.utilities.TextFile;
033import org.hl7.fhir.utilities.Utilities;
034
035import com.google.gson.JsonArray;
036import com.google.gson.JsonElement;
037import com.google.gson.JsonNull;
038import com.google.gson.JsonObject;
039import com.google.gson.JsonPrimitive;
040
041
042/**
043 * This is created to get a json parser that can track line numbers... grr...
044 * 
045 * @author Grahame Grieve
046 *
047 */
048public class JsonTrackingParser {
049
050        public class PresentedBigDecimal extends BigDecimal {
051
052          public String presentation;
053          
054    public PresentedBigDecimal(String value) {
055      super(value);
056      presentation = value;
057    }
058
059    public String getPresentation() {
060      return presentation;
061    }
062
063    
064  }
065
066  public enum TokenType {
067                Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean;
068        }
069        
070        public class LocationData {
071                private int line;
072                private int col;
073                
074                protected LocationData(int line, int col) {
075                        super();
076                        this.line = line;
077                        this.col = col;
078                }
079                
080                public int getLine() {
081                        return line;
082                }
083                
084                public int getCol() {
085                        return col;
086                }
087                
088                public void newLine() {
089                        line++;
090                        col = 1;                
091                }
092
093                public LocationData copy() {
094                        return new LocationData(line, col);
095                }
096        }
097        
098        private class State {
099                private String name;
100                private boolean isProp;
101                protected State(String name, boolean isProp) {
102                        super();
103                        this.name = name;
104                        this.isProp = isProp;
105                }
106                public String getName() {
107                        return name;
108                }
109                public boolean isProp() {
110                        return isProp;
111                }
112        }
113        
114        private class Lexer {
115                private String source;
116                private int cursor;
117                private String peek;
118                private String value;
119                private TokenType type;
120                private Stack<State> states = new Stack<State>();
121                private LocationData lastLocationBWS;
122                private LocationData lastLocationAWS;
123                private LocationData location;
124                private StringBuilder b = new StringBuilder();
125                
126    public Lexer(String source) throws IOException {
127        this.source = source;
128        cursor = -1;
129        location = new LocationData(1, 1);  
130        start();
131    }
132    
133    private boolean more() {
134        return peek != null || cursor < source.length(); 
135    }
136    
137    private String getNext(int length) throws IOException {
138        String result = "";
139      if (peek != null) {
140        if (peek.length() > length) {
141                result = peek.substring(0, length);
142                peek = peek.substring(length);
143        } else {
144                result = peek;
145                peek = null;
146        }
147      }
148      if (result.length() < length) {
149        int len = length - result.length(); 
150        if (cursor > source.length() - len) 
151                throw error("Attempt to read past end of source");
152        result = result + source.substring(cursor+1, cursor+len+1);
153        cursor = cursor + len;
154      }
155       for (char ch : result.toCharArray())
156        if (ch == '\n')
157          location.newLine();
158        else
159          location.col++;
160      return result;
161    }
162    
163    private char getNextChar() throws IOException {
164      if (peek != null) {
165        char ch = peek.charAt(0);
166        peek = peek.length() == 1 ? null : peek.substring(1);
167        return ch;
168      } else {
169        cursor++;
170        if (cursor >= source.length())
171          return (char) 0;
172        char ch = source.charAt(cursor);
173        if (ch == '\n') {
174          location.newLine();
175        } else {
176          location.col++;
177        }
178        return ch;
179      }
180    }
181    
182    private void push(char ch){
183        peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek;
184    }
185    
186    private void parseWord(String word, char ch, TokenType type) throws IOException {
187      this.type = type;
188      value = ""+ch+getNext(word.length()-1);
189      if (!value.equals(word))
190        throw error("Syntax error in json reading special word "+word);
191    }
192    
193    private IOException error(String msg) {
194      return new IOException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])");
195    }
196    
197    private String path() {
198      if (states.empty())
199        return value;
200      else {
201        String result = "";
202        for (State s : states) 
203          result = result + '/'+ s.getName();
204        result = result + value;
205        return result;
206      }
207    }
208
209    public void start() throws IOException {
210//      char ch = getNextChar();
211//      if (ch = '\.uEF')
212//      begin
213//        // skip BOM
214//        getNextChar();
215//        getNextChar();
216//      end
217//      else
218//        push(ch);
219      next();
220    }
221    
222    public TokenType getType() {
223        return type;
224    }
225    
226    public String getValue() {
227        return value;
228    }
229
230
231    public LocationData getLastLocationBWS() {
232        return lastLocationBWS;
233    }
234
235    public LocationData getLastLocationAWS() {
236        return lastLocationAWS;
237    }
238
239    public void next() throws IOException {
240        lastLocationBWS = location.copy();
241        char ch;
242        do {
243                ch = getNextChar();
244        } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t'));
245        lastLocationAWS = location.copy();
246
247        if (!more()) {
248                type = TokenType.Eof;
249        } else {
250                switch (ch) {
251                case '{' : 
252                        type = TokenType.Open;
253                        break;
254                case '}' : 
255                        type = TokenType.Close;
256                        break;
257                case '"' :
258                        type = TokenType.String;
259                        b.setLength(0);
260                        do {
261                                ch = getNextChar();
262                                if (ch == '\\') {
263                                        ch = getNextChar();
264                                        switch (ch) {
265              case '"': b.append('"'); break;
266              case '\'': b.append('\''); break;
267                                        case '\\': b.append('\\'); break;
268                                        case '/': b.append('/'); break;
269                                        case 'n': b.append('\n'); break;
270                                        case 'r': b.append('\r'); break;
271                                        case 't': b.append('\t'); break;
272                                        case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break;
273                                        default :
274                                                throw error("unknown escape sequence: \\"+ch);
275                                        }
276                                        ch = ' ';
277                                } else if (ch != '"')
278                                        b.append(ch);
279                        } while (more() && (ch != '"'));
280                        if (!more())
281                                throw error("premature termination of json stream during a string");
282                        value = b.toString();
283                        break;
284                case ':' : 
285                        type = TokenType.Colon;
286                        break;
287                case ',' : 
288                        type = TokenType.Comma;
289                        break;
290                case '[' : 
291                        type = TokenType.OpenArray;
292                        break;
293                case ']' : 
294                        type = TokenType.CloseArray;
295                        break;
296                case 't' : 
297                        parseWord("true", ch, TokenType.Boolean);
298                        break;
299                case 'f' : 
300                        parseWord("false", ch, TokenType.Boolean);
301                        break;
302                case 'n' : 
303                        parseWord("null", ch, TokenType.Null);
304                        break;
305                default:
306                        if ((ch >= '0' && ch <= '9') || ch == '-') {
307                                type = TokenType.Number;
308                                b.setLength(0);
309                                while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') || ch == '+' || ch == 'e' || ch == 'E') {
310                                        b.append(ch);
311                                        ch = getNextChar();
312                                }
313                                value = b.toString();
314                                push(ch);
315                        } else
316                                throw error("Unexpected char '"+ch+"' in json stream");
317                }
318        }
319    }
320
321    public String consume(TokenType type) throws IOException {
322      if (this.type != type)
323        throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString());
324      String result = value;
325      next();
326      return result;
327    }
328
329        }
330
331        enum ItemType {
332          Object, String, Number, Boolean, Array, End, Eof, Null;
333        }
334        private Map<JsonElement, LocationData> map;
335  private Lexer lexer;
336  private ItemType itemType = ItemType.Object;
337  private String itemName;
338  private String itemValue;
339  private boolean errorOnDuplicates = true;
340
341  public static JsonObject parseJson(String source) throws IOException {
342    return parse(source, null);
343  }
344  
345  public static JsonObject parseJson(InputStream stream) throws IOException {
346    return parse(TextFile.streamToString(stream), null);
347  }
348  
349  public static JsonObject parseJson(byte[] stream) throws IOException {
350    return parse(TextFile.bytesToString(stream), null);
351  }
352  
353  public static JsonObject parseJson(byte[] stream, boolean allowDuplicates) throws IOException {
354    return parse(TextFile.bytesToString(stream), null, allowDuplicates);
355  }
356  
357  public static JsonObject parseJson(File source) throws IOException {
358    return parse(TextFile.fileToString(source), null);
359  }
360  
361  public static JsonObject parseJsonFile(String source) throws IOException {
362    return parse(TextFile.fileToString(source), null);
363  }
364  
365  public static JsonObject parse(String source, Map<JsonElement, LocationData> map) throws IOException {
366    return parse(source, map, false);
367  }
368    
369  public static JsonObject parse(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates) throws IOException {
370                JsonTrackingParser self = new JsonTrackingParser();
371                self.map = map;
372                self.setErrorOnDuplicates(!allowDuplicates);
373    return self.parse(Utilities.stripBOM(source));
374        }
375
376        private JsonObject parse(String source) throws IOException {
377                lexer = new Lexer(source);
378                JsonObject result = new JsonObject();
379                LocationData loc = lexer.location.copy();
380    if (lexer.getType() == TokenType.Open) {
381      lexer.next();
382      lexer.states.push(new State("", false));
383    } 
384    else
385      throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString());
386
387    parseProperty();
388    readObject(result, true);
389    if (map != null)
390                  map.put(result, loc);
391    return result;
392        }
393
394        private void readObject(JsonObject obj, boolean root) throws IOException {
395          if (map != null)
396      map.put(obj, lexer.location.copy());
397
398                while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) {
399                        switch (itemType) {
400                        case Object:
401                                JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName);
402                                LocationData loc = lexer.location.copy();
403              if (!obj.has(itemName))
404                obj.add(itemName, child);
405              else if (errorOnDuplicates)
406                throw lexer.error("Duplicated property name: "+itemName);
407                                next();
408                                readObject(child, false);
409                                if (map != null)
410                      map.put(obj, loc);
411                                break;
412                        case Boolean :
413                                JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue));
414        if (!obj.has(itemName))
415                                obj.add(itemName, v);
416        else if (errorOnDuplicates)
417          throw lexer.error("Duplicated property name: "+itemName);
418                                if (map != null)
419                      map.put(v, lexer.location.copy());
420                                break;
421                        case String:
422                                v = new JsonPrimitive(itemValue);
423        if (!obj.has(itemName))
424                                obj.add(itemName, v);
425        else if (errorOnDuplicates)
426          throw lexer.error("Duplicated property name: "+itemName);
427                                if (map != null)
428                      map.put(v, lexer.location.copy());
429                                break;
430                        case Number:
431                                v = new JsonPrimitive(new PresentedBigDecimal(itemValue));
432        if (!obj.has(itemName))
433                                obj.add(itemName, v);
434        else if (errorOnDuplicates)
435          throw lexer.error("Duplicated property name: "+itemName);
436                                if (map != null)
437                      map.put(v, lexer.location.copy());
438                                break;
439                        case Null:
440                                JsonNull n = new JsonNull();
441        if (!obj.has(itemName))
442                                obj.add(itemName, n);
443        else if (errorOnDuplicates)
444          throw lexer.error("Duplicated property name: "+itemName);
445                                if (map != null)
446                      map.put(n, lexer.location.copy());
447                                break;
448                        case Array:
449                                JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName);
450                                loc = lexer.location.copy();
451        if (!obj.has(itemName))
452                                obj.add(itemName, arr);
453        else if (errorOnDuplicates)
454          throw lexer.error("Duplicated property name: "+itemName);
455                                next();
456                                if (!readArray(arr, false))
457                                  next(true);
458                                if (map != null)
459                      map.put(arr, loc);
460                                break;
461                        case Eof : 
462                                throw lexer.error("Unexpected End of File");
463                        case End:
464                                // TODO GG: This isn't handled. Should it be?
465                                break;
466                        }
467                        next();
468                }
469        }
470
471        private boolean readArray(JsonArray arr, boolean root) throws IOException {
472          boolean res = false;
473          while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) {
474            res  = true;
475            switch (itemType) {
476            case Object:
477                JsonObject obj  = new JsonObject(); // (arr.path+'['+inttostr(i)+']');
478                                LocationData loc = lexer.location.copy();
479                arr.add(obj);
480              next();
481              readObject(obj, false);
482              if (map != null)
483                map.put(obj, loc);
484              break;
485            case String:
486                JsonPrimitive v = new JsonPrimitive(itemValue);
487                                arr.add(v);
488                                if (map != null)
489                      map.put(v, lexer.location.copy());
490                                break;
491            case Number:
492                v = new JsonPrimitive(new BigDecimal(itemValue));
493                                arr.add(v);
494                                if (map != null)
495                      map.put(v, lexer.location.copy());
496                                break;
497            case Null :
498                JsonNull n = new JsonNull();
499                                arr.add(n);
500                                if (map != null)
501                      map.put(n, lexer.location.copy());
502                                break;
503            case Array:
504        JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']');
505                                loc = lexer.location.copy();
506                                arr.add(child);
507        next();
508              readArray(child, false);
509              if (map != null)
510                map.put(arr, loc);
511        break;
512            case Eof : 
513                throw lexer.error("Unexpected End of File");
514                 case End:
515                 case Boolean:
516                        // TODO GG: These aren't handled. SHould they be?
517                        break;
518            }
519            next();
520          }
521          return res;
522        }
523
524  private void next() throws IOException {
525    next(false);
526  }
527  
528        private void next(boolean noPop) throws IOException {
529                switch (itemType) {
530                case Object :
531                        lexer.consume(TokenType.Open);
532                        lexer.states.push(new State(itemName, false));
533                        if (lexer.getType() == TokenType.Close) {
534                                itemType = ItemType.End;
535                                lexer.next();
536                        } else
537                                parseProperty();
538                        break;
539                case Null:
540                case String:
541                case Number: 
542                case End: 
543                case Boolean :
544                        if (itemType == ItemType.End && !noPop)
545                                lexer.states.pop();
546                        if (lexer.getType() == TokenType.Comma) {
547                                lexer.next();
548                                parseProperty();
549                        } else if (lexer.getType() == TokenType.Close) {
550                                itemType = ItemType.End;
551                                lexer.next();
552                        } else if (lexer.getType() == TokenType.CloseArray) {
553                                itemType = ItemType.End;
554                                lexer.next();
555                        } else if (lexer.getType() == TokenType.Eof) {
556                                itemType = ItemType.Eof;
557                        } else
558                                throw lexer.error("Unexpected JSON syntax");
559                        break;
560                case Array :
561                        lexer.next();
562                        lexer.states.push(new State(itemName+"[]", true));
563                        parseProperty();
564                        break;
565                case Eof :
566                        throw lexer.error("JSON Syntax Error - attempt to read past end of json stream");
567                default:
568                        throw lexer.error("not done yet (a): "+itemType.toString());
569                }
570        }
571
572        private void parseProperty() throws IOException {
573                if (!lexer.states.peek().isProp) {
574                        itemName = lexer.consume(TokenType.String);
575                        itemValue = null;
576                        lexer.consume(TokenType.Colon);
577                }
578                switch (lexer.getType()) {
579                case Null :
580                        itemType = ItemType.Null;
581                        itemValue = lexer.value;
582                        lexer.next();
583                        break;
584                case String :
585                        itemType = ItemType.String;
586                        itemValue = lexer.value;
587                        lexer.next();
588                        break;
589                case Boolean :
590                        itemType = ItemType.Boolean;
591                        itemValue = lexer.value;
592                        lexer.next();
593                        break;
594                case Number :
595                        itemType = ItemType.Number;
596                        itemValue = lexer.value;
597                        lexer.next();
598                        break;
599                case Open :
600                        itemType = ItemType.Object;
601                        break;
602                case OpenArray :
603                        itemType = ItemType.Array;
604                        break;
605                case CloseArray :
606                        itemType = ItemType.End;
607                        break;
608                        // case Close, , case Colon, case Comma, case OpenArray,       !
609                default:
610                        throw lexer.error("not done yet (b): "+lexer.getType().toString());
611                }
612        }
613
614  public boolean isErrorOnDuplicates() {
615    return errorOnDuplicates;
616  }
617
618  public void setErrorOnDuplicates(boolean errorOnDuplicates) {
619    this.errorOnDuplicates = errorOnDuplicates;
620  }
621        
622        
623}