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}