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