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