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