001package org.hl7.fhir.r5.utils;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.util.ArrayList;
035import java.util.List;
036
037import org.hl7.fhir.exceptions.FHIRException;
038import org.hl7.fhir.utilities.Utilities;
039
040public class SnomedExpressions {
041
042  public class Base {
043    private int stop;
044    private int start;
045    public int getStop() {
046      return stop;
047    }
048    public void setStop(int stop) {
049      this.stop = stop;
050    }
051    public int getStart() {
052      return start;
053    }
054    public void setStart(int start) {
055      this.start = start;
056    }
057  }
058
059  public class Concept extends Base {
060    private long reference;
061    private String code;
062    private String description;
063    private String literal;
064    private String decimal;
065    public long getReference() {
066      return reference;
067    }
068    public void setReference(long reference) {
069      this.reference = reference;
070    }
071    public String getCode() {
072      return code;
073    }
074    public void setCode(String code) {
075      this.code = code;
076    }
077    public String getDescription() {
078      return description;
079    }
080    public void setDescription(String description) {
081      this.description = description;
082    }
083    public String getLiteral() {
084      return literal;
085    }
086    public void setLiteral(String literal) {
087      this.literal = literal;
088    }
089    public String getDecimal() {
090      return decimal;
091    }
092    public void setDecimal(String decimal) {
093      this.decimal = decimal;
094    }
095    @Override
096    public String toString() {
097      if (code != null) 
098      return code;
099    else if (decimal != null) 
100      return "#"+decimal;
101    else if (literal != null)
102      return "\""+literal+"\"";
103    else
104      return "";
105    }
106  }
107
108  public enum ExpressionStatus {
109    Unknown, Equivalent, SubsumedBy;
110  }
111
112  public class Expression extends Base {
113    private List<RefinementGroup> refinementGroups = new ArrayList<RefinementGroup>();
114    private List<Refinement> refinements = new ArrayList<Refinement>();
115    private List<Concept> concepts = new ArrayList<Concept>();
116    private ExpressionStatus status;
117    public ExpressionStatus getStatus() {
118      return status;
119    }
120    public void setStatus(ExpressionStatus status) {
121      this.status = status;
122    }
123    public List<RefinementGroup> getRefinementGroups() {
124      return refinementGroups;
125    }
126    public List<Refinement> getRefinements() {
127      return refinements;
128    }
129    public List<Concept> getConcepts() {
130      return concepts;
131    }
132    @Override
133    public String toString() {
134      StringBuilder b = new StringBuilder();
135      if (status == ExpressionStatus.Equivalent)
136        b.append("===");
137      else if (status == ExpressionStatus.SubsumedBy)
138        b.append("<<<");
139      boolean first = true;
140      for (Concept concept : concepts) {
141        if (first) first = false; else b.append(',');
142        b.append(concept.toString());
143      }
144      for (Refinement refinement : refinements) {
145        if (first) first = false; else b.append(',');
146        b.append(refinement.toString());
147      }
148      for (RefinementGroup refinementGroup : refinementGroups) {
149        if (first) first = false; else b.append(',');
150        b.append(refinementGroup.toString());
151      }
152      return b.toString();
153    }
154  }
155
156  public class Refinement extends Base {
157    private Concept name;
158    private Expression value;
159    public Concept getName() {
160      return name;
161    }
162    public void setName(Concept name) {
163      this.name = name;
164    }
165    public Expression getValue() {
166      return value;
167    }
168    public void setValue(Expression value) {
169      this.value = value;
170    }
171
172    @Override
173    public String toString() {
174      return name.toString()+"="+value.toString();
175    }
176  }
177
178  public class RefinementGroup extends Base {
179    private List<Refinement> refinements = new ArrayList<Refinement>();
180
181    public List<Refinement> getRefinements() {
182      return refinements;
183    }
184
185    @Override
186    public String toString() {
187      StringBuilder b = new StringBuilder();
188      boolean first = true;
189      for (Refinement refinement : refinements) {
190        if (first) first = false; else b.append(',');
191        b.append(refinement.toString());
192      }
193      return b.toString();
194    }
195  }
196
197  private static final int MAX_TERM_LIMIT = 1024;
198
199    private String source;
200    private int cursor;
201
202    private Concept concept() throws FHIRException {
203      Concept res = new Concept();
204      res.setStart(cursor);
205      ws();
206      if (peek() == '#')
207        res.decimal = decimal();
208      else if (peek() == '"') 
209        res.literal = stringConstant();
210      else
211        res.code = conceptId();
212      ws();
213      if (gchar('|')) {
214        ws();
215        res.description = term().trim();
216        ws();
217        fixed('|');
218        ws();
219      }
220      res.setStop(cursor);
221      return res;
222    }
223
224    private void refinements(Expression expr) throws FHIRException {
225      boolean n = true;
226      while (n) {
227        if (peek() != '{')
228          expr.refinements.add(attribute());
229        else
230          expr.refinementGroups.add(attributeGroup());
231        ws();
232        n = gchar(',');
233        ws();
234      }
235    }
236
237    private RefinementGroup attributeGroup() throws FHIRException {
238      RefinementGroup res = new RefinementGroup();
239      fixed('{');
240      ws();
241      res.setStart(cursor);
242      res.refinements.add(attribute());
243      while (gchar(','))
244        res.refinements.add(attribute());
245      res.setStop(cursor);
246      ws();
247      fixed('}');
248      ws();
249      return res;
250    }
251
252    private Refinement attribute() throws FHIRException {
253      Refinement res = new Refinement();
254      res.setStart(cursor);
255      res.name = attributeName();
256      fixed('=');
257      res.value = attributeValue();
258      ws();
259      res.setStop(cursor);
260      return res;
261    }
262
263    private Concept attributeName() throws FHIRException {
264      Concept res = new Concept();
265      res.setStart(cursor);
266      ws();
267      res.code = conceptId();
268      ws();
269      if (gchar('|')) {
270        ws();
271        res.description = term();
272        ws();
273        fixed('|');
274        ws();
275      }
276      res.setStop(cursor);
277      return res;
278    }
279
280    private Expression attributeValue() throws FHIRException {
281      Expression res;
282      ws();
283      if (gchar('(')) {
284        res = expression();
285        fixed(')');
286      } else {
287        res = expression();
288      }
289      return res;
290    }
291
292    private Expression expression() throws FHIRException {
293      Expression res = new Expression();
294      res.setStart(cursor);
295      ws();
296      res.concepts.add(concept());
297      while (gchar('+'))
298        res.concepts.add(concept());
299      if (gchar(':')) {
300        ws();
301        refinements(res);
302      }
303      res.setStop(cursor);
304      return res;
305    }
306
307    private String conceptId() throws FHIRException {
308      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', 18));
309      int i = 0;
310      while (peek() >= '0' && peek() <= '9') {
311        res.setCharAt(i, next());
312        i++;
313      }
314      rule(i > 0, "Concept not found (next char = \""+peekDisp()+"\", in '"+source+"')");
315      return res.substring(0, i);
316    }
317
318    private String decimal() throws FHIRException {
319      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
320      int i = 0;
321      fixed('#');
322      while ((peek() >= '0' && peek() <= '9') || peek() == '.') {
323        res.setCharAt(i, next());
324        i++;
325      }
326      return res.substring(0, i);
327    }
328
329    private String term() {
330      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
331      int i = 0;
332      while (peek() != '|') {
333        res.setCharAt(i, next());
334        i++;
335      }
336      return res.substring(0, i);
337    }
338
339    private void ws() {
340      while (Utilities.existsInList(peek(), ' ', '\t', '\r', 'n'))
341        next();
342    }
343
344    private boolean gchar(char  ch) {
345      boolean result = peek() == ch;
346      if (result)
347        next();
348      return result;
349    }
350
351    private void fixed(char ch) throws FHIRException {
352      boolean b = gchar(ch);
353      rule(b, "Expected character \""+ch+"\" but found "+peek());
354      ws();
355    }
356
357    private Expression parse() throws FHIRException {
358      Expression res = new Expression();
359      res.setStart(cursor);
360      ws();
361      if (peek() == '=') {
362        res.status = ExpressionStatus.Equivalent;
363        prefix('=');
364      } else if (peek() == '<') {
365        res.status = ExpressionStatus.SubsumedBy;
366        prefix('<');
367      }
368
369      res.concepts.add(concept());
370      while (gchar('+'))
371        res.concepts.add(concept());
372      if (gchar(':')) {
373        ws();
374        refinements(res);
375      }
376      res.setStop(cursor);
377      rule(cursor >= source.length(), "Found content (\""+peekDisp()+"\") after end of expression");
378      return res;
379    }
380    
381    public static Expression parse(String source) throws FHIRException {
382      SnomedExpressions self = new SnomedExpressions();
383      self.source = source;
384      self.cursor = 0;
385      return self.parse();
386    }
387
388    private char peek() {
389      if (cursor >= source.length())
390        return '\0';
391      else
392        return source.charAt(cursor);
393    }
394
395    private String peekDisp() {
396      if (cursor >= source.length()) 
397        return "[n/a: overrun]";
398      else
399        return String.valueOf(source.charAt(cursor));
400    }
401
402    private void prefix(char c) throws FHIRException {
403      fixed(c);
404      fixed(c);
405      fixed(c);
406      ws();
407    }
408
409    private char next() {
410      char res = peek();
411      cursor++;
412      return res;
413    }
414
415    private void rule(boolean test, String message) throws FHIRException {
416      if (!test) 
417        throw new FHIRException(message+" at character "+Integer.toString(cursor));
418    }
419
420    private String stringConstant() throws FHIRException {
421      StringBuffer res = new StringBuffer(Utilities.padLeft("", ' ', MAX_TERM_LIMIT));
422      fixed('"');
423      int i = 0;
424      while (peek() != '"') {
425        i++;
426        res.setCharAt(i, next());
427      }
428      fixed('"');
429      return res.substring(0, i);
430    }
431
432}