
001package org.hl7.fhir.r5.terminologies.utilities; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.List; 006 007import org.hl7.fhir.r5.formats.JsonParser; 008import org.hl7.fhir.r5.model.CodeType; 009import org.hl7.fhir.r5.model.Enumerations; 010import org.hl7.fhir.r5.model.ValueSet; 011import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 012import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 013import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; 014 015public class VCLParser { 016 017 public static class VCLParseException extends Exception { 018 public VCLParseException(String message) { 019 super(message); 020 } 021 022 public VCLParseException(String message, int position) { 023 super(message + " at position " + position); 024 } 025 } 026 027 private static String VCL_URI = "http://fhir.org/VCL?v1="; 028 029 private enum TokenType { 030 DASH, OPEN, CLOSE, LCRLY, RCRLY, SEMI, COMMA, DOT, STAR, 031 EQ, IS_A, IS_NOT_A, DESC_OF, REGEX, IN, NOT_IN, 032 GENERALIZES, CHILD_OF, DESC_LEAF, EXISTS, 033 URI, SCODE, QUOTED_VALUE, EOF 034 } 035 036 private static class Token { 037 TokenType type; 038 String value; 039 int position; 040 041 Token(TokenType type, String value, int position) { 042 this.type = type; 043 this.value = value; 044 this.position = position; 045 } 046 047 @Override 048 public String toString() { 049 return type + "(" + value + ")"; 050 } 051 } 052 053 private static class Lexer { 054 private String input; 055 private int pos = 0; 056 057 public Lexer(String input) { 058 this.input = input.trim(); 059 } 060 061 public List<Token> tokenize() throws VCLParseException { 062 List<Token> tokens = new ArrayList<>(); 063 064 while (pos < input.length()) { 065 skipWhitespace(); 066 if (pos >= input.length()) break; 067 068 int startPos = pos; 069 char ch = input.charAt(pos); 070 071 switch (ch) { 072 case '-': tokens.add(new Token(TokenType.DASH, "-", startPos)); pos++; continue; 073 case '(': tokens.add(new Token(TokenType.OPEN, "(", startPos)); pos++; continue; 074 case ')': tokens.add(new Token(TokenType.CLOSE, ")", startPos)); pos++; continue; 075 case '{': tokens.add(new Token(TokenType.LCRLY, "{", startPos)); pos++; continue; 076 case '}': tokens.add(new Token(TokenType.RCRLY, "}", startPos)); pos++; continue; 077 case ';': tokens.add(new Token(TokenType.SEMI, ";", startPos)); pos++; continue; 078 case ',': tokens.add(new Token(TokenType.COMMA, ",", startPos)); pos++; continue; 079 case '.': tokens.add(new Token(TokenType.DOT, ".", startPos)); pos++; continue; 080 case '*': tokens.add(new Token(TokenType.STAR, "*", startPos)); pos++; continue; 081 case '=': tokens.add(new Token(TokenType.EQ, "=", startPos)); pos++; continue; 082 case '/': tokens.add(new Token(TokenType.REGEX, "/", startPos)); pos++; continue; 083 case '^': tokens.add(new Token(TokenType.IN, "^", startPos)); pos++; continue; 084 case '>': 085 if (peek() == '>') { 086 tokens.add(new Token(TokenType.GENERALIZES, ">>", startPos)); 087 pos += 2; 088 } else { 089 throw new VCLParseException("Unexpected character: " + ch, pos); 090 } 091 continue; 092 case '<': 093 if (peek() == '<') { 094 tokens.add(new Token(TokenType.IS_A, "<<", startPos)); 095 pos += 2; 096 } else if (peek() == '!') { 097 tokens.add(new Token(TokenType.CHILD_OF, "<!", startPos)); 098 pos += 2; 099 } else { 100 tokens.add(new Token(TokenType.DESC_OF, "<", startPos)); 101 pos++; 102 } 103 continue; 104 case '~': 105 if (peek() == '<' && peek(1) == '<') { 106 tokens.add(new Token(TokenType.IS_NOT_A, "~<<", startPos)); 107 pos += 3; 108 } else if (peek() == '^') { 109 tokens.add(new Token(TokenType.NOT_IN, "~^", startPos)); 110 pos += 2; 111 } else { 112 throw new VCLParseException("Unexpected character: " + ch, pos); 113 } 114 continue; 115 case '!': 116 if (peek() == '!' && peek(1) == '<') { 117 tokens.add(new Token(TokenType.DESC_LEAF, "!!<", startPos)); 118 pos += 3; 119 } else { 120 throw new VCLParseException("Unexpected character: " + ch, pos); 121 } 122 continue; 123 case '?': tokens.add(new Token(TokenType.EXISTS, "?", startPos)); pos++; continue; 124 case '"': 125 tokens.add(readQuotedValue(startPos)); 126 continue; 127 } 128 129 if (Character.isLetter(ch)) { 130 String value = readWhile(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_'); 131 if (pos < input.length() && input.charAt(pos) == ':') { 132 pos++; 133 String restOfUri = readWhile(c -> Character.isLetterOrDigit(c) || c == '?' || c == '&' 134 || c == '%' || c == '+' || c == '-' || c == '.' || c == '@' || c == '#' || c == '$' 135 || c == '!' || c == '{' || c == '}' || c == '_' || c == '/'); 136 value += ":" + restOfUri; 137 138 if (pos < input.length() && input.charAt(pos) == '|') { 139 pos++; 140 String version = readWhile(c -> c != '(' && c != ')' && !Character.isWhitespace(c)); 141 value += "|" + version; 142 } 143 tokens.add(new Token(TokenType.URI, value, startPos)); 144 } else { 145 tokens.add(new Token(TokenType.SCODE, value, startPos)); 146 } 147 } else if (Character.isDigit(ch)) { 148 String value = readWhile(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_'); 149 tokens.add(new Token(TokenType.SCODE, value, startPos)); 150 } else { 151 throw new VCLParseException("Unexpected character: " + ch, pos); 152 } 153 } 154 155 tokens.add(new Token(TokenType.EOF, "", pos)); 156 return tokens; 157 } 158 159 private Token readQuotedValue(int startPos) throws VCLParseException { 160 StringBuilder sb = new StringBuilder(); 161 pos++; 162 163 while (pos < input.length()) { 164 char ch = input.charAt(pos); 165 if (ch == '"') { 166 pos++; 167 return new Token(TokenType.QUOTED_VALUE, sb.toString(), startPos); 168 } else if (ch == '\\' && pos + 1 < input.length()) { 169 pos++; 170 char escaped = input.charAt(pos); 171 if (escaped == '"' || escaped == '\\') { 172 sb.append(escaped); 173 } else { 174 sb.append('\\').append(escaped); 175 } 176 pos++; 177 } else { 178 sb.append(ch); 179 pos++; 180 } 181 } 182 183 throw new VCLParseException("Unterminated quoted string", startPos); 184 } 185 186 private String readWhile(java.util.function.Predicate<Character> predicate) { 187 StringBuilder sb = new StringBuilder(); 188 while (pos < input.length() && predicate.test(input.charAt(pos))) { 189 sb.append(input.charAt(pos)); 190 pos++; 191 } 192 return sb.toString(); 193 } 194 195 private char peek() { 196 return peek(0); 197 } 198 199 private char peek(int offset) { 200 int peekPos = pos + 1 + offset; 201 return peekPos < input.length() ? input.charAt(peekPos) : '\0'; 202 } 203 204 private void skipWhitespace() { 205 while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { 206 pos++; 207 } 208 } 209 } 210 211 private static class Parser { 212 private List<Token> tokens; 213 private int pos = 0; 214 private ValueSet valueSet; 215 private ValueSetComposeComponent compose; 216 217 private static String getImplicitVcl(String system, String expression) { 218 String encodedVcl = ("(" + system + ")(" + expression + ")") 219 .replace("%", "%25") 220 .replace("&", "%26") 221 .replace("(", "%28") 222 .replace(")", "%29") 223 .replace("=", "%3D") 224 .replace("?", "%3F") 225 .replace("#", "%23") 226 ; 227 return VCL_URI + encodedVcl; 228 } 229 230 public Parser(List<Token> tokens) { 231 this.tokens = tokens; 232 this.valueSet = new ValueSet(); 233 this.valueSet.setStatus(Enumerations.PublicationStatus.DRAFT); 234 this.compose = new ValueSetComposeComponent(); 235 this.valueSet.setCompose(compose); 236 } 237 238 public ValueSet parse() throws VCLParseException { 239 parseExpr(); 240 expect(TokenType.EOF); 241 return valueSet; 242 } 243 244 private void parseExpr() throws VCLParseException { 245 parseSubExpr(false); 246 247 if (current().type == TokenType.COMMA) { 248 parseConjunction(); 249 } else if (current().type == TokenType.SEMI) { 250 parseDisjunction(); 251 } else if (current().type == TokenType.DASH) { 252 parseExclusion(); 253 } 254 } 255 256 private void parseSubExpr(boolean isExclusion) throws VCLParseException { 257 String systemUri = null; 258 259 if (current().type == TokenType.OPEN && peek().type == TokenType.URI) { 260 consume(TokenType.OPEN); 261 systemUri = current().value; 262 consume(TokenType.URI); 263 consume(TokenType.CLOSE); 264 } 265 266 if (current().type == TokenType.OPEN) { 267 consume(TokenType.OPEN); 268 269 if (current().type == TokenType.OPEN && peek().type == TokenType.URI) { 270 consume(TokenType.OPEN); 271 systemUri = current().value; 272 consume(TokenType.URI); 273 consume(TokenType.CLOSE); 274 } 275 276 if (isSimpleCodeList()) { 277 parseSimpleCodeList(systemUri, isExclusion); 278 } else { 279 parseExprWithinParentheses(isExclusion); 280 } 281 282 consume(TokenType.CLOSE); 283 } else { 284 parseSimpleExpr(systemUri, isExclusion); 285 } 286 } 287 288 private boolean isSimpleCodeList() { 289 int lookahead = pos; 290 while (lookahead < tokens.size()) { 291 Token token = tokens.get(lookahead); 292 293 if (token.type == TokenType.CLOSE) { 294 return true; 295 } 296 297 if (token.type == TokenType.OPEN && lookahead + 2 < tokens.size()) { 298 Token nextToken = tokens.get(lookahead + 1); 299 Token tokenAfterNext = tokens.get(lookahead + 2); 300 if (nextToken.type == TokenType.URI && tokenAfterNext.type == TokenType.CLOSE) { 301 lookahead += 3; 302 continue; 303 } 304 } 305 306 if (token.type == TokenType.OPEN || 307 token.type == TokenType.DASH || 308 isFilterOperator(token.type)) { 309 return false; 310 } 311 lookahead++; 312 } 313 return true; 314 } 315 316 private void parseExprWithinParentheses(boolean isExclusion) throws VCLParseException { 317 parseSubExpr(isExclusion); 318 319 while (current().type == TokenType.COMMA || current().type == TokenType.SEMI || current().type == TokenType.DASH) { 320 if (current().type == TokenType.COMMA) { 321 parseConjunctionWithFlag(isExclusion); 322 } else if (current().type == TokenType.SEMI) { 323 parseDisjunctionWithFlag(isExclusion); 324 } else if (current().type == TokenType.DASH) { 325 parseExclusion(); 326 } 327 } 328 } 329 330 private void parseSimpleCodeList(String systemUri, boolean isExclusion) throws VCLParseException { 331 ConceptSetComponent conceptSet = createConceptSet(systemUri, isExclusion); 332 333 if (current().type == TokenType.STAR) { 334 consume(TokenType.STAR); 335 conceptSet.addFilter() 336 .setProperty("concept") 337 .setOp(Enumerations.FilterOperator.EXISTS) 338 .setValue("true"); 339 return; 340 } else if (current().type == TokenType.IN) { 341 parseIncludeVs(conceptSet); 342 return; 343 } else { 344 String code = parseCode(); 345 conceptSet.addConcept().setCode(code); 346 } 347 348 while (current().type == TokenType.SEMI || current().type == TokenType.COMMA) { 349 consume(current().type); 350 351 if (current().type == TokenType.STAR) { 352 consume(TokenType.STAR); 353 conceptSet.addFilter() 354 .setProperty("concept") 355 .setOp(Enumerations.FilterOperator.EXISTS) 356 .setValue("true"); 357 } else if (current().type == TokenType.IN) { 358 parseIncludeVs(conceptSet); 359 } else { 360 String code = parseCode(); 361 conceptSet.addConcept().setCode(code); 362 } 363 } 364 } 365 366 private void parseSimpleExpr(String systemUri, boolean isExclusion) throws VCLParseException { 367 ConceptSetComponent conceptSet = createConceptSet(systemUri, isExclusion); 368 369 if (peek().type == TokenType.DOT) { 370 parseOf(systemUri, conceptSet); 371 } else if (current().type == TokenType.STAR) { 372 consume(TokenType.STAR); 373 conceptSet.addFilter() 374 .setProperty("concept") 375 .setOp(Enumerations.FilterOperator.EXISTS) 376 .setValue("true"); 377 } else if (current().type == TokenType.SCODE || current().type == TokenType.QUOTED_VALUE) { 378 if (current().type == TokenType.SCODE && current().value.contains(".")) { 379 parseOf(systemUri, conceptSet); 380 } else { 381 String code = parseCode(); 382 383 if (isFilterOperator(current().type)) { 384 parseFilter(conceptSet, code); 385 } else { 386 conceptSet.addConcept().setCode(code); 387 } 388 } 389 } else if (current().type == TokenType.IN) { 390 parseIncludeVs(conceptSet); 391 } else { 392 parseOf(systemUri, conceptSet); 393 } 394 } 395 396 private void parseOf(String systemUri, ConceptSetComponent conceptSet) throws VCLParseException { 397 boolean isVcl = false; 398 StringBuilder sb = new StringBuilder(); 399 400 switch (current().type) { 401 case LCRLY: { 402 // codeList or filterList 403 consume(TokenType.LCRLY); 404 if (peek().type == TokenType.COMMA) { 405 sb.append(parseCode()); 406 while (current().type == TokenType.COMMA) { 407 consume(TokenType.COMMA); 408 sb.append(",").append(parseCode()); 409 } 410 } else { 411 isVcl = true; 412 parseFilterList(sb); 413 } 414 consume(TokenType.RCRLY); 415 break; 416 } 417 case STAR: { 418 sb.append(current().value); 419 consume(TokenType.STAR); 420 break; 421 } 422 case URI: { 423 sb.append(current().value); 424 consume(TokenType.URI); 425 break; 426 } 427 case SCODE: 428 case QUOTED_VALUE: { 429 sb.append(current().value); 430 parseCode(); 431 break; 432 } 433 default: 434 throw new VCLParseException("Expected code, codeList, STAR, URI or filterList", current().position); 435 } 436 437 consume(TokenType.DOT); 438 439 String property = parseCode(); 440 String implicitVcl = isVcl ? getImplicitVcl(systemUri, sb.toString()) : sb.toString(); 441 442 addOf(conceptSet, property, implicitVcl); 443 } 444 445 private void addOf(ConceptSetComponent conceptSet, String property, String value) { 446 ConceptSetFilterComponent filter = conceptSet.addFilter().setProperty(property) 447 .setOp(Enumerations.FilterOperator.EQUAL).setValue(value); 448 449 filter.addExtension().setUrl("http://hl7.org/fhir/6.0/StructureDefinition/extension-ValueSet.compose.include.filter.op").setValue(new CodeType("of")); 450 } 451 452 private void parseFilter(ConceptSetComponent conceptSet, String property) throws VCLParseException { 453 ConceptSetFilterComponent filter = conceptSet.addFilter(); 454 filter.setProperty(property); 455 456 TokenType op = current().type; 457 consume(op); 458 459 switch (op) { 460 case EQ: 461 filter.setOp(Enumerations.FilterOperator.EQUAL); 462 filter.setValue(parseCode()); 463 break; 464 case IS_A: 465 filter.setOp(Enumerations.FilterOperator.ISA); 466 filter.setValue(parseCode()); 467 break; 468 case IS_NOT_A: 469 filter.setOp(Enumerations.FilterOperator.ISNOTA); 470 filter.setValue(parseCode()); 471 break; 472 case DESC_OF: 473 filter.setOp(Enumerations.FilterOperator.DESCENDENTOF); 474 filter.setValue(parseCode()); 475 break; 476 case REGEX: 477 filter.setOp(Enumerations.FilterOperator.REGEX); 478 filter.setValue(parseQuotedString()); 479 break; 480 case IN: 481 filter.setOp(Enumerations.FilterOperator.IN); 482 filter.setValue(parseFilterValue(conceptSet.getSystem())); 483 break; 484 case NOT_IN: 485 filter.setOp(Enumerations.FilterOperator.NOTIN); 486 filter.setValue(parseFilterValue(conceptSet.getSystem())); 487 break; 488 case GENERALIZES: 489 filter.setOp(Enumerations.FilterOperator.GENERALIZES); 490 filter.setValue(parseCode()); 491 break; 492 case CHILD_OF: 493 filter.setOp(Enumerations.FilterOperator.CHILDOF); 494 filter.setValue(parseCode()); 495 break; 496 case DESC_LEAF: 497 filter.setOp(Enumerations.FilterOperator.DESCENDENTLEAF); 498 filter.setValue(parseCode()); 499 break; 500 case EXISTS: 501 filter.setOp(Enumerations.FilterOperator.EXISTS); 502 filter.setValue(parseCode()); 503 break; 504 default: 505 throw new VCLParseException("Unexpected filter operator: " + op, current().position); 506 } 507 } 508 509 private void parseIncludeVs(ConceptSetComponent conceptSet) throws VCLParseException { 510 consume(TokenType.IN); 511 512 if (current().type == TokenType.URI) { 513 conceptSet.addValueSet(current().value); 514 consume(TokenType.URI); 515 } else if (current().type == TokenType.OPEN) { 516 consume(TokenType.OPEN); 517 conceptSet.addValueSet(current().value); 518 consume(TokenType.URI); 519 consume(TokenType.CLOSE); 520 } else { 521 throw new VCLParseException("Expected URI after ^", current().position); 522 } 523 } 524 525 private void parseConjunction() throws VCLParseException { 526 ConceptSetComponent currentConceptSet = getCurrentConceptSet(false); 527 528 while (current().type == TokenType.COMMA) { 529 consume(TokenType.COMMA); 530 531 if (current().type == TokenType.SCODE || current().type == TokenType.QUOTED_VALUE) { 532 String code = parseCode(); 533 if (isFilterOperator(current().type)) { 534 parseFilter(currentConceptSet, code); 535 } else { 536 currentConceptSet.addConcept().setCode(code); 537 } 538 } else { 539 parseSubExpr(false); 540 } 541 } 542 } 543 544 private void parseConjunctionWithFlag(boolean isExclusion) throws VCLParseException { 545 ConceptSetComponent currentConceptSet = getCurrentConceptSet(isExclusion); 546 547 while (current().type == TokenType.COMMA) { 548 consume(TokenType.COMMA); 549 550 if (current().type == TokenType.SCODE || current().type == TokenType.QUOTED_VALUE) { 551 String code = parseCode(); 552 if (isFilterOperator(current().type)) { 553 parseFilter(currentConceptSet, code); 554 } else { 555 currentConceptSet.addConcept().setCode(code); 556 } 557 } else { 558 parseSubExpr(isExclusion); 559 } 560 } 561 } 562 563 private void parseDisjunction() throws VCLParseException { 564 while (current().type == TokenType.SEMI) { 565 consume(TokenType.SEMI); 566 parseSubExpr(false); 567 } 568 } 569 570 private void parseDisjunctionWithFlag(boolean isExclusion) throws VCLParseException { 571 while (current().type == TokenType.SEMI) { 572 consume(TokenType.SEMI); 573 parseSubExpr(isExclusion); 574 } 575 } 576 577 private void parseExclusion() throws VCLParseException { 578 consume(TokenType.DASH); 579 parseSubExpr(true); 580 } 581 582 private String parseCode() throws VCLParseException { 583 if (current().type == TokenType.SCODE) { 584 String code = current().value; 585 consume(TokenType.SCODE); 586 return code; 587 } else if (current().type == TokenType.QUOTED_VALUE) { 588 String code = current().value; 589 consume(TokenType.QUOTED_VALUE); 590 return code; 591 } else { 592 throw new VCLParseException("Expected code", current().position); 593 } 594 } 595 596 private String parseQuotedString() throws VCLParseException { 597 if (current().type == TokenType.QUOTED_VALUE) { 598 String value = current().value; 599 consume(TokenType.QUOTED_VALUE); 600 return value; 601 } else { 602 throw new VCLParseException("Expected quoted string", current().position); 603 } 604 } 605 606 private String parseFilterValue(String systemUri) throws VCLParseException { 607 if (current().type == TokenType.LCRLY) { 608 consume(TokenType.LCRLY); 609 StringBuilder sb = new StringBuilder(); 610 sb.append(parseCode()); 611 612 if (isFilterOperator(current().type)) { 613 parseFilterList(sb); 614 consume(TokenType.RCRLY); 615 return getImplicitVcl(systemUri, sb.toString()); 616 } else { 617 while (current().type == TokenType.COMMA) { 618 consume(TokenType.COMMA); 619 sb.append(",").append(parseCode()); 620 } 621 622 consume(TokenType.RCRLY); 623 return sb.toString(); 624 } 625 } else if (current().type == TokenType.URI) { 626 String uri = current().value; 627 consume(TokenType.URI); 628 return uri; 629 } else { 630 return parseCode(); 631 } 632 } 633 634 private void parseFilterList(StringBuilder sb) throws VCLParseException { 635 int depth = 1; 636 while (depth > 0) { 637 TokenType tokenType = current().type; 638 639 switch (tokenType) { 640 case LCRLY: 641 depth++; 642 break; 643 case RCRLY: 644 depth--; 645 break; 646 default: 647 // do nothing 648 } 649 650 if (depth > 0) { 651 sb.append(current().value); 652 consume(tokenType); 653 } 654 } 655 } 656 657 private ConceptSetComponent createConceptSet(String systemUri, boolean isExclusion) { 658 ConceptSetComponent conceptSet = new ConceptSetComponent(); 659 660 if (systemUri != null) { 661 conceptSet.setSystem(systemUri); 662 } 663 664 if (isExclusion) { 665 compose.addExclude(conceptSet); 666 } else { 667 compose.addInclude(conceptSet); 668 } 669 670 return conceptSet; 671 } 672 673 private ConceptSetComponent getCurrentConceptSet(boolean isExclusion) { 674 if (isExclusion) { 675 List<ConceptSetComponent> excludes = compose.getExclude(); 676 return excludes.isEmpty() ? createConceptSet(null, true) : excludes.get(excludes.size() - 1); 677 } else { 678 List<ConceptSetComponent> includes = compose.getInclude(); 679 return includes.isEmpty() ? createConceptSet(null, false) : includes.get(includes.size() - 1); 680 } 681 } 682 683 private boolean isFilterOperator(TokenType type) { 684 return type == TokenType.EQ || type == TokenType.IS_A || type == TokenType.IS_NOT_A || 685 type == TokenType.DESC_OF || type == TokenType.REGEX || type == TokenType.IN || 686 type == TokenType.NOT_IN || type == TokenType.GENERALIZES || type == TokenType.CHILD_OF || 687 type == TokenType.DESC_LEAF || type == TokenType.EXISTS; 688 } 689 690 private Token current() { 691 return pos < tokens.size() ? tokens.get(pos) : new Token(TokenType.EOF, "", -1); 692 } 693 694 private Token peek() { 695 return pos + 1 < tokens.size() ? tokens.get(pos + 1) : new Token(TokenType.EOF, "", -1); 696 } 697 698 private void consume(TokenType expected) throws VCLParseException { 699 if (current().type != expected) { 700 throw new VCLParseException("Expected " + expected + " but got " + current().type, current().position); 701 } 702 pos++; 703 } 704 705 private void expect(TokenType expected) throws VCLParseException { 706 if (current().type != expected) { 707 throw new VCLParseException("Expected " + expected + " but got " + current().type, current().position); 708 } 709 } 710 } 711 712 public static ValueSet parse(String vclExpression) throws VCLParseException { 713 if (vclExpression == null || vclExpression.trim().isEmpty()) { 714 throw new VCLParseException("VCL expression cannot be empty"); 715 } 716 717 Lexer lexer = new Lexer(vclExpression); 718 List<Token> tokens = lexer.tokenize(); 719 720 Parser parser = new Parser(tokens); 721 return parser.parse(); 722 } 723 724 public static ValueSet parseAndId(String vclExpression) throws VCLParseException, IOException { 725 ValueSet vs = parse(vclExpression); 726 String json = new JsonParser().composeString(vs); 727 vs.setUrl("cid:" + json.hashCode()); 728 return vs; 729 } 730 731}