001package org.hl7.fhir.utilities.graphql;
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.io.FileNotFoundException;
035import java.io.IOException;
036import java.io.InputStream;
037import java.io.InputStreamReader;
038import java.io.Reader;
039import java.io.StringReader;
040import java.util.Map.Entry;
041
042import org.hl7.fhir.utilities.TextFile;
043import org.hl7.fhir.utilities.Utilities;
044import org.hl7.fhir.utilities.graphql.Argument.ArgumentListStatus;
045
046import com.google.gson.JsonElement;
047import com.google.gson.JsonObject;
048
049public class Parser {  
050  public static Package parse(String source) throws IOException, EGraphQLException, EGraphEngine {
051    Parser self = new Parser();
052    self.reader = new StringReader(source);
053    self.next();
054    Document doc = self.parseDocument();
055    return new Package(doc);
056  }
057
058  public static Package parse(InputStream source) throws IOException, EGraphQLException, EGraphEngine {
059    Parser self = new Parser();
060    self.reader = new InputStreamReader(source);
061    self.next();
062    Document doc = self.parseDocument();
063    return new Package(doc);
064  }
065
066  public static Package parseFile(String filename) throws FileNotFoundException, IOException, EGraphQLException, EGraphEngine {
067    String src = TextFile.fileToString(filename);
068    return parse(src);
069  }
070
071  public static Package parseJson(InputStream source) throws EGraphQLException, IOException, EGraphEngine {
072    JsonObject json = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.streamToString(source));
073    Parser self = new Parser();
074    self.reader = new StringReader(json.get("query").getAsString());
075    self.next();
076    Package result = new Package(self.parseDocument());
077    result.setOperationName(json.get("operationName").getAsString());
078    if (json.has("variables")) {
079      JsonObject vl = json.getAsJsonObject("variables");
080      for (Entry<String, JsonElement> n : vl.entrySet())
081        result.getVariables().add(new Argument(n.getKey(), n.getValue()));
082    }
083    return result;
084  }
085
086  enum LexType {gqlltNull, gqlltName, gqlltPunctuation, gqlltString, gqlltNumber}
087
088  static class SourceLocation {
089    int line;
090    int col;
091  }
092  
093  private Reader reader;
094  private StringBuilder token;
095  private String peek;
096  private LexType lexType;
097  private SourceLocation location = new SourceLocation();
098  boolean readerDone = false;
099
100  private char getNextChar() throws IOException {
101    char result = '\0';
102    if (peek != null) {
103      result = peek.charAt(0);
104      peek = peek.length() == 1 ? null : peek.substring(1);
105    } else if (reader.ready()) {
106      int c = reader.read();
107      if (c > -1) {
108        result = (char) c;
109        if (result == '\n') {
110          location.line++;
111          location.col = 1;
112        } else
113          location.col++;
114      }
115    }
116    readerDone = result == '\0';
117    return result;
118  }
119
120  private void pushChar(char ch) {
121    if (ch != '\0')
122      if (peek == null)
123        peek = String.valueOf(ch);
124      else
125        peek = String.valueOf(ch)+peek;
126  }
127
128  private void skipIgnore() throws IOException{
129    char ch = getNextChar();
130    while (Character.isWhitespace(ch) || (ch == ',')) 
131      ch = getNextChar();
132    if (ch == '#') {
133      while (ch != '\r' && ch != '\n')
134        ch = getNextChar();
135      pushChar(ch);
136      skipIgnore();
137    } else
138      pushChar(ch);
139  }
140
141  private void next() throws IOException, EGraphQLException {
142    //  var
143    //    ch : Char;
144    //    hex : String;
145    skipIgnore();
146    token = new StringBuilder();
147    if (readerDone && peek == null)
148      lexType = LexType.gqlltNull;
149    else {
150      char ch = getNextChar();
151      if (Utilities.existsInList(ch, '!', '$', '(', ')', ':', '=', '@', '[', ']', '{', '|', '}')) {
152        lexType = LexType.gqlltPunctuation;
153        token.append(ch);
154      } else if (ch == '.') {
155        do {
156          token.append(ch);
157          ch = getNextChar();
158        } while (ch == '.');
159        pushChar(ch);
160        if ((token.length() != 3))
161          throw new EGraphQLException("Found \""+token.toString()+"\" expecting \"...\"");
162      } else if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '_')) {
163        lexType = LexType.gqlltName;
164        do {
165          token.append(ch);
166          ch = getNextChar();
167        } while ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '_'));
168        pushChar(ch);
169      } else if ((ch >= '0' && ch <= '9') || (ch == '-')) {
170        lexType = LexType.gqlltNumber;
171        do {
172          token.append(ch);
173          ch = getNextChar();
174        } while ((ch >= '0' && ch <= '9') || ((ch == '.') && token.toString().indexOf('.') == -1)  || ((ch == 'e') && token.toString().indexOf('e') == -1));
175        pushChar(ch);
176      } else if ((ch == '"')) {
177        lexType = LexType.gqlltString;
178        do {
179          ch = getNextChar();
180          if (ch == '\\') {
181            if (!reader.ready())
182              throw new EGraphQLException("premature termination of GraphQL during a string constant");
183            ch = getNextChar();
184            if (ch == '"') token.append('"');
185            else if (ch == '\\') token.append('\'');
186            else if (ch == '/') token.append('/');
187            else if (ch == 'n') token.append('\n');
188            else if (ch == 'r') token.append('\r');
189            else if (ch == 't') token.append('\t');
190            else if (ch == 'u') {
191              String hex = String.valueOf(getNextChar()) + getNextChar() + getNextChar() + getNextChar();
192              token.append((char) Integer.parseInt(hex, 16));
193            } else
194              throw new EGraphQLException("Unexpected character: \""+ch+"\"");
195            ch = '\0';
196          }  else if (ch != '"') 
197            token.append(ch);
198        } while (!(readerDone || ch == '"'));
199        if (ch != '"')
200          throw new EGraphQLException("premature termination of GraphQL during a string constant");
201      } else
202        throw new EGraphQLException("Unexpected character \""+ch+"\"");
203    }
204  }
205
206  private boolean hasPunctuation(String punc) {
207    return lexType == LexType.gqlltPunctuation && token.toString().equals(punc);
208  }
209
210  private void consumePunctuation(String punc) throws EGraphQLException, IOException {
211    if (lexType != LexType.gqlltPunctuation)
212      throw new EGraphQLException("Found \""+token.toString()+"\" expecting \""+punc+"\"");
213    if (!token.toString().equals(punc))
214      throw new EGraphQLException("Found \""+token.toString()+"\" expecting \""+punc+"\"");
215    next();
216  }
217
218  private boolean hasName() {
219    return (lexType == LexType.gqlltName) && (token.toString().length() > 0);
220  }
221
222  private boolean hasName(String name) {
223    return (lexType == LexType.gqlltName) && (token.toString().equals(name));
224  }
225
226  private String consumeName() throws EGraphQLException, IOException {
227    if (lexType != LexType.gqlltName)
228      throw new EGraphQLException("Found \""+token.toString()+"\" expecting a name");
229    String result = token.toString();
230    next();
231    return result;
232  }
233
234  private void consumeName(String name) throws EGraphQLException, IOException{
235    if (lexType != LexType.gqlltName)
236      throw new EGraphQLException("Found \""+token.toString()+"\" expecting a name");
237    if (!token.toString().equals(name))
238      throw new EGraphQLException("Found \""+token.toString()+"\" expecting \""+name+"\"");
239    next();
240  }
241
242  private Value parseValue() throws EGraphQLException, IOException {
243    Value result = null;
244    switch (lexType) {
245    case gqlltNull: throw new EGraphQLException("Attempt to read a value after reading off the } of the GraphQL statement");
246    case gqlltName: 
247      result = new NameValue(token.toString());
248      break;
249    case gqlltPunctuation:
250      if (hasPunctuation("$")) {
251        consumePunctuation("$");
252        result = new VariableValue(token.toString());
253      } else if (hasPunctuation("{")) {
254        consumePunctuation("{");
255        ObjectValue obj = new ObjectValue();
256        while (!hasPunctuation("}"))
257          obj.getFields().add(parseArgument());
258        result = obj;
259      } else
260        throw new EGraphQLException("Attempt to read a value at \""+token.toString()+"\"");
261      break;
262    case gqlltString: 
263      result = new StringValue(token.toString());
264      break;
265    case gqlltNumber: 
266      result = new NumberValue(token.toString());
267      break;
268    }
269    next();
270    return result;
271  }
272
273  private Argument parseArgument() throws EGraphQLException, IOException {
274    Argument result = new Argument();
275    result.setName(consumeName());
276    consumePunctuation(":");
277    if (hasPunctuation("[")) {
278      result.setListStatus(ArgumentListStatus.REPEATING);
279      consumePunctuation("[");
280      while (!hasPunctuation("]"))
281        result.getValues().add(parseValue());
282      consumePunctuation("]");
283    } else
284      result.getValues().add(parseValue());
285    return result;
286  }
287
288  private Directive parseDirective() throws EGraphQLException, IOException {
289    Directive result = new Directive();
290    consumePunctuation("@");
291    result.setName(consumeName());
292    if (hasPunctuation("(")) {
293      consumePunctuation("(");
294      do { 
295        result.getArguments().add(parseArgument());
296      } while (!hasPunctuation(")"));
297      consumePunctuation(")");
298    }
299    return result;
300  }
301
302  private Document parseDocument() throws EGraphQLException, IOException, EGraphEngine {
303    Document doc = new Document();
304    if (!hasName()) {
305      Operation op = new Operation();
306      parseOperationInner(op);
307      doc.getOperations().add(op);
308
309    } else {
310      while (!readerDone || (peek != null)) {
311        String s = consumeName();
312        if (s.equals("mutation") || (s.equals("query"))) 
313          doc.getOperations().add(parseOperation(s));
314        else if (s.equals("fragment"))
315          doc.getFragments().add(parseFragment());
316        else
317          throw new EGraphEngine("Not done yet"); // doc.Operations.Add(parseOperation(s))?          
318      }
319    }
320    return doc;
321  }
322
323  private Field parseField() throws EGraphQLException, IOException {
324    Field result = new Field();
325    result.setName(consumeName());
326    result.setAlias(result.getName());
327    if (hasPunctuation(":")) {
328      consumePunctuation(":");
329      result.setName(consumeName());
330    }
331    if (hasPunctuation("(")) {
332      consumePunctuation("(");
333      while (!hasPunctuation(")"))
334        result.getArguments().add(parseArgument());
335      consumePunctuation(")");
336    }
337    while (hasPunctuation("@")) 
338      result.getDirectives().add(parseDirective());
339
340    if (hasPunctuation("{")) {
341      consumePunctuation("{");
342      do {
343        result.getSelectionSet().add(parseSelection());
344      } while (!hasPunctuation("}"));
345      consumePunctuation("}");
346    }
347    return result;
348  }
349
350  private void parseFragmentInner(Fragment fragment) throws EGraphQLException, IOException {
351    while (hasPunctuation("@"))
352      fragment.getDirectives().add(parseDirective());
353    consumePunctuation("{");
354    do
355      fragment.getSelectionSet().add(parseSelection());
356    while (!hasPunctuation("}"));
357    consumePunctuation("}");
358  }
359
360  private Fragment parseFragment() throws EGraphQLException, IOException {
361    Fragment result = new Fragment();
362    result.setName(consumeName());
363    consumeName("on");
364    result.setTypeCondition(consumeName());
365    parseFragmentInner(result);
366    return result;
367  }
368
369  private FragmentSpread parseFragmentSpread() throws EGraphQLException, IOException {
370    FragmentSpread result = new FragmentSpread();
371    result.setName(consumeName());
372    while (hasPunctuation("@"))
373      result.getDirectives().add(parseDirective());
374    return result;
375  }
376
377  private Fragment parseInlineFragment() throws EGraphQLException, IOException {
378    Fragment result = new Fragment();
379    if (hasName("on"))
380    {
381      consumeName("on");
382      result.setTypeCondition(consumeName());
383    }
384    parseFragmentInner(result);
385    return result;
386  }
387
388  private Operation parseOperation(String name) throws EGraphQLException, IOException {
389    Operation result = new Operation();
390    if (name.equals("mutation")) {
391      result.setOperationType(Operation.OperationType.qglotMutation);
392      if (hasName())
393        result.setName(consumeName());
394    } else if (name.equals("query")) {
395      result.setOperationType(Operation.OperationType.qglotQuery);
396      if (hasName())
397        result.setName(consumeName());
398    }  else
399      result.setName(name);
400    parseOperationInner(result);
401    return result;
402  }
403
404  private void parseOperationInner(Operation op) throws EGraphQLException, IOException {
405    if (hasPunctuation("(")) {
406      consumePunctuation("(");
407      do 
408        op.getVariables().add(parseVariable());
409      while (!hasPunctuation(")"));
410      consumePunctuation(")");
411    }
412    while (hasPunctuation("@"))
413      op.getDirectives().add(parseDirective());
414    if (hasPunctuation("{")) {
415      consumePunctuation("{");
416      do
417        op.getSelectionSet().add(parseSelection());
418      while (!hasPunctuation("}"));
419      consumePunctuation("}");
420    }
421  }
422
423  private Selection parseSelection() throws EGraphQLException, IOException {
424    Selection result = new Selection();
425    if (hasPunctuation("...")) {
426      consumePunctuation("...");
427      if (hasName() && !token.toString().equals("on")) 
428        result.setFragmentSpread(parseFragmentSpread());
429      else
430        result.setInlineFragment(parseInlineFragment());
431    } else
432      result.setField(parseField());
433    return result;
434  }
435
436  private Variable parseVariable() throws EGraphQLException, IOException {
437    Variable result = new Variable();
438    consumePunctuation("$");
439    result.setName(consumeName());
440    consumePunctuation(":");
441    result.setTypeName(consumeName());
442    if (hasPunctuation("="))
443    {
444      consumePunctuation("=");
445      result.setDefaultValue(parseValue());
446    }
447    return result;
448  }
449
450}