001package org.hl7.fhir.dstu2016may.utils;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.math.BigDecimal;
035import java.util.Map;
036import java.util.Stack;
037
038import org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.utilities.Utilities;
040
041import com.google.gson.JsonArray;
042import com.google.gson.JsonElement;
043import com.google.gson.JsonNull;
044import com.google.gson.JsonObject;
045import com.google.gson.JsonPrimitive;
046
047
048/**
049 * This is created to get a json parser that can track line numbers... grr...
050 * 
051 * @author Grahame Grieve
052 *
053 */
054public class JsonTrackingParser {
055
056        public enum TokenType {
057                Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean;
058        }
059        
060        public class LocationData {
061                private int line;
062                private int col;
063                
064                protected LocationData(int line, int col) {
065                        super();
066                        this.line = line;
067                        this.col = col;
068                }
069                
070                public int getLine() {
071                        return line;
072                }
073                
074                public int getCol() {
075                        return col;
076                }
077                
078                public void newLine() {
079                        line++;
080                        col = 1;                
081                }
082
083                public LocationData copy() {
084                        return new LocationData(line, col);
085                }
086        }
087        
088        private class State {
089                private String name;
090                private boolean isProp;
091                protected State(String name, boolean isProp) {
092                        super();
093                        this.name = name;
094                        this.isProp = isProp;
095                }
096                public String getName() {
097                        return name;
098                }
099                public boolean isProp() {
100                        return isProp;
101                }
102        }
103        
104        private class Lexer {
105                private String source;
106                private int cursor;
107                private String peek;
108                private String value;
109                private TokenType type;
110                private Stack<State> states = new Stack<State>();
111                private LocationData lastLocationBWS;
112                private LocationData lastLocationAWS;
113                private LocationData location;
114                private StringBuilder b = new StringBuilder();
115                
116    public Lexer(String source) throws FHIRException {
117        this.source = source;
118        cursor = -1;
119        location = new LocationData(1, 1);  
120        start();
121    }
122    
123    private boolean more() {
124        return peek != null || cursor < source.length(); 
125    }
126    
127    private String getNext(int length) throws FHIRException {
128        String result = "";
129      if (peek != null) {
130        if (peek.length() > length) {
131                result = peek.substring(0, length);
132                peek = peek.substring(length);
133        } else {
134                result = peek;
135                peek = null;
136        }
137      }
138      if (result.length() < length) {
139        int len = length - result.length(); 
140        if (cursor > source.length() - len) 
141                throw error("Attempt to read past end of source");
142        result = result + source.substring(cursor+1, cursor+len+1);
143        cursor = cursor + len;
144      }
145       for (char ch : result.toCharArray())
146        if (ch == '\n')
147          location.newLine();
148        else
149          location.col++;
150      return result;
151    }
152    
153    private char getNextChar() throws FHIRException {
154      if (peek != null) {
155        char ch = peek.charAt(0);
156        peek = peek.length() == 1 ? null : peek.substring(1);
157        return ch;
158      } else {
159        cursor++;
160        if (cursor >= source.length())
161          return (char) 0;
162        char ch = source.charAt(cursor);
163        if (ch == '\n') {
164          location.newLine();
165        } else {
166          location.col++;
167        }
168        return ch;
169      }
170    }
171    
172    private void push(char ch){
173        peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek;
174    }
175    
176    private void parseWord(String word, char ch, TokenType type) throws FHIRException {
177      this.type = type;
178      value = ""+ch+getNext(word.length()-1);
179      if (!value.equals(word))
180        throw error("Syntax error in json reading special word "+word);
181    }
182    
183    private FHIRException error(String msg) {
184      return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])");
185    }
186    
187    private String path() {
188      if (states.empty())
189        return value;
190      else {
191        String result = "";
192        for (State s : states) 
193          result = result + '/'+ s.getName();
194        result = result + value;
195        return result;
196      }
197    }
198
199    public void start() throws FHIRException {
200//      char ch = getNextChar();
201//      if (ch = '\.uEF')
202//      begin
203//        // skip BOM
204//        getNextChar();
205//        getNextChar();
206//      end
207//      else
208//        push(ch);
209      next();
210    }
211    
212    public TokenType getType() {
213        return type;
214    }
215    
216    public String getValue() {
217        return value;
218    }
219
220
221    public LocationData getLastLocationBWS() {
222        return lastLocationBWS;
223    }
224
225    public LocationData getLastLocationAWS() {
226        return lastLocationAWS;
227    }
228
229    public void next() throws FHIRException {
230        lastLocationBWS = location.copy();
231        char ch;
232        do {
233                ch = getNextChar();
234        } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t'));
235        lastLocationAWS = location.copy();
236
237        if (!more()) {
238                type = TokenType.Eof;
239        } else {
240                switch (ch) {
241                case '{' : 
242                        type = TokenType.Open;
243                        break;
244                case '}' : 
245                        type = TokenType.Close;
246                        break;
247                case '"' :
248                        type = TokenType.String;
249                        b.setLength(0);
250                        do {
251                                ch = getNextChar();
252                                if (ch == '\\') {
253                                        ch = getNextChar();
254                                        switch (ch) {
255                                        case '"': b.append('"'); break;
256                                        case '\\': b.append('\\'); break;
257                                        case '/': b.append('/'); break;
258                                        case 'n': b.append('\n'); break;
259                                        case 'r': b.append('\r'); break;
260                                        case 't': b.append('\t'); break;
261                                        case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break;
262                                        default :
263                                                throw error("unknown escape sequence: \\"+ch);
264                                        }
265                                        ch = ' ';
266                                } else if (ch != '"')
267                                        b.append(ch);
268                        } while (more() && (ch != '"'));
269                        if (!more())
270                                throw error("premature termination of json stream during a string");
271                        value = b.toString();
272                        break;
273                case ':' : 
274                        type = TokenType.Colon;
275                        break;
276                case ',' : 
277                        type = TokenType.Comma;
278                        break;
279                case '[' : 
280                        type = TokenType.OpenArray;
281                        break;
282                case ']' : 
283                        type = TokenType.CloseArray;
284                        break;
285                case 't' : 
286                        parseWord("true", ch, TokenType.Boolean);
287                        break;
288                case 'f' : 
289                        parseWord("false", ch, TokenType.Boolean);
290                        break;
291                case 'n' : 
292                        parseWord("null", ch, TokenType.Null);
293                        break;
294                default:
295                        if ((ch >= '0' && ch <= '9') || ch == '-') {
296                                type = TokenType.Number;
297                                b.setLength(0);
298                                while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) {
299                                        b.append(ch);
300                                        ch = getNextChar();
301                                }
302                                value = b.toString();
303                                push(ch);
304                        } else
305                                throw error("Unexpected char '"+ch+"' in json stream");
306                }
307        }
308    }
309
310    public String consume(TokenType type) throws FHIRException {
311      if (this.type != type)
312        throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString());
313      String result = value;
314      next();
315      return result;
316    }
317
318        }
319
320        enum ItemType {
321          Object, String, Number, Boolean, Array, End, Eof, Null;
322        }
323        private Map<JsonElement, LocationData> map;
324  private Lexer lexer;
325  private ItemType itemType = ItemType.Object;
326  private String itemName;
327  private String itemValue;
328
329        public static JsonObject parse(String source, Map<JsonElement, LocationData> map) throws FHIRException {
330                JsonTrackingParser self = new JsonTrackingParser();
331                self.map = map;
332    return self.parse(source);
333        }
334
335        private JsonObject parse(String source) throws FHIRException {
336                lexer = new Lexer(source);
337                JsonObject result = new JsonObject();
338                LocationData loc = lexer.location.copy();
339    if (lexer.getType() == TokenType.Open) {
340      lexer.next();
341      lexer.states.push(new State("", false));
342    } 
343    else
344      throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString());
345
346    parseProperty();
347    readObject(result, true);
348                map.put(result, loc);
349    return result;
350        }
351
352        private void readObject(JsonObject obj, boolean root) throws FHIRException {
353                map.put(obj, lexer.location.copy());
354
355                while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) {
356                        if (obj.has(itemName))
357                                throw lexer.error("Duplicated property name: "+itemName);
358
359                        switch (itemType) {
360                        case Object:
361                                JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName);
362                                LocationData loc = lexer.location.copy();
363                                obj.add(itemName, child);
364                                next();
365                                readObject(child, false);
366                                map.put(obj, loc);
367                                break;
368                        case Boolean :
369                                JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue));
370                                obj.add(itemName, v);
371                                map.put(v, lexer.location.copy());
372                                break;
373                        case String:
374                                v = new JsonPrimitive(itemValue);
375                                obj.add(itemName, v);
376                                map.put(v, lexer.location.copy());
377                                break;
378                        case Number:
379                                v = new JsonPrimitive(new BigDecimal(itemValue));
380                                obj.add(itemName, v);
381                                map.put(v, lexer.location.copy());
382                                break;
383                        case Null:
384                                JsonNull n = new JsonNull();
385                                obj.add(itemName, n);
386                                map.put(n, lexer.location.copy());
387                                break;
388                        case Array:
389                                JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName);
390                                loc = lexer.location.copy();
391                                obj.add(itemName, arr);
392                                next();
393                                readArray(arr, false);
394                                map.put(arr, loc);
395                                break;
396                        case Eof : 
397                                throw lexer.error("Unexpected End of File");
398                        }
399                        next();
400                }
401        }
402
403        private void readArray(JsonArray arr, boolean root) throws FHIRException {
404          while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) {
405            switch (itemType) {
406            case Object:
407                JsonObject obj  = new JsonObject(); // (arr.path+'['+inttostr(i)+']');
408                                LocationData loc = lexer.location.copy();
409                arr.add(obj);
410              next();
411              readObject(obj, false);
412                                map.put(obj, loc);
413              break;
414            case String:
415                JsonPrimitive v = new JsonPrimitive(itemValue);
416                                arr.add(v);
417                                map.put(v, lexer.location.copy());
418                                break;
419            case Number:
420                v = new JsonPrimitive(new BigDecimal(itemValue));
421                                arr.add(v);
422                                map.put(v, lexer.location.copy());
423                                break;
424            case Null :
425                JsonNull n = new JsonNull();
426                                arr.add(n);
427                                map.put(n, lexer.location.copy());
428                                break;
429            case Array:
430        JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']');
431                                loc = lexer.location.copy();
432                                arr.add(child);
433        next();
434              readArray(child, false);
435                                map.put(arr, loc);
436        break;
437            case Eof : 
438                throw lexer.error("Unexpected End of File");
439            }
440            next();
441          }
442        }
443
444        private void next() throws FHIRException {
445                switch (itemType) {
446                case Object :
447                        lexer.consume(TokenType.Open);
448                        lexer.states.push(new State(itemName, false));
449                        if (lexer.getType() == TokenType.Close) {
450                                itemType = ItemType.End;
451                                lexer.next();
452                        } else
453                                parseProperty();
454                        break;
455                case Null:
456                case String:
457                case Number: 
458                case End: 
459                case Boolean :
460                        if (itemType == ItemType.End)
461                                lexer.states.pop();
462                        if (lexer.getType() == TokenType.Comma) {
463                                lexer.next();
464                                parseProperty();
465                        } else if (lexer.getType() == TokenType.Close) {
466                                itemType = ItemType.End;
467                                lexer.next();
468                        } else if (lexer.getType() == TokenType.CloseArray) {
469                                itemType = ItemType.End;
470                                lexer.next();
471                        } else if (lexer.getType() == TokenType.Eof) {
472                                itemType = ItemType.Eof;
473                        } else
474                                throw lexer.error("Unexpected JSON syntax");
475                        break;
476                case Array :
477                        lexer.next();
478                        lexer.states.push(new State(itemName+"[]", true));
479                        parseProperty();
480                        break;
481                case Eof :
482                        throw lexer.error("JSON Syntax Error - attempt to read past end of json stream");
483                default:
484                        throw lexer.error("not done yet (a): "+itemType.toString());
485                }
486        }
487
488        private void parseProperty() throws FHIRException {
489                if (!lexer.states.peek().isProp) {
490                        itemName = lexer.consume(TokenType.String);
491                        itemValue = null;
492                        lexer.consume(TokenType.Colon);
493                }
494                switch (lexer.getType()) {
495                case Null :
496                        itemType = ItemType.Null;
497                        itemValue = lexer.value;
498                        lexer.next();
499                        break;
500                case String :
501                        itemType = ItemType.String;
502                        itemValue = lexer.value;
503                        lexer.next();
504                        break;
505                case Boolean :
506                        itemType = ItemType.Boolean;
507                        itemValue = lexer.value;
508                        lexer.next();
509                        break;
510                case Number :
511                        itemType = ItemType.Number;
512                        itemValue = lexer.value;
513                        lexer.next();
514                        break;
515                case Open :
516                        itemType = ItemType.Object;
517                        break;
518                case OpenArray :
519                        itemType = ItemType.Array;
520                        break;
521                case CloseArray :
522                        itemType = ItemType.End;
523                        break;
524                        // case Close, , case Colon, case Comma, case OpenArray,       !
525                default:
526                        throw lexer.error("not done yet (b): "+lexer.getType().toString());
527                }
528        }
529}