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