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}