001package org.hl7.fhir.r4.utils;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import org.hl7.fhir.exceptions.FHIRException;
007import org.hl7.fhir.utilities.Utilities;
008
009public class SnomedExpressions {
010
011  public class Base {
012    private int stop;
013    private int start;
014    public int getStop() {
015      return stop;
016    }
017    public void setStop(int stop) {
018      this.stop = stop;
019    }
020    public int getStart() {
021      return start;
022    }
023    public void setStart(int start) {
024      this.start = start;
025    }
026  }
027
028  public class Concept extends Base {
029    private long reference;
030    private String code;
031    private String description;
032    private String literal;
033    private String decimal;
034    public long getReference() {
035      return reference;
036    }
037    public void setReference(long reference) {
038      this.reference = reference;
039    }
040    public String getCode() {
041      return code;
042    }
043    public void setCode(String code) {
044      this.code = code;
045    }
046    public String getDescription() {
047      return description;
048    }
049    public void setDescription(String description) {
050      this.description = description;
051    }
052    public String getLiteral() {
053      return literal;
054    }
055    public void setLiteral(String literal) {
056      this.literal = literal;
057    }
058    public String getDecimal() {
059      return decimal;
060    }
061    public void setDecimal(String decimal) {
062      this.decimal = decimal;
063    }
064    @Override
065    public String toString() {
066      if (code != null) 
067      return code;
068    else if (decimal != null) 
069      return "#"+decimal;
070    else if (literal != null)
071      return "\""+literal+"\"";
072    else
073      return "";
074    }
075  }
076
077  public enum ExpressionStatus {
078    Unknown, Equivalent, SubsumedBy;
079  }
080
081  public class Expression extends Base {
082    private List<RefinementGroup> refinementGroups = new ArrayList<RefinementGroup>();
083    private List<Refinement> refinements = new ArrayList<Refinement>();
084    private List<Concept> concepts = new ArrayList<Concept>();
085    private ExpressionStatus status;
086    public ExpressionStatus getStatus() {
087      return status;
088    }
089    public void setStatus(ExpressionStatus status) {
090      this.status = status;
091    }
092    public List<RefinementGroup> getRefinementGroups() {
093      return refinementGroups;
094    }
095    public List<Refinement> getRefinements() {
096      return refinements;
097    }
098    public List<Concept> getConcepts() {
099      return concepts;
100    }
101    @Override
102    public String toString() {
103      StringBuilder b = new StringBuilder();
104      if (status == ExpressionStatus.Equivalent)
105        b.append("===");
106      else if (status == ExpressionStatus.SubsumedBy)
107        b.append("<<<");
108      boolean first = true;
109      for (Concept concept : concepts) {
110        if (first) first = false; else b.append(',');
111        b.append(concept.toString());
112      }
113      for (Refinement refinement : refinements) {
114        if (first) first = false; else b.append(',');
115        b.append(refinement.toString());
116      }
117      for (RefinementGroup refinementGroup : refinementGroups) {
118        if (first) first = false; else b.append(',');
119        b.append(refinementGroup.toString());
120      }
121      return b.toString();
122    }
123  }
124
125  public class Refinement extends Base {
126    private Concept name;
127    private Expression value;
128    public Concept getName() {
129      return name;
130    }
131    public void setName(Concept name) {
132      this.name = name;
133    }
134    public Expression getValue() {
135      return value;
136    }
137    public void setValue(Expression value) {
138      this.value = value;
139    }
140
141    @Override
142    public String toString() {
143      return name.toString()+"="+value.toString();
144    }
145  }
146
147  public class RefinementGroup extends Base {
148    private List<Refinement> refinements = new ArrayList<Refinement>();
149
150    public List<Refinement> getRefinements() {
151      return refinements;
152    }
153
154    @Override
155    public String toString() {
156      StringBuilder b = new StringBuilder();
157      boolean first = true;
158      for (Refinement refinement : refinements) {
159        if (first) first = false; else b.append(',');
160        b.append(refinement.toString());
161      }
162      return b.toString();
163    }
164  }
165
166  private static final int MAX_TERM_LIMIT = 1024;
167
168    private String source;
169    private int cursor;
170
171    private Concept concept() throws FHIRException {
172      Concept res = new Concept();
173      res.setStart(cursor);
174      ws();
175      if (peek() == '#')
176        res.decimal = decimal();
177      else if (peek() == '"') 
178        res.literal = stringConstant();
179      else
180        res.code = conceptId();
181      ws();
182      if (gchar('|')) {
183        ws();
184        res.description = term().trim();
185        ws();
186        fixed('|');
187        ws();
188      }
189      res.setStop(cursor);
190      return res;
191    }
192
193    private void refinements(Expression expr) throws FHIRException {
194      boolean n = true;
195      while (n) {
196        if (peek() != '{')
197          expr.refinements.add(attribute());
198        else
199          expr.refinementGroups.add(attributeGroup());
200        ws();
201        n = gchar(',');
202        ws();
203      }
204    }
205
206    private RefinementGroup attributeGroup() throws FHIRException {
207      RefinementGroup res = new RefinementGroup();
208      fixed('{');
209      ws();
210      res.setStart(cursor);
211      res.refinements.add(attribute());
212      while (gchar(','))
213        res.refinements.add(attribute());
214      res.setStop(cursor);
215      ws();
216      fixed('}');
217      ws();
218      return res;
219    }
220
221    private Refinement attribute() throws FHIRException {
222      Refinement res = new Refinement();
223      res.setStart(cursor);
224      res.name = attributeName();
225      fixed('=');
226      res.value = attributeValue();
227      ws();
228      res.setStop(cursor);
229      return res;
230    }
231
232    private Concept attributeName() throws FHIRException {
233      Concept res = new Concept();
234      res.setStart(cursor);
235      ws();
236      res.code = conceptId();
237      ws();
238      if (gchar('|')) {
239        ws();
240        res.description = term();
241        ws();
242        fixed('|');
243        ws();
244      }
245      res.setStop(cursor);
246      return res;
247    }
248
249    private Expression attributeValue() throws FHIRException {
250      Expression res;
251      ws();
252      if (gchar('(')) {
253        res = expression();
254        fixed(')');
255      } else {
256        res = expression();
257      }
258      return res;
259    }
260
261    private Expression expression() throws FHIRException {
262      Expression res = new Expression();
263      res.setStart(cursor);
264      ws();
265      res.concepts.add(concept());
266      while (gchar('+'))
267        res.concepts.add(concept());
268      if (gchar(':')) {
269        ws();
270        refinements(res);
271      }
272      res.setStop(cursor);
273      return res;
274    }
275
276    private String conceptId() throws FHIRException {
277      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', 18));
278      int i = 0;
279      while (peek() >= '0' && peek() <= '9') {
280        res.setCharAt(i, next());
281        i++;
282      }
283      rule(i > 0, "Concept not found (next char = \""+peekDisp()+"\")");
284      return res.substring(0, i);
285    }
286
287    private String decimal() throws FHIRException {
288      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
289      int i = 0;
290      fixed('#');
291      while ((peek() >= '0' && peek() <= '9') || peek() == '.') {
292        res.setCharAt(i, next());
293        i++;
294      }
295      return res.substring(0, i);
296    }
297
298    private String term() {
299      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
300      int i = 0;
301      while (peek() != '|') {
302        res.setCharAt(i, next());
303        i++;
304      }
305      return res.substring(0, i);
306    }
307
308    private void ws() {
309      while (Utilities.existsInList(peek(), ' ', '\t', '\r', 'n'))
310        next();
311    }
312
313    private boolean gchar(char  ch) {
314      boolean result = peek() == ch;
315      if (result)
316        next();
317      return result;
318    }
319
320    private void fixed(char ch) throws FHIRException {
321      boolean b = gchar(ch);
322      rule(b, "Expected character \""+ch+"\" but found "+peek());
323      ws();
324    }
325
326    private Expression parse() throws FHIRException {
327      Expression res = new Expression();
328      res.setStart(cursor);
329      ws();
330      if (peek() == '=') {
331        res.status = ExpressionStatus.Equivalent;
332        prefix('=');
333      } else if (peek() == '<') {
334        res.status = ExpressionStatus.SubsumedBy;
335        prefix('<');
336      }
337
338      res.concepts.add(concept());
339      while (gchar('+'))
340        res.concepts.add(concept());
341      if (gchar(':')) {
342        ws();
343        refinements(res);
344      }
345      res.setStop(cursor);
346      rule(cursor >= source.length(), "Found content (\""+peekDisp()+"\") after end of expression");
347      return res;
348    }
349    
350    public static Expression parse(String source) throws FHIRException {
351      SnomedExpressions self = new SnomedExpressions();
352      self.source = source;
353      self.cursor = 0;
354      return self.parse();
355    }
356
357    private char peek() {
358      if (cursor >= source.length())
359        return '\0';
360      else
361        return source.charAt(cursor);
362    }
363
364    private String peekDisp() {
365      if (cursor >= source.length()) 
366        return "[n/a: overrun]";
367      else
368        return String.valueOf(source.charAt(cursor));
369    }
370
371    private void prefix(char c) throws FHIRException {
372      fixed(c);
373      fixed(c);
374      fixed(c);
375      ws();
376    }
377
378    private char next() {
379      char res = peek();
380      cursor++;
381      return res;
382    }
383
384    private void rule(boolean test, String message) throws FHIRException {
385      if (!test) 
386        throw new FHIRException(message+" at character "+Integer.toString(cursor));
387    }
388
389    private String stringConstant() throws FHIRException {
390      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
391      fixed('"');
392      int i = 0;
393      while (peek() != '"') {
394        i++;
395        res.setCharAt(i, next());
396      }
397      fixed('"');
398      return res.substring(0, i);
399    }
400
401}