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}