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