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