001package org.hl7.fhir.utilities.json;
002
003import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
004
005import java.io.File;
006
007/*
008  Copyright (c) 2011+, HL7, Inc.
009  All rights reserved.
010  
011  Redistribution and use in source and binary forms, with or without modification, 
012  are permitted provided that the following conditions are met:
013    
014   * Redistributions of source code must retain the above copyright notice, this 
015     list of conditions and the following disclaimer.
016   * Redistributions in binary form must reproduce the above copyright notice, 
017     this list of conditions and the following disclaimer in the documentation 
018     and/or other materials provided with the distribution.
019   * Neither the name of HL7 nor the names of its contributors may be used to 
020     endorse or promote products derived from this software without specific 
021     prior written permission.
022  
023  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
024  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
025  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
026  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
027  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
028  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
029  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
030  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
031  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
032  POSSIBILITY OF SUCH DAMAGE.
033  
034 */
035
036
037
038import java.io.IOException;
039import java.io.InputStream;
040import java.math.BigDecimal;
041import java.nio.charset.StandardCharsets;
042import java.util.Map;
043import java.util.Stack;
044
045import org.hl7.fhir.utilities.SimpleHTTPClient;
046import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult;
047import org.hl7.fhir.utilities.TextFile;
048import org.hl7.fhir.utilities.Utilities;
049
050import com.google.gson.Gson;
051import com.google.gson.GsonBuilder;
052import com.google.gson.JsonArray;
053import com.google.gson.JsonElement;
054import com.google.gson.JsonNull;
055import com.google.gson.JsonObject;
056import com.google.gson.JsonPrimitive;
057
058
059/**
060 * This is created to get a json parser that can track line numbers... grr...
061 * 
062 * @author Grahame Grieve
063 *
064 */
065public class JsonTrackingParser {
066
067        public class PresentedBigDecimal extends BigDecimal {
068
069          public String presentation;
070          
071    public PresentedBigDecimal(String value) {
072      super(value);
073      presentation = value;
074    }
075
076    public String getPresentation() {
077      return presentation;
078    }
079
080  }
081
082  public enum TokenType {
083                Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean;
084        }
085        
086        public class LocationData {
087                private int line;
088                private int col;
089                
090                protected LocationData(int line, int col) {
091                        super();
092                        this.line = line;
093                        this.col = col;
094                }
095                
096                public int getLine() {
097                        return line;
098                }
099                
100                public int getCol() {
101                        return col;
102                }
103                
104                public void newLine() {
105                        line++;
106                        col = 1;                
107                }
108
109                public LocationData copy() {
110                        return new LocationData(line, col);
111                }
112        }
113        
114        private class State {
115                private String name;
116                private boolean isProp;
117                protected State(String name, boolean isProp) {
118                        super();
119                        this.name = name;
120                        this.isProp = isProp;
121                }
122                public String getName() {
123                        return name;
124                }
125                public boolean isProp() {
126                        return isProp;
127                }
128        }
129        
130        private class Lexer {
131                private String source;
132                private int cursor;
133                private String peek;
134                private String value;
135                private TokenType type;
136                private Stack<State> states = new Stack<State>();
137                private LocationData lastLocationBWS;
138                private LocationData lastLocationAWS;
139                private LocationData location;
140                private StringBuilder b = new StringBuilder();
141                
142    public Lexer(String source) throws IOException {
143        this.source = source;
144        cursor = -1;
145        location = new LocationData(1, 1);  
146        start();
147    }
148    
149    private boolean more() {
150        return peek != null || cursor < source.length(); 
151    }
152    
153    private String getNext(int length) throws IOException {
154        String result = "";
155      if (peek != null) {
156        if (peek.length() > length) {
157                result = peek.substring(0, length);
158                peek = peek.substring(length);
159        } else {
160                result = peek;
161                peek = null;
162        }
163      }
164      if (result.length() < length) {
165        int len = length - result.length(); 
166        if (cursor > source.length() - len) 
167                throw error("Attempt to read past end of source");
168        result = result + source.substring(cursor+1, cursor+len+1);
169        cursor = cursor + len;
170      }
171       for (char ch : result.toCharArray())
172        if (ch == '\n')
173          location.newLine();
174        else
175          location.col++;
176      return result;
177    }
178    
179    private char getNextChar() throws IOException {
180      if (peek != null) {
181        char ch = peek.charAt(0);
182        peek = peek.length() == 1 ? null : peek.substring(1);
183        return ch;
184      } else {
185        cursor++;
186        if (cursor >= source.length())
187          return (char) 0;
188        char ch = source.charAt(cursor);
189        if (ch == '\n') {
190          location.newLine();
191        } else {
192          location.col++;
193        }
194        return ch;
195      }
196    }
197    
198    private void push(char ch){
199        peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek;
200    }
201    
202    private void parseWord(String word, char ch, TokenType type) throws IOException {
203      this.type = type;
204      value = ""+ch+getNext(word.length()-1);
205      if (!value.equals(word))
206        throw error("Syntax error in json reading special word "+word);
207    }
208    
209    private IOException error(String msg) {
210      return new IOException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])");
211    }
212    
213    private String path() {
214      if (states.empty())
215        return value;
216      else {
217        String result = "";
218        for (State s : states) 
219          result = result + '/'+ s.getName();
220        result = result + value;
221        return result;
222      }
223    }
224
225    public void start() throws IOException {
226//      char ch = getNextChar();
227//      if (ch = '\.uEF')
228//      begin
229//        // skip BOM
230//        getNextChar();
231//        getNextChar();
232//      end
233//      else
234//        push(ch);
235      next();
236    }
237    
238    public TokenType getType() {
239        return type;
240    }
241    
242    public String getValue() {
243        return value;
244    }
245
246
247    public LocationData getLastLocationBWS() {
248        return lastLocationBWS;
249    }
250
251    public LocationData getLastLocationAWS() {
252        return lastLocationAWS;
253    }
254
255    public void next() throws IOException {
256        lastLocationBWS = location.copy();
257        char ch;
258        do {
259                ch = getNextChar();
260                if (allowComments && ch == '/') {
261                  char ch1 = getNextChar();
262                  if (ch1 == '/') {
263                    while (more() && !Utilities.charInSet(ch, '\r', '\n')) {
264                      ch = getNextChar();
265                    }
266                  } else {
267                    push(ch1);
268                  }               
269                }
270        } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t'));
271        lastLocationAWS = location.copy();
272
273        if (!more()) {
274                type = TokenType.Eof;
275        } else {
276                switch (ch) {
277                case '{' : 
278                        type = TokenType.Open;
279                        break;
280                case '}' : 
281                        type = TokenType.Close;
282                        break;
283                case '"' :
284                        type = TokenType.String;
285                        b.setLength(0);
286                        do {
287                                ch = getNextChar();
288                                if (ch == '\\') {
289                                        ch = getNextChar();
290                                        switch (ch) {
291              case '"': b.append('"'); break;
292              case '\'': b.append('\''); break;
293                                        case '\\': b.append('\\'); break;
294                                        case '/': b.append('/'); break;
295                                        case 'n': b.append('\n'); break;
296                                        case 'r': b.append('\r'); break;
297                                        case 't': b.append('\t'); break;
298                                        case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break;
299                                        default :
300                                                throw error("unknown escape sequence: \\"+ch);
301                                        }
302                                        ch = ' ';
303                                } else if (ch != '"')
304                                        b.append(ch);
305                        } while (more() && (ch != '"'));
306                        if (!more())
307                                throw error("premature termination of json stream during a string");
308                        value = b.toString();
309                        break;
310                case ':' : 
311                        type = TokenType.Colon;
312                        break;
313                case ',' : 
314                        type = TokenType.Comma;
315                        break;
316                case '[' : 
317                        type = TokenType.OpenArray;
318                        break;
319                case ']' : 
320                        type = TokenType.CloseArray;
321                        break;
322                case 't' : 
323                        parseWord("true", ch, TokenType.Boolean);
324                        break;
325                case 'f' : 
326                        parseWord("false", ch, TokenType.Boolean);
327                        break;
328                case 'n' : 
329                        parseWord("null", ch, TokenType.Null);
330                        break;
331                default:
332                        if ((ch >= '0' && ch <= '9') || ch == '-') {
333                                type = TokenType.Number;
334                                b.setLength(0);
335                                while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') || ch == '+' || ch == 'e' || ch == 'E') {
336                                        b.append(ch);
337                                        ch = getNextChar();
338                                }
339                                value = b.toString();
340                                push(ch);
341                        } else
342                                throw error("Unexpected char '"+ch+"' in json stream");
343                }
344        }
345    }
346
347    public String consume(TokenType type) throws IOException {
348      if (this.type != type)
349        throw error("JSON syntax error - found "+this.type.toString()+" expecting "+type.toString());
350      String result = value;
351      next();
352      return result;
353    }
354
355        }
356
357        enum ItemType {
358          Object, String, Number, Boolean, Array, End, Eof, Null;
359        }
360        private Map<JsonElement, LocationData> map;
361  private Lexer lexer;
362  private ItemType itemType = ItemType.Object;
363  private String itemName;
364  private String itemValue;
365  private boolean errorOnDuplicates = true;
366  private boolean allowComments = false;
367
368  public static JsonObject parseJson(String source) throws IOException {
369    return parse(source, null);
370  }
371  
372  public static JsonObject parseJson(InputStream stream) throws IOException {
373    return parse(TextFile.streamToString(stream), null);
374  }
375  
376  public static JsonObject parseJson(byte[] stream) throws IOException {
377    return parse(TextFile.bytesToString(stream), null);
378  }
379  
380  public static JsonArray parseJsonArray(byte[] stream) throws IOException {
381    return parseArray(TextFile.bytesToString(stream), null);
382  }
383  
384  public static JsonObject parseJson(byte[] stream, boolean allowDuplicates) throws IOException {
385    return parse(TextFile.bytesToString(stream), null, allowDuplicates);
386  }
387  
388  public static JsonObject parseJson(File source) throws IOException {
389    return parse(TextFile.fileToString(source), null);
390  }
391  
392  public static JsonObject parseJsonFile(String source) throws IOException {
393    return parse(TextFile.fileToString(source), null);
394  }
395  
396  public static JsonObject parse(String source, Map<JsonElement, LocationData> map) throws IOException {
397    return parse(source, map, false);
398  }
399    
400  public static JsonArray parseArray(String source, Map<JsonElement, LocationData> map) throws IOException {
401    return parseArray(source, map, false);
402  }
403    
404    
405  public static JsonObject parse(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates) throws IOException {
406    return parse(source, map, allowDuplicates, false);
407  }
408  
409  public static JsonArray parseArray(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates) throws IOException {
410    return parseArray(source, map, allowDuplicates, false);
411  }
412  
413  public static JsonObject parse(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates, boolean allowComments) throws IOException {
414    JsonTrackingParser self = new JsonTrackingParser();
415    self.map = map;
416    self.setErrorOnDuplicates(!allowDuplicates);
417    self.setAllowComments(allowComments);
418    return self.parse(Utilities.stripBOM(source));
419  }
420
421  public static JsonArray parseArray(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates, boolean allowComments) throws IOException {
422    JsonTrackingParser self = new JsonTrackingParser();
423    self.map = map;
424    self.setErrorOnDuplicates(!allowDuplicates);
425    self.setAllowComments(allowComments);
426    return self.parseArray(Utilities.stripBOM(source));
427  }
428
429        private JsonObject parse(String source) throws IOException {
430                lexer = new Lexer(source);
431                JsonObject result = new JsonObject();
432                LocationData loc = lexer.location.copy();
433    if (lexer.getType() == TokenType.Open) {
434      lexer.next();
435      lexer.states.push(new State("", false));
436    } 
437    else
438      throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString());
439
440    if (lexer.getType() != TokenType.Close) {
441      parseProperty();
442      readObject(result, true);
443    }
444    if (map != null)
445                  map.put(result, loc);
446    return result;
447        }
448
449  private JsonArray parseArray(String source) throws IOException {
450    return new Gson().fromJson(source, JsonArray.class);
451  }
452
453        private void readObject(JsonObject obj, boolean root) throws IOException {
454          if (map != null)
455      map.put(obj, lexer.location.copy());
456
457                while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) {
458                        switch (itemType) {
459                        case Object:
460                                JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName);
461                                LocationData loc = lexer.location.copy();
462              if (!obj.has(itemName))
463                obj.add(itemName, child);
464              else if (errorOnDuplicates)
465                throw lexer.error("Duplicated property name: "+itemName);
466                                next();
467                                readObject(child, false);
468                                if (map != null)
469                      map.put(obj, loc);
470                                break;
471                        case Boolean :
472                                JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue));
473        if (!obj.has(itemName))
474                                obj.add(itemName, v);
475        else if (errorOnDuplicates)
476          throw lexer.error("Duplicated property name: "+itemName);
477                                if (map != null)
478                      map.put(v, lexer.location.copy());
479                                break;
480                        case String:
481                                v = new JsonPrimitive(itemValue);
482        if (!obj.has(itemName))
483                                obj.add(itemName, v);
484        else if (errorOnDuplicates)
485          throw lexer.error("Duplicated property name: "+itemName);
486                                if (map != null)
487                      map.put(v, lexer.location.copy());
488                                break;
489                        case Number:
490                                v = new JsonPrimitive(new PresentedBigDecimal(itemValue));
491        if (!obj.has(itemName))
492                                obj.add(itemName, v);
493        else if (errorOnDuplicates)
494          throw lexer.error("Duplicated property name: "+itemName);
495                                if (map != null)
496                      map.put(v, lexer.location.copy());
497                                break;
498                        case Null:
499                                JsonNull n = new JsonNull();
500        if (!obj.has(itemName))
501                                obj.add(itemName, n);
502        else if (errorOnDuplicates)
503          throw lexer.error("Duplicated property name: "+itemName);
504                                if (map != null)
505                      map.put(n, lexer.location.copy());
506                                break;
507                        case Array:
508                                JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName);
509                                loc = lexer.location.copy();
510        if (!obj.has(itemName))
511                                obj.add(itemName, arr);
512        else if (errorOnDuplicates)
513          throw lexer.error("Duplicated property name: "+itemName);
514                                next();
515                                if (!readArray(arr, false))
516                                  next(true);
517                                if (map != null)
518                      map.put(arr, loc);
519                                break;
520                        case Eof : 
521                                throw lexer.error("Unexpected End of File");
522                        case End:
523                                // TODO GG: This isn't handled. Should it be?
524                                break;
525                        }
526                        next();
527                }
528        }
529
530        private boolean readArray(JsonArray arr, boolean root) throws IOException {
531          boolean res = false;
532          while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) {
533            res  = true;
534            switch (itemType) {
535            case Object:
536                JsonObject obj  = new JsonObject(); // (arr.path+'['+inttostr(i)+']');
537                                LocationData loc = lexer.location.copy();
538                arr.add(obj);
539              next();
540              readObject(obj, false);
541              if (map != null)
542                map.put(obj, loc);
543              break;
544            case String:
545                JsonPrimitive v = new JsonPrimitive(itemValue);
546                                arr.add(v);
547                                if (map != null)
548                      map.put(v, lexer.location.copy());
549                                break;
550            case Number:
551                v = new JsonPrimitive(new BigDecimal(itemValue));
552                                arr.add(v);
553                                if (map != null)
554                      map.put(v, lexer.location.copy());
555                                break;
556            case Null :
557                JsonNull n = new JsonNull();
558                                arr.add(n);
559                                if (map != null)
560                      map.put(n, lexer.location.copy());
561                                break;
562            case Array:
563        JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']');
564                                loc = lexer.location.copy();
565                                arr.add(child);
566        next();
567              readArray(child, false);
568              if (map != null)
569                map.put(arr, loc);
570        break;
571            case Eof : 
572                throw lexer.error("Unexpected End of File");
573                 case End:
574                 case Boolean:
575                        // TODO GG: These aren't handled. SHould they be?
576                        break;
577            }
578            next();
579          }
580          return res;
581        }
582
583  private void next() throws IOException {
584    next(false);
585  }
586  
587        private void next(boolean noPop) throws IOException {
588                switch (itemType) {
589                case Object :
590                        lexer.consume(TokenType.Open);
591                        lexer.states.push(new State(itemName, false));
592                        if (lexer.getType() == TokenType.Close) {
593                                itemType = ItemType.End;
594                                lexer.next();
595                        } else
596                                parseProperty();
597                        break;
598                case Null:
599                case String:
600                case Number: 
601                case End: 
602                case Boolean :
603                        if (itemType == ItemType.End && !noPop)
604                                lexer.states.pop();
605                        if (lexer.getType() == TokenType.Comma) {
606                                lexer.next();
607                                parseProperty();
608                        } else if (lexer.getType() == TokenType.Close) {
609                                itemType = ItemType.End;
610                                lexer.next();
611                        } else if (lexer.getType() == TokenType.CloseArray) {
612                                itemType = ItemType.End;
613                                lexer.next();
614                        } else if (lexer.getType() == TokenType.Eof) {
615                                itemType = ItemType.Eof;
616                        } else
617                                throw lexer.error("Unexpected JSON syntax");
618                        break;
619                case Array :
620                        lexer.next();
621                        lexer.states.push(new State(itemName+"[]", true));
622                        parseProperty();
623                        break;
624                case Eof :
625                        throw lexer.error("JSON Syntax Error - attempt to read past end of json stream");
626                default:
627                        throw lexer.error("not done yet (a): "+itemType.toString());
628                }
629        }
630
631        private void parseProperty() throws IOException {
632                if (!lexer.states.peek().isProp) {
633                        itemName = lexer.consume(TokenType.String);
634                        itemValue = null;
635                        lexer.consume(TokenType.Colon);
636                }
637                switch (lexer.getType()) {
638                case Null :
639                        itemType = ItemType.Null;
640                        itemValue = lexer.value;
641                        lexer.next();
642                        break;
643                case String :
644                        itemType = ItemType.String;
645                        itemValue = lexer.value;
646                        lexer.next();
647                        break;
648                case Boolean :
649                        itemType = ItemType.Boolean;
650                        itemValue = lexer.value;
651                        lexer.next();
652                        break;
653                case Number :
654                        itemType = ItemType.Number;
655                        itemValue = lexer.value;
656                        lexer.next();
657                        break;
658                case Open :
659                        itemType = ItemType.Object;
660                        break;
661                case OpenArray :
662                        itemType = ItemType.Array;
663                        break;
664                case CloseArray :
665                        itemType = ItemType.End;
666                        break;
667                        // case Close, , case Colon, case Comma, case OpenArray,       !
668                default:
669                        throw lexer.error("not done yet (b): "+lexer.getType().toString());
670                }
671        }
672
673  public boolean isErrorOnDuplicates() {
674    return errorOnDuplicates;
675  }
676
677  public void setErrorOnDuplicates(boolean errorOnDuplicates) {
678    this.errorOnDuplicates = errorOnDuplicates;
679  }
680
681  
682  public boolean isAllowComments() {
683    return allowComments;
684  }
685
686  public void setAllowComments(boolean allowComments) {
687    this.allowComments = allowComments;
688  }
689
690  public static void write(JsonObject json, File file) throws IOException {
691    Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
692    String jcnt = gson.toJson(json);
693    TextFile.stringToFile(jcnt, file);    
694  }
695    
696  public static void write(JsonObject json, File file, boolean pretty) throws IOException {
697    Gson gson = pretty ? new GsonBuilder().setPrettyPrinting().create() : new GsonBuilder().create();
698    String jcnt = gson.toJson(json);
699    TextFile.stringToFile(jcnt, file);    
700  }
701    
702  public static void write(JsonObject json, String fileName) throws IOException {
703    Gson gson = new GsonBuilder().setPrettyPrinting().create();
704    String jcnt = gson.toJson(json);
705    TextFile.stringToFile(jcnt, fileName);    
706  }
707  
708  public static String write(JsonObject json) {
709    Gson gson = new GsonBuilder().setPrettyPrinting().create();
710    return gson.toJson(json);    
711  }
712
713  public static String writeDense(JsonObject json) {
714    Gson gson = new GsonBuilder().create();
715    return gson.toJson(json);    
716  }
717
718  public static byte[] writeBytes(JsonObject json, boolean pretty) {
719    if (pretty) {
720      Gson gson = new GsonBuilder().setPrettyPrinting().create();
721      return gson.toJson(json).getBytes(StandardCharsets.UTF_8);    
722    } else {
723      Gson gson = new GsonBuilder().create();
724      return gson.toJson(json).getBytes(StandardCharsets.UTF_8);    
725    }    
726  }
727  
728  public static JsonObject fetchJson(String source) throws IOException {
729    SimpleHTTPClient fetcher = new SimpleHTTPClient();
730    HTTPResult res = fetcher.get(source+"?nocache=" + System.currentTimeMillis());
731    res.checkThrowException();
732    return parseJson(res.getContent());
733  }
734  
735  public static JsonArray fetchJsonArray(String source) throws IOException {
736    SimpleHTTPClient fetcher = new SimpleHTTPClient();
737    HTTPResult res = fetcher.get(source+"?nocache=" + System.currentTimeMillis());
738    res.checkThrowException();
739    return parseJsonArray(res.getContent());
740  }
741        
742}