001package org.hl7.fhir.dstu3.utils;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu3
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 org.hl7.fhir.dstu3.model.ExpressionNode;
025import org.hl7.fhir.dstu3.model.ExpressionNode.SourceLocation;
026import org.hl7.fhir.exceptions.FHIRException;
027import org.hl7.fhir.utilities.Utilities;
028
029// shared lexer for concrete syntaxes 
030// - FluentPath
031// - Mapping language
032
033public class FHIRLexer {
034  public class FHIRLexerException extends FHIRException {
035
036    public FHIRLexerException() {
037      super();
038    }
039
040    public FHIRLexerException(String message, Throwable cause) {
041      super(message, cause);
042    }
043
044    public FHIRLexerException(String message) {
045      super(message);
046    }
047
048    public FHIRLexerException(Throwable cause) {
049      super(cause);
050    }
051
052  }
053  private String source;
054  private int cursor;
055  private int currentStart;
056  private String current;
057  private SourceLocation currentLocation;
058  private SourceLocation currentStartLocation;
059  private int id;
060
061  public FHIRLexer(String source) throws FHIRLexerException {
062    this.source = source;
063    currentLocation = new SourceLocation(1, 1);
064    next();
065  }
066  public String getCurrent() {
067    return current;
068  }
069  public SourceLocation getCurrentLocation() {
070    return currentLocation;
071  }
072
073  public boolean isConstant(boolean incDoubleQuotes) {
074    return current.charAt(0) == '\'' || (incDoubleQuotes && current.charAt(0) == '"') || current.charAt(0) == '@' || current.charAt(0) == '%' || 
075        current.charAt(0) == '-' || current.charAt(0) == '+' || (current.charAt(0) >= '0' && current.charAt(0) <= '9') || 
076        current.equals("true") || current.equals("false") || current.equals("{}");
077  }
078
079  public boolean isStringConstant() {
080    return current.charAt(0) == '\'' || current.charAt(0) == '"';
081  }
082
083  public String take() throws FHIRLexerException {
084    String s = current;
085    next();
086    return s;
087  }
088
089  public int takeInt() throws FHIRLexerException {
090    String s = current;
091    if (!Utilities.isInteger(s))
092      throw error("Found "+current+" expecting an integer");
093    next();
094    return Integer.parseInt(s);
095  }
096
097  public boolean isToken() {
098    if (Utilities.noString(current))
099      return false;
100
101    if (current.startsWith("$"))
102      return true;
103
104    if (current.equals("*") || current.equals("**"))
105      return true;
106
107    if ((current.charAt(0) >= 'A' && current.charAt(0) <= 'Z') || (current.charAt(0) >= 'a' && current.charAt(0) <= 'z')) {
108      for (int i = 1; i < current.length(); i++) 
109        if (!( (current.charAt(1) >= 'A' && current.charAt(1) <= 'Z') || (current.charAt(1) >= 'a' && current.charAt(1) <= 'z') ||
110            (current.charAt(1) >= '0' && current.charAt(1) <= '9')))
111          return false;
112      return true;
113    }
114    return false;
115  }
116
117  public FHIRLexerException error(String msg) {
118    return error(msg, currentLocation.toString());
119  }
120
121  public FHIRLexerException error(String msg, String location) {
122    return new FHIRLexerException("Error at "+location+": "+msg);
123  }
124
125  public void next() throws FHIRLexerException {
126    current = null;
127    boolean last13 = false;
128    while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) {
129      if (source.charAt(cursor) == '\r') {
130        currentLocation.setLine(currentLocation.getLine() + 1);
131        currentLocation.setColumn(1);
132        last13 = true;
133      } else if (!last13 && (source.charAt(cursor) == '\n')) {
134        currentLocation.setLine(currentLocation.getLine() + 1);
135        currentLocation.setColumn(1);
136        last13 = false;
137      } else {
138        last13 = false;
139        currentLocation.setColumn(currentLocation.getColumn() + 1);
140      }
141      cursor++;
142    }
143    currentStart = cursor;
144    currentStartLocation = currentLocation;
145    if (cursor < source.length()) {
146      char ch = source.charAt(cursor);
147      if (ch == '!' || ch == '>' || ch == '<' || ch == ':' || ch == '-' || ch == '=')  {
148        cursor++;
149        if (cursor < source.length() && (source.charAt(cursor) == '=' || source.charAt(cursor) == '~' || source.charAt(cursor) == '-')) 
150          cursor++;
151        current = source.substring(currentStart, cursor);
152      } else if (ch == '.' ) {
153        cursor++;
154        if (cursor < source.length() && (source.charAt(cursor) == '.')) 
155          cursor++;
156        current = source.substring(currentStart, cursor);
157      } else if (ch >= '0' && ch <= '9') {
158          cursor++;
159        boolean dotted = false;
160        while (cursor < source.length() && ((source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || (source.charAt(cursor) == '.') && !dotted)) {
161          if (source.charAt(cursor) == '.')
162            dotted = true;
163          cursor++;
164        }
165        if (source.charAt(cursor-1) == '.')
166          cursor--;
167        current = source.substring(currentStart, cursor);
168      }  else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
169        while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') || 
170            (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == '_')) 
171          cursor++;
172        current = source.substring(currentStart, cursor);
173      } else if (ch == '%') {
174        cursor++;
175        if (cursor < source.length() && (source.charAt(cursor) == '"')) {
176          cursor++;
177          while (cursor < source.length() && (source.charAt(cursor) != '"'))
178            cursor++;
179          cursor++;
180        } else
181        while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') || 
182            (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == ':' || source.charAt(cursor) == '-'))
183          cursor++;
184        current = source.substring(currentStart, cursor);
185      } else if (ch == '/') {
186        cursor++;
187        if (cursor < source.length() && (source.charAt(cursor) == '/')) {
188          cursor++;
189          while (cursor < source.length() && !((source.charAt(cursor) == '\r') || source.charAt(cursor) == '\n')) 
190            cursor++;
191        }
192        current = source.substring(currentStart, cursor);
193      } else if (ch == '$') {
194        cursor++;
195        while (cursor < source.length() && (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z'))
196          cursor++;
197        current = source.substring(currentStart, cursor);
198      } else if (ch == '{') {
199        cursor++;
200        ch = source.charAt(cursor);
201        if (ch == '}')
202          cursor++;
203        current = source.substring(currentStart, cursor);
204      } else if (ch == '"'){
205        cursor++;
206        boolean escape = false;
207        while (cursor < source.length() && (escape || source.charAt(cursor) != '"')) {
208          if (escape)
209            escape = false;
210          else 
211            escape = (source.charAt(cursor) == '\\');
212          cursor++;
213        }
214        if (cursor == source.length())
215          throw error("Unterminated string");
216        cursor++;
217        current = "\""+source.substring(currentStart+1, cursor-1)+"\"";
218      } else if (ch == '\''){
219        cursor++;
220        char ech = ch;
221        boolean escape = false;
222        while (cursor < source.length() && (escape || source.charAt(cursor) != ech)) {
223          if (escape)
224            escape = false;
225          else 
226            escape = (source.charAt(cursor) == '\\');
227          cursor++;
228        }
229        if (cursor == source.length())
230          throw error("Unterminated string");
231        cursor++;
232        current = source.substring(currentStart, cursor);
233        if (ech == '\'')
234          current = "\'"+current.substring(1, current.length() - 1)+"\'";
235      } else if (ch == '@'){
236        cursor++;
237        while (cursor < source.length() && isDateChar(source.charAt(cursor)))
238          cursor++;          
239        current = source.substring(currentStart, cursor);
240      } else { // if CharInSet(ch, ['.', ',', '(', ')', '=', '$']) then
241        cursor++;
242        current = source.substring(currentStart, cursor);
243      }
244    }
245  }
246
247
248  private boolean isDateChar(char ch) {
249    return ch == '-' || ch == ':' || ch == 'T' || ch == '+' || ch == 'Z' || Character.isDigit(ch);
250  }
251  public boolean isOp() {
252    return ExpressionNode.Operation.fromCode(current) != null;
253  }
254  public boolean done() {
255    return currentStart >= source.length();
256  }
257  public int nextId() {
258    id++;
259    return id;
260  }
261  public SourceLocation getCurrentStartLocation() {
262    return currentStartLocation;
263  }
264  
265  // special case use
266  public void setCurrent(String current) {
267    this.current = current;
268  }
269
270  public boolean hasComment() {
271    return !done() && current.startsWith("//");
272  }
273  public boolean hasToken(String kw) {
274    return !done() && kw.equals(current);
275  }
276  public boolean hasToken(String... names) {
277    if (done()) 
278      return false;
279    for (String s : names)
280      if (s.equals(current))
281        return true;
282    return false;
283  }
284  
285  public void token(String kw) throws FHIRLexerException {
286    if (!kw.equals(current)) 
287      throw error("Found \""+current+"\" expecting \""+kw+"\"");
288    next();
289  }
290  
291  public String readConstant(String desc) throws FHIRLexerException {
292    if (!isStringConstant())
293      throw error("Found "+current+" expecting \"["+desc+"]\"");
294
295    return processConstant(take());
296  }
297
298  public String processConstant(String s) throws FHIRLexerException {
299    StringBuilder b = new StringBuilder();
300    int i = 1;
301    while (i < s.length()-1) {
302      char ch = s.charAt(i);
303      if (ch == '\\') {
304        i++;
305        switch (s.charAt(i)) {
306        case 't': 
307          b.append('\t');
308          break;
309        case 'r':
310          b.append('\r');
311          break;
312        case 'n': 
313          b.append('\n');
314          break;
315        case 'f': 
316          b.append('\f');
317          break;
318        case '\'':
319          b.append('\'');
320          break;
321        case '\\': 
322          b.append('\\');
323          break;
324        case '/': 
325          b.append('\\');
326          break;
327        case 'u':
328          i++;
329          int uc = Integer.parseInt(s.substring(i, i+4), 16);
330          b.append((char) uc);
331          i = i + 4;
332          break;
333        default:
334          throw new FHIRLexerException("Unknown character escape \\"+s.charAt(i));
335        }
336      } else {
337        b.append(ch);
338        i++;
339      }
340    }
341    return b.toString();
342
343  }
344  public void skipToken(String token) throws FHIRLexerException {
345    if (getCurrent().equals(token))
346      next();
347    
348  }
349  public String takeDottedToken() throws FHIRLexerException {
350    StringBuilder b = new StringBuilder();
351    b.append(take());
352    while (!done() && getCurrent().equals(".")) {
353      b.append(take());
354      b.append(take());
355    }
356    return b.toString();
357  }
358  
359  void skipComments() throws FHIRLexerException {
360    while (!done() && hasComment())
361      next();
362  }
363
364}