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