001package org.hl7.fhir.r5.terminologies.utilities;
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.io.File;
035import java.io.FileNotFoundException;
036import java.io.IOException;
037import java.io.OutputStreamWriter;
038import java.util.*;
039
040import lombok.Getter;
041import lombok.Setter;
042import lombok.experimental.Accessors;
043import lombok.extern.slf4j.Slf4j;
044import org.hl7.fhir.exceptions.FHIRException;
045import org.hl7.fhir.r5.formats.IParser.OutputStyle;
046import org.hl7.fhir.r5.formats.JsonParser;
047import org.hl7.fhir.r5.model.*;
048import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
049import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
050import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
051import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
052import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
053import org.hl7.fhir.r5.utils.UserDataNames;
054import org.hl7.fhir.utilities.*;
055import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
056import org.hl7.fhir.utilities.json.model.JsonNull;
057import org.hl7.fhir.utilities.json.model.JsonProperty;
058import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
059import org.hl7.fhir.utilities.validation.ValidationOptions;
060
061import com.google.gson.JsonElement;
062import com.google.gson.JsonObject;
063import com.google.gson.JsonPrimitive;
064
065/**
066 * This implements a two level cache. 
067 *  - a temporary cache for remembering previous local operations
068 *  - a persistent cache for remembering tx server operations
069 *  
070 * the cache is a series of pairs: a map, and a list. the map is the loaded cache, the list is the persistent cache, carefully maintained in order for version control consistency
071 * 
072 * @author graha
073 *
074 */
075@MarkedToMoveToAdjunctPackage
076@Slf4j
077public class TerminologyCache {
078  
079  public static class SourcedCodeSystem {
080    private String server;
081    private CodeSystem cs;
082    
083    public SourcedCodeSystem(String server, CodeSystem cs) {
084      super();
085      this.server = server;
086      this.cs = cs;
087    }
088    public String getServer() {
089      return server;
090    }
091    public CodeSystem getCs() {
092      return cs;
093    } 
094  }
095
096
097  public static class SourcedCodeSystemEntry {
098    private String server;
099    private String filename;
100    
101    public SourcedCodeSystemEntry(String server, String filename) {
102      super();
103      this.server = server;
104      this.filename = filename;
105    }
106    public String getServer() {
107      return server;
108    }
109    public String getFilename() {
110      return filename;
111    }    
112  }
113
114  
115  public static class SourcedValueSet {
116    private String server;
117    private ValueSet vs;
118    
119    public SourcedValueSet(String server, ValueSet vs) {
120      super();
121      this.server = server;
122      this.vs = vs;
123    }
124    public String getServer() {
125      return server;
126    }
127    public ValueSet getVs() {
128      return vs;
129    } 
130  }
131
132  public static class SourcedValueSetEntry {
133    private String server;
134    private String filename;
135    
136    public SourcedValueSetEntry(String server, String filename) {
137      super();
138      this.server = server;
139      this.filename = filename;
140    }
141    public String getServer() {
142      return server;
143    }
144    public String getFilename() {
145      return filename;
146    }    
147  }
148
149  public static final boolean TRANSIENT = false;
150  public static final boolean PERMANENT = true;
151  private static final String NAME_FOR_NO_SYSTEM = "all-systems";
152  private static final String ENTRY_MARKER = "-------------------------------------------------------------------------------------";
153  private static final String BREAK = "####";
154  private static final String CACHE_FILE_EXTENSION = ".cache";
155  private static final String CAPABILITY_STATEMENT_TITLE = ".capabilityStatement";
156  private static final String TERMINOLOGY_CAPABILITIES_TITLE = ".terminologyCapabilities";
157  private static final String FIXED_CACHE_VERSION = "4"; // last change: change the way tx.fhir.org handles expansions
158
159
160  private SystemNameKeyGenerator systemNameKeyGenerator = new SystemNameKeyGenerator();
161
162  public class CacheToken {
163    @Getter
164    private String name;
165    private String key;
166    @Getter
167    private String request;
168    @Accessors(fluent = true)
169    @Getter
170    private boolean hasVersion;
171
172    public void setName(String n) {
173      String systemName = getSystemNameKeyGenerator().getNameForSystem(n);
174      if (name == null)
175        name = systemName;
176      else if (!systemName.equals(name))
177        name = NAME_FOR_NO_SYSTEM;
178    }
179  }
180
181  public static class SubsumesResult {
182    
183    private Boolean result;
184
185    protected SubsumesResult(Boolean result) {
186      super();
187      this.result = result;
188    }
189
190    public Boolean getResult() {
191      return result;
192    }
193    
194  }
195  
196  protected SystemNameKeyGenerator getSystemNameKeyGenerator() {
197    return systemNameKeyGenerator;
198  }
199  public class SystemNameKeyGenerator {
200    public static final String SNOMED_SCT_CODESYSTEM_URL = "http://snomed.info/sct";
201    public static final String RXNORM_CODESYSTEM_URL = "http://www.nlm.nih.gov/research/umls/rxnorm";
202    public static final String LOINC_CODESYSTEM_URL = "http://loinc.org";
203    public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org";
204
205    public static final String HL7_TERMINOLOGY_CODESYSTEM_BASE_URL = "http://terminology.hl7.org/CodeSystem/";
206    public static final String HL7_SID_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/sid/";
207    public static final String HL7_FHIR_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/";
208
209    public static final String ISO_CODESYSTEM_URN = "urn:iso:std:iso:";
210    public static final String LANG_CODESYSTEM_URN = "urn:ietf:bcp:47";
211    public static final String MIMETYPES_CODESYSTEM_URN = "urn:ietf:bcp:13";
212
213    public static final String _11073_CODESYSTEM_URN = "urn:iso:std:iso:11073:10101";
214    public static final String DICOM_CODESYSTEM_URL = "http://dicom.nema.org/resources/ontology/DCM";
215
216    public String getNameForSystem(String system) {
217      final int lastPipe = system.lastIndexOf('|');
218      final String systemBaseName = lastPipe == -1 ? system : system.substring(0,lastPipe);
219      String systemVersion = lastPipe == -1 ? null : system.substring(lastPipe + 1);
220
221      if (systemVersion != null) {
222        if (systemVersion.startsWith("http://snomed.info/sct/")) {
223          systemVersion = systemVersion.substring(23);
224        }
225        systemVersion = systemVersion.replace(":", "").replace("/", "").replace("\\", "").replace("?", "").replace("$", "").replace("*", "").replace("#", "").replace("%", "");
226      }
227      if (systemBaseName.equals(SNOMED_SCT_CODESYSTEM_URL))
228        return getVersionedSystem("snomed", systemVersion);
229      if (systemBaseName.equals(RXNORM_CODESYSTEM_URL))
230        return getVersionedSystem("rxnorm", systemVersion);
231      if (systemBaseName.equals(LOINC_CODESYSTEM_URL))
232        return getVersionedSystem("loinc", systemVersion);
233      if (systemBaseName.equals(UCUM_CODESYSTEM_URL))
234        return getVersionedSystem("ucum", systemVersion);
235      if (systemBaseName.startsWith(HL7_SID_CODESYSTEM_BASE_URL))
236        return getVersionedSystem(normalizeBaseURL(HL7_SID_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
237      if (systemBaseName.equals(_11073_CODESYSTEM_URN))
238        return getVersionedSystem("11073", systemVersion);
239      if (systemBaseName.startsWith(ISO_CODESYSTEM_URN))
240        return getVersionedSystem("iso"+systemBaseName.substring(ISO_CODESYSTEM_URN.length()).replace(":", ""), systemVersion);
241      if (systemBaseName.startsWith(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL))
242        return getVersionedSystem(normalizeBaseURL(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
243      if (systemBaseName.startsWith(HL7_FHIR_CODESYSTEM_BASE_URL))
244        return getVersionedSystem(normalizeBaseURL(HL7_FHIR_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
245      if (systemBaseName.equals(LANG_CODESYSTEM_URN))
246        return getVersionedSystem("lang", systemVersion);
247      if (systemBaseName.equals(MIMETYPES_CODESYSTEM_URN))
248        return getVersionedSystem("mimetypes", systemVersion);
249      if (systemBaseName.equals(DICOM_CODESYSTEM_URL))
250        return getVersionedSystem("dicom", systemVersion);
251      return getVersionedSystem(systemBaseName.replace("/", "_").replace(":", "_").replace("?", "X").replace("#", "X"), systemVersion);
252    }
253
254    public String normalizeBaseURL(String baseUrl, String fullUrl) {
255      return fullUrl.substring(baseUrl.length()).replace("/", "");
256    }
257
258    public String getVersionedSystem(String baseSystem, String version) {
259      if (version != null) {
260        return baseSystem + "_" + version;
261      }
262      return baseSystem;
263    }
264  }
265
266
267  private class CacheEntry {
268    private String request;
269    private boolean persistent;
270    private ValidationResult v;
271    private ValueSetExpansionOutcome e;
272    private SubsumesResult s;
273  }
274
275  private class NamedCache {
276    private String name; 
277    private List<CacheEntry> list = new ArrayList<CacheEntry>(); // persistent entries
278    private Map<String, CacheEntry> map = new HashMap<String, CacheEntry>();
279  }
280
281
282  private Object lock;
283  private String folder;
284  @Getter private int requestCount;
285  @Getter private int hitCount;
286  @Getter private int networkCount;
287  private Map<String, CapabilityStatement> capabilityStatementCache = new HashMap<>();
288  private Map<String, TerminologyCapabilities> terminologyCapabilitiesCache = new HashMap<>();
289  private Map<String, NamedCache> caches = new HashMap<String, NamedCache>();
290  private Map<String, SourcedValueSetEntry> vsCache = new HashMap<>();
291  private Map<String, SourcedCodeSystemEntry> csCache = new HashMap<>();
292  private Map<String, String> serverMap = new HashMap<>();
293  @Getter @Setter private static boolean noCaching;
294
295  @Getter @Setter private static boolean cacheErrors;
296
297
298  // use lock from the context
299  public TerminologyCache(Object lock, String folder) throws FileNotFoundException, IOException, FHIRException {
300    super();
301    this.lock = lock;
302    if (folder == null) {
303      folder = Utilities.path("[tmp]", "default-tx-cache");
304    } else if ("n/a".equals(folder)) {
305      // this is a weird way to do things but it maintains the legacy interface
306      folder = null;
307    }
308    this.folder = folder;
309    requestCount = 0;
310    hitCount = 0;
311    networkCount = 0;
312
313    if (folder != null) {
314      File f = ManagedFileAccess.file(folder);
315      if (!f.exists()) {
316        FileUtilities.createDirectory(folder);
317      }
318      if (!f.exists()) {
319        throw new IOException("Unable to create terminology cache at "+folder);
320      }
321      checkVersion();      
322      load();
323    }
324  }
325
326  private void checkVersion() throws IOException {
327    File verFile = ManagedFileAccess.file(Utilities.path(folder, "version.ctl"));
328    if (verFile.exists()) {
329      String ver = FileUtilities.fileToString(verFile);
330      if (!ver.equals(FIXED_CACHE_VERSION)) {
331        log.info("Terminology Cache Version has changed from 1 to "+FIXED_CACHE_VERSION+", so clearing txCache");
332        clear();
333      }
334      FileUtilities.stringToFile(FIXED_CACHE_VERSION, verFile);
335    } else {
336      FileUtilities.stringToFile(FIXED_CACHE_VERSION, verFile);
337    }
338  }
339
340  public String getServerId(String address) throws IOException  {
341    if (serverMap.containsKey(address)) {
342      return serverMap.get(address);
343    }
344    String id = address.replace("http://", "").replace("https://", "").replace("/", ".");
345    int i = 1;
346    while (serverMap.containsValue(id)) {
347      i++;
348      id =  address.replace("https:", "").replace("https:", "").replace("/", ".")+i;
349    }
350    serverMap.put(address, id);
351    if (folder != null) {
352      IniFile ini = new IniFile(Utilities.path(folder, "servers.ini"));
353      ini.setStringProperty("servers", id, address, null);
354      ini.save();
355    }
356    return id;
357  }
358  
359  public void unload() {
360    // not useable after this is called
361    caches.clear();
362    vsCache.clear();
363    csCache.clear();
364  }
365  
366  public void clear() throws IOException {
367    if (folder != null) {
368      FileUtilities.clearDirectory(folder);
369    }
370    caches.clear();
371    vsCache.clear();
372    csCache.clear();
373  }
374  
375  public boolean hasCapabilityStatement(String address) {
376    return capabilityStatementCache.containsKey(address);
377  }
378
379  public CapabilityStatement getCapabilityStatement(String address) {
380    return capabilityStatementCache.get(address);
381  }
382
383  public void cacheCapabilityStatement(String address, CapabilityStatement capabilityStatement) throws IOException {
384    if (noCaching) {
385      return;
386    } 
387    this.capabilityStatementCache.put(address, capabilityStatement);
388    save(capabilityStatement, CAPABILITY_STATEMENT_TITLE+"."+getServerId(address));
389  }
390
391
392  public boolean hasTerminologyCapabilities(String address) {
393    return terminologyCapabilitiesCache.containsKey(address);
394  }
395
396  public TerminologyCapabilities getTerminologyCapabilities(String address) {
397    return terminologyCapabilitiesCache.get(address);
398  }
399
400  public void cacheTerminologyCapabilities(String address, TerminologyCapabilities terminologyCapabilities) throws IOException {
401    if (noCaching) {
402      return;
403    }
404    this.terminologyCapabilitiesCache.put(address, terminologyCapabilities);
405    save(terminologyCapabilities, TERMINOLOGY_CAPABILITIES_TITLE+"."+getServerId(address));
406  }
407
408
409  public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs, Parameters expParameters) {
410    try {
411      CacheToken ct = new CacheToken();
412      if (code.hasSystem()) {
413        ct.setName(code.getSystem());
414        ct.hasVersion = code.hasVersion();
415      }
416      else
417        ct.name = NAME_FOR_NO_SYSTEM;
418      nameCacheToken(vs, ct);
419      JsonParser json = new JsonParser();
420      json.setOutputStyle(OutputStyle.PRETTY);
421      String expJS = expParameters == null ? "" : json.composeString(expParameters);
422
423      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
424        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())
425        +"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";
426      } else if (options.getVsAsUrl()) {
427        ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+extracted(json, vs)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
428      } else {
429        ValueSet vsc = getVSEssense(vs);
430        ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : extracted(json, vsc))+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
431      }
432      ct.key = String.valueOf(hashJson(ct.request));
433      return ct;
434    } catch (IOException e) {
435      throw new Error(e);
436    }
437  }
438
439  public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) {
440    try {
441      CacheToken ct = new CacheToken();
442      if (code.hasSystem()) {
443        ct.setName(code.getSystem());
444        ct.hasVersion = code.hasVersion();
445      } else {
446        ct.name = NAME_FOR_NO_SYSTEM;
447      }
448      ct.setName(vsUrl);
449      JsonParser json = new JsonParser();
450      json.setOutputStyle(OutputStyle.PRETTY);
451      String expJS = json.composeString(expParameters);
452
453      ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
454      ct.key = String.valueOf(hashJson(ct.request));
455      return ct;
456    } catch (IOException e) {
457      throw new Error(e);
458    }
459  }
460
461  public String extracted(JsonParser json, ValueSet vsc) throws IOException {
462    String s = null;
463    if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) {      
464      s =  vsc.getUrl();
465    } else {
466      s = json.composeString(vsc);
467    }
468    return s;
469  }
470
471  public CacheToken generateValidationToken(ValidationOptions options, CodeableConcept code, ValueSet vs, Parameters expParameters) {
472    try {
473      CacheToken ct = new CacheToken();
474      for (Coding c : code.getCoding()) {
475        if (c.hasSystem()) {
476          ct.setName(c.getSystem());
477          ct.hasVersion = c.hasVersion();
478        }
479      }
480      nameCacheToken(vs, ct);
481      JsonParser json = new JsonParser();
482      json.setOutputStyle(OutputStyle.PRETTY);
483      String expJS = json.composeString(expParameters);
484      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
485        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+
486            "\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";      
487      } else if (vs == null) { 
488        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";        
489      } else {
490        ValueSet vsc = getVSEssense(vs);
491        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+extracted(json, vsc)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
492      }
493      ct.key = String.valueOf(hashJson(ct.request));
494      return ct;
495    } catch (IOException e) {
496      throw new Error(e);
497    }
498  }
499
500  public ValueSet getVSEssense(ValueSet vs) {
501    if (vs == null)
502      return null;
503    ValueSet vsc = new ValueSet();
504    vsc.setCompose(vs.getCompose());
505    if (vs.hasExpansion()) {
506      vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter());
507      vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains());
508    }
509    return vsc;
510  }
511
512  public CacheToken generateExpandToken(ValueSet vs, boolean hierarchical) {
513    CacheToken ct = new CacheToken();
514    nameCacheToken(vs, ct);
515    if (vs.hasUrl() && vs.hasVersion()) {
516      ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\"}\r\n";      
517    } else {
518      ValueSet vsc = getVSEssense(vs);
519      JsonParser json = new JsonParser();
520      json.setOutputStyle(OutputStyle.PRETTY);
521      try {
522        ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"valueSet\" :"+extracted(json, vsc)+"}\r\n";
523      } catch (IOException e) {
524        throw new Error(e);
525      }
526    }
527    ct.key = String.valueOf(hashJson(ct.request));
528    return ct;
529  }
530  
531  public CacheToken generateExpandToken(String url, boolean hierarchical) {
532    CacheToken ct = new CacheToken();
533    ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(url)+"\"}\r\n";      
534    ct.key = String.valueOf(hashJson(ct.request));
535    return ct;
536  }
537
538  public void nameCacheToken(ValueSet vs, CacheToken ct) {
539    if (vs != null) {
540      for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
541        if (inc.hasSystem()) {
542          ct.setName(inc.getSystem());
543          ct.hasVersion = inc.hasVersion();
544        }
545      }
546      for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
547        if (inc.hasSystem()) {
548          ct.setName(inc.getSystem());
549          ct.hasVersion = inc.hasVersion();
550        }
551      }
552      for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains()) {
553        if (inc.hasSystem()) {
554          ct.setName(inc.getSystem());
555          ct.hasVersion = inc.hasVersion();
556        }
557      }
558    }
559  }
560
561  private String normalizeSystemPath(String path) {
562    return path.replace("/", "").replace('|','X');
563  }
564
565
566
567  public NamedCache getNamedCache(CacheToken cacheToken) {
568
569    final String cacheName = cacheToken.name == null ? "null" : cacheToken.name;
570
571    NamedCache nc = caches.get(cacheName);
572
573    if (nc == null) {
574      nc = new NamedCache();
575      nc.name = cacheName;
576      caches.put(nc.name, nc);
577    }
578    return nc;
579  }
580
581  public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) {
582    synchronized (lock) {
583      NamedCache nc = getNamedCache(cacheToken);
584      CacheEntry e = nc.map.get(cacheToken.key);
585      if (e == null)
586        return null;
587      else
588        return e.e;
589    }
590  }
591
592  public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) {
593    synchronized (lock) {      
594      NamedCache nc = getNamedCache(cacheToken);
595      CacheEntry e = new CacheEntry();
596      e.request = cacheToken.request;
597      e.persistent = persistent;
598      e.e = res;
599      store(cacheToken, persistent, nc, e);
600    }    
601  }
602
603  public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) {
604    if (noCaching) {
605      return;
606    }
607
608    if ( !cacheErrors &&
609        ( e.v!= null
610        && e.v.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED
611        && !cacheToken.hasVersion)) {
612      return;
613    }
614
615    boolean n = nc.map.containsKey(cacheToken.key);
616    nc.map.put(cacheToken.key, e);
617    if (persistent) {
618      if (n) {
619        for (int i = nc.list.size()- 1; i>= 0; i--) {
620          if (nc.list.get(i).request.equals(e.request)) {
621            nc.list.remove(i);
622          }
623        }
624      }
625      nc.list.add(e);
626      save(nc);  
627    }
628  }
629
630  public ValidationResult getValidation(CacheToken cacheToken) {
631    if (cacheToken.key == null) {
632      return null;
633    }
634    synchronized (lock) {
635      requestCount++;
636      NamedCache nc = getNamedCache(cacheToken);
637      CacheEntry e = nc.map.get(cacheToken.key);
638      if (e == null) {
639        networkCount++;
640        return null;
641      } else {
642        hitCount++;
643        return new ValidationResult(e.v);
644      }
645    }
646  }
647
648  public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) {
649    if (cacheToken.key != null) {
650      synchronized (lock) {      
651        NamedCache nc = getNamedCache(cacheToken);
652        CacheEntry e = new CacheEntry();
653        e.request = cacheToken.request;
654        e.persistent = persistent;
655        e.v = new ValidationResult(res);
656        store(cacheToken, persistent, nc, e);
657      }    
658    }
659  }
660
661
662  // persistence
663
664  public void save() {
665
666  }
667
668  private <K extends Resource> void save(K resource, String title) {
669    if (folder == null)
670      return;
671
672    try {
673      OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, title + CACHE_FILE_EXTENSION)), "UTF-8");
674
675      JsonParser json = new JsonParser();
676      json.setOutputStyle(OutputStyle.PRETTY);
677
678      sw.write(json.composeString(resource).trim());
679      sw.close();
680    } catch (Exception e) {
681      log.error("error saving capability statement "+e.getMessage(), e);
682    }
683  }
684
685  private void save(NamedCache nc) {
686    if (folder == null)
687      return;
688
689    try {
690      OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, nc.name+CACHE_FILE_EXTENSION)), "UTF-8");
691      sw.write(ENTRY_MARKER+"\r\n");
692      JsonParser json = new JsonParser();
693      json.setOutputStyle(OutputStyle.PRETTY);
694      for (CacheEntry ce : nc.list) {
695        sw.write(ce.request.trim());
696        sw.write(BREAK+"\r\n");
697        if (ce.e != null) {
698          sw.write("e: {\r\n");
699          if (ce.e.isFromServer())
700            sw.write("  \"from-server\" : true,\r\n");
701          if (ce.e.getValueset() != null) {
702            if (ce.e.getValueset().hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) {
703              sw.write("  \"source\" : "+Utilities.escapeJson(ce.e.getValueset().getUserString(UserDataNames.VS_EXPANSION_SOURCE)).trim()+",\r\n");              
704            }
705            sw.write("  \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n");
706          }
707          sw.write("  \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n");
708        } else if (ce.s != null) {
709          sw.write("s: {\r\n");
710          sw.write("  \"result\" : "+ce.s.result+"\r\n}\r\n");
711        } else {
712          sw.write("v: {\r\n");
713          boolean first = true;
714          if (ce.v.getDisplay() != null) {            
715            if (first) first = false; else sw.write(",\r\n");
716            sw.write("  \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\"");
717          }
718          if (ce.v.getCode() != null) {
719            if (first) first = false; else sw.write(",\r\n");
720            sw.write("  \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\"");
721          }
722          if (ce.v.getSystem() != null) {
723            if (first) first = false; else sw.write(",\r\n");
724            sw.write("  \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\"");
725          }
726          if (ce.v.getVersion() != null) {
727            if (first) first = false; else sw.write(",\r\n");
728            sw.write("  \"version\" : \""+Utilities.escapeJson(ce.v.getVersion()).trim()+"\"");
729          }
730          if (ce.v.getSeverity() != null) {
731            if (first) first = false; else sw.write(",\r\n");
732            sw.write("  \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+"");
733          }
734          if (ce.v.getMessage() != null) {
735            if (first) first = false; else sw.write(",\r\n");
736            sw.write("  \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"");
737          }
738          if (ce.v.getErrorClass() != null) {
739            if (first) first = false; else sw.write(",\r\n");
740            sw.write("  \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\"");
741          }
742          if (ce.v.getDefinition() != null) {
743            if (first) first = false; else sw.write(",\r\n");
744            sw.write("  \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\"");
745          }
746          if (ce.v.getStatus() != null) {
747            if (first) first = false; else sw.write(",\r\n");
748            sw.write("  \"status\" : \""+Utilities.escapeJson(ce.v.getStatus()).trim()+"\"");
749          }
750          if (ce.v.getServer() != null) {
751            if (first) first = false; else sw.write(",\r\n");
752            sw.write("  \"server\" : \""+Utilities.escapeJson(ce.v.getServer()).trim()+"\"");
753          }
754          if (ce.v.isInactive()) {
755            if (first) first = false; else sw.write(",\r\n");
756            sw.write("  \"inactive\" : true");
757          }
758          if (ce.v.getUnknownSystems() != null) {
759            if (first) first = false; else sw.write(",\r\n");
760            sw.write("  \"unknown-systems\" : \""+Utilities.escapeJson(CommaSeparatedStringBuilder.join(",", ce.v.getUnknownSystems())).trim()+"\"");
761          }
762          if (ce.v.getParameters() != null) {
763            if (first) first = false; else sw.write(",\r\n");
764            sw.write("  \"parameters\" : "+json.composeString(ce.v.getParameters()).trim()+"\r\n");
765          }
766          if (ce.v.getIssues() != null) {
767            if (first) first = false; else sw.write(",\r\n");
768            OperationOutcome oo = new OperationOutcome();
769            oo.setIssue(ce.v.getIssues());
770            sw.write("  \"issues\" : "+json.composeString(oo).trim()+"\r\n");
771          }
772          sw.write("\r\n}\r\n");
773        }
774        sw.write(ENTRY_MARKER+"\r\n");
775      }      
776      sw.close();
777    } catch (Exception e) {
778      log.error("error saving "+nc.name+": "+e.getMessage(), e);
779    }
780  }
781
782  private boolean isCapabilityCache(String fn) {
783    if (fn == null) {
784      return false;
785    }
786    return fn.startsWith(CAPABILITY_STATEMENT_TITLE) || fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE);
787  }
788
789  private void loadCapabilityCache(String fn) {
790    try {
791      String src = FileUtilities.fileToString(Utilities.path(folder, fn));
792      String serverId = Utilities.getFileNameForName(fn).replace(CACHE_FILE_EXTENSION, "");
793      serverId = serverId.substring(serverId.indexOf(".")+1);
794      serverId = serverId.substring(serverId.indexOf(".")+1);
795      String address = getServerForId(serverId);
796      if (address != null) {
797        JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(src);
798        Resource resource = new JsonParser().parse(o);
799
800        if (fn.startsWith(CAPABILITY_STATEMENT_TITLE)) {
801          this.capabilityStatementCache.put(address, (CapabilityStatement) resource);
802        } else if (fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE)) {
803          this.terminologyCapabilitiesCache.put(address, (TerminologyCapabilities) resource);
804        }
805      }
806    } catch (Exception e) {
807      e.printStackTrace();
808      throw new FHIRException("Error loading " + fn + ": " + e.getMessage(), e);
809    }
810  }
811
812  private String getServerForId(String serverId) {
813    for (String n : serverMap.keySet()) {
814      if (serverMap.get(n).equals(serverId)) {
815        return n;
816      }
817    }
818    return null;
819  }
820
821  private CacheEntry getCacheEntry(String request, String resultString) throws IOException {
822    CacheEntry ce = new CacheEntry();
823    ce.persistent = true;
824    ce.request = request;
825    char e = resultString.charAt(0);
826    resultString = resultString.substring(3);
827    JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString);
828    String error = loadJS(o.get("error"));
829    if (e == 'e') {
830      if (o.has("valueSet")) {
831        ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server"));
832        if (o.has("source")) {
833          ce.e.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, o.get("source").getAsString());
834        }
835      } else {
836        ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server"));
837      }
838    } else if (e == 's') {
839      ce.s = new SubsumesResult(o.get("result").getAsBoolean());
840    } else {
841      String t = loadJS(o.get("severity"));
842      IssueSeverity severity = t == null ? null :  IssueSeverity.fromCode(t);
843      String display = loadJS(o.get("display"));
844      String code = loadJS(o.get("code"));
845      String system = loadJS(o.get("system"));
846      String version = loadJS(o.get("version"));
847      String definition = loadJS(o.get("definition"));
848      String server = loadJS(o.get("server"));
849      String status = loadJS(o.get("status"));
850      boolean inactive = "true".equals(loadJS(o.get("inactive")));
851      String unknownSystems = loadJS(o.get("unknown-systems"));
852      OperationOutcome oo = o.has("issues") ? (OperationOutcome) new JsonParser().parse(o.getAsJsonObject("issues")) : null;
853      Parameters p = o.has("parameters") ? (Parameters) new JsonParser().parse(o.getAsJsonObject("parameters")) : null;
854      t = loadJS(o.get("class")); 
855      TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ;
856      ce.v = new ValidationResult(severity, error, system, version, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code), display, null).setErrorClass(errorClass);
857      ce.v.setUnknownSystems(CommaSeparatedStringBuilder.toSet(unknownSystems));
858      ce.v.setServer(server);
859      ce.v.setStatus(inactive, status);
860      if (oo != null) {
861        ce.v.setIssues(oo.getIssue());
862      }
863      if (p != null) {
864        ce.v.setParameters(p);
865      }
866    }
867    return ce;
868  }
869
870  private void loadNamedCache(String fn) {
871    int c = 0;
872    try {
873      String src = FileUtilities.fileToString(Utilities.path(folder, fn));
874      String title = fn.substring(0, fn.lastIndexOf("."));
875
876      NamedCache nc = new NamedCache();
877      nc.name = title;
878
879      if (src.startsWith("?"))
880        src = src.substring(1);
881      int i = src.indexOf(ENTRY_MARKER);
882      while (i > -1) {
883        c++;
884        String s = src.substring(0, i);
885        src = src.substring(i + ENTRY_MARKER.length() + 1);
886        i = src.indexOf(ENTRY_MARKER);
887        if (!Utilities.noString(s)) {
888          int j = s.indexOf(BREAK);
889          String request = s.substring(0, j);
890          String p = s.substring(j + BREAK.length() + 1).trim();
891
892          CacheEntry cacheEntry = getCacheEntry(request, p);
893
894          nc.map.put(String.valueOf(hashJson(cacheEntry.request)), cacheEntry);
895          nc.list.add(cacheEntry);
896        }
897        caches.put(nc.name, nc);
898      }        
899    } catch (Exception e) {
900      log.error("Error loading "+fn+": "+e.getMessage()+" entry "+c+" - ignoring it", e);
901    }
902  }
903
904  private void load() throws FHIRException, IOException {
905    IniFile ini = new IniFile(Utilities.path(folder, "servers.ini"));
906    if (ini.hasSection("servers")) {
907      for (String n : ini.getPropertyNames("servers")) {
908        serverMap.put(ini.getStringProperty("servers", n), n);
909      }
910    }
911
912    for (String fn : ManagedFileAccess.file(folder).list()) {
913      if (fn.endsWith(CACHE_FILE_EXTENSION) && !fn.equals("validation" + CACHE_FILE_EXTENSION)) {
914        try {
915          if (isCapabilityCache(fn)) {
916            loadCapabilityCache(fn);
917          } else {
918            loadNamedCache(fn);
919          }
920        } catch (FHIRException e) {
921          throw e;
922        }
923      }
924    }
925    try {
926      File f = ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json"));
927      if (f.exists()) {
928        org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f);
929        for (JsonProperty p : json.getProperties()) {
930          if (p.getValue().isJsonNull()) {
931            vsCache.put(p.getName(), null);
932          } else {
933            org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject();
934            vsCache.put(p.getName(), new SourcedValueSetEntry(j.asString("server"), j.asString("filename")));        
935          }
936        }
937      }
938    } catch (Exception e) {
939      log.error("Error loading vs external cache: "+e.getMessage(), e);
940    }
941    try {
942      File f = ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json"));
943      if (f.exists()) {
944        org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f);
945        for (JsonProperty p : json.getProperties()) {
946          if (p.getValue().isJsonNull()) {
947            csCache.put(p.getName(), null);
948          } else {
949            org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject();
950            csCache.put(p.getName(), new SourcedCodeSystemEntry(j.asString("server"), j.asString("filename")));        
951          }
952        }
953      }
954    } catch (Exception e) {
955      log.error("Error loading vs external cache: "+e.getMessage(), e);
956    }
957  }
958
959  private String loadJS(JsonElement e) {
960    if (e == null)
961      return null;
962    if (!(e instanceof JsonPrimitive))
963      return null;
964    String s = e.getAsString();
965    if ("".equals(s))
966      return null;
967    return s;
968  }
969
970  public String hashJson(String s) {
971    return String.valueOf(s
972      .trim()
973      .replaceAll("\\r\\n?", "\n")
974      .hashCode());
975  }
976
977  // management
978
979  public String summary(ValueSet vs) {
980    if (vs == null)
981      return "null";
982
983    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
984    for (ConceptSetComponent cc : vs.getCompose().getInclude())
985      b.append("Include "+getIncSummary(cc));
986    for (ConceptSetComponent cc : vs.getCompose().getExclude())
987      b.append("Exclude "+getIncSummary(cc));
988    return b.toString();
989  }
990
991  private String getIncSummary(ConceptSetComponent cc) {
992    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
993    for (UriType vs : cc.getValueSet())
994      b.append(vs.asStringValue());
995    String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : "";
996    String system = cc.getSystem();
997    if (cc.hasConcept())
998      return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd;
999    if (cc.hasFilter()) {
1000      String s = "";
1001      for (ConceptSetFilterComponent f : cc.getFilter()) {
1002        if (!Utilities.noString(s))
1003          s = s + " & ";
1004        s = s + f.getProperty()+" "+(f.hasOp() ? f.getOp().toCode() : "?")+" "+f.getValue();
1005      }
1006      return "from "+system+" where "+s+vsd;
1007    }
1008    return "All codes from "+system+vsd;
1009  }
1010
1011  public String summary(Coding code) {
1012    return code.getSystem()+"#"+code.getCode()+(code.hasDisplay() ? ": \""+code.getDisplay()+"\"" : "");
1013  }
1014
1015  public String summary(CodeableConcept code) {
1016    StringBuilder b = new StringBuilder();
1017    b.append("{");
1018    boolean first = true;
1019    for (Coding c : code.getCoding()) {
1020      if (first) first = false; else b.append(",");
1021      b.append(summary(c));
1022    }
1023    b.append("}: \"");
1024    b.append(code.getText());
1025    b.append("\"");
1026    return b.toString();
1027  }
1028
1029  public void removeCS(String url) {
1030    synchronized (lock) {
1031      String name = getSystemNameKeyGenerator().getNameForSystem(url);
1032      if (caches.containsKey(name)) {
1033        caches.remove(name);
1034      }
1035    }   
1036  }
1037
1038  public String getFolder() {
1039    return folder;
1040  }
1041
1042  public Map<String, String> servers() {
1043    Map<String, String> servers = new HashMap<>();
1044//    servers.put("http://local.fhir.org/r2", "tx.fhir.org");
1045//    servers.put("http://local.fhir.org/r3", "tx.fhir.org");
1046//    servers.put("http://local.fhir.org/r4", "tx.fhir.org");
1047//    servers.put("http://local.fhir.org/r5", "tx.fhir.org");
1048//
1049//    servers.put("http://tx-dev.fhir.org/r2", "tx.fhir.org");
1050//    servers.put("http://tx-dev.fhir.org/r3", "tx.fhir.org");
1051//    servers.put("http://tx-dev.fhir.org/r4", "tx.fhir.org");
1052//    servers.put("http://tx-dev.fhir.org/r5", "tx.fhir.org");
1053
1054    servers.put("http://tx.fhir.org/r2", "tx.fhir.org");
1055    servers.put("http://tx.fhir.org/r3", "tx.fhir.org");
1056    servers.put("http://tx.fhir.org/r4", "tx.fhir.org");
1057    servers.put("http://tx.fhir.org/r5", "tx.fhir.org");
1058
1059    return servers;
1060  }
1061
1062  public boolean hasValueSet(String canonical) {
1063    return vsCache.containsKey(canonical);
1064  }
1065
1066  public boolean hasCodeSystem(String canonical) {
1067    return csCache.containsKey(canonical);
1068  }
1069
1070  public SourcedValueSet getValueSet(String canonical) {
1071    SourcedValueSetEntry sp = vsCache.get(canonical);
1072    if (sp == null || folder == null) {
1073      return null;
1074    } else {
1075      try {
1076        return new SourcedValueSet(sp.getServer(), sp.getFilename() == null ? null : (ValueSet) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename()))));
1077      } catch (Exception e) {
1078        return null;
1079      }
1080    }
1081  }
1082
1083  public SourcedCodeSystem getCodeSystem(String canonical) {
1084    SourcedCodeSystemEntry sp = csCache.get(canonical);
1085    if (sp == null || folder == null) {
1086      return null;
1087    } else {
1088      try {
1089        return new SourcedCodeSystem(sp.getServer(), sp.getFilename() == null ? null : (CodeSystem) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename()))));
1090      } catch (Exception e) {
1091        return null;
1092      }
1093    }
1094  }
1095
1096  public void cacheValueSet(String canonical, SourcedValueSet svs) {
1097    if (canonical == null) {
1098      return;
1099    }
1100    try {
1101      if (svs == null) {
1102        vsCache.put(canonical, null);
1103      } else {
1104        String uuid = UUIDUtilities.makeUuidLC();
1105        String fn = "vs-"+uuid+".json";
1106        if (folder != null) {
1107          new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), svs.getVs());
1108        }
1109        vsCache.put(canonical, new SourcedValueSetEntry(svs.getServer(), fn));
1110      }    
1111      org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject();
1112      for (String k : vsCache.keySet()) {
1113        SourcedValueSetEntry sve = vsCache.get(k);
1114        if (sve == null) {
1115          j.add(k, new JsonNull());
1116        } else {
1117          org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject();
1118          e.set("server", sve.getServer());
1119          if (sve.getFilename() != null) {
1120            e.set("filename", sve.getFilename());
1121          }
1122          j.add(k, e);
1123        }
1124      }
1125      if (folder != null) {
1126        org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json")), true);
1127      }
1128    } catch (Exception e) {
1129      e.printStackTrace();
1130    }
1131  }
1132
1133  public void cacheCodeSystem(String canonical, SourcedCodeSystem scs) {
1134    if (canonical == null) {
1135      return;
1136    }
1137    try {
1138      if (scs == null) {
1139        csCache.put(canonical, null);
1140      } else {
1141        String uuid = UUIDUtilities.makeUuidLC();
1142        String fn = "cs-"+uuid+".json";
1143        if (folder != null) {
1144          new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), scs.getCs());
1145        }
1146        csCache.put(canonical, new SourcedCodeSystemEntry(scs.getServer(), fn));
1147      }    
1148      org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject();
1149      for (String k : csCache.keySet()) {
1150        SourcedCodeSystemEntry sve = csCache.get(k);
1151        if (sve == null) {
1152          j.add(k, new JsonNull());
1153        } else {
1154          org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject();
1155          e.set("server", sve.getServer());
1156          if (sve.getFilename() != null) {
1157            e.set("filename", sve.getFilename());
1158          }
1159          j.add(k, e);
1160        }
1161      }
1162      if (folder != null) {
1163        org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json")), true);
1164      }
1165    } catch (Exception e) {
1166      e.printStackTrace();
1167    }
1168  }
1169
1170  public CacheToken generateSubsumesToken(ValidationOptions options, Coding parent, Coding child, Parameters expParameters) {
1171    try {
1172      CacheToken ct = new CacheToken();
1173      if (parent.hasSystem()) {
1174        ct.setName(parent.getSystem());
1175      }
1176      if (child.hasSystem()) {
1177        ct.setName(child.getSystem());
1178      }
1179      ct.hasVersion = parent.hasVersion() || child.hasVersion();
1180      JsonParser json = new JsonParser();
1181      json.setOutputStyle(OutputStyle.PRETTY);
1182      String expJS = json.composeString(expParameters);
1183      ct.request = "{\"op\": \"subsumes\", \"parent\" : "+json.composeString(parent, "code")+", \"child\" :"+json.composeString(child, "code")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
1184      ct.key = String.valueOf(hashJson(ct.request));
1185      return ct;
1186    } catch (IOException e) {
1187      throw new Error(e);
1188    }
1189  }
1190
1191  public Boolean getSubsumes(CacheToken cacheToken) {
1192   if (cacheToken.key == null) {
1193     return null;
1194   }
1195   synchronized (lock) {
1196     requestCount++;
1197     NamedCache nc = getNamedCache(cacheToken);
1198     CacheEntry e = nc.map.get(cacheToken.key);
1199     if (e == null) {
1200       networkCount++;
1201       return null;
1202     } else {
1203       hitCount++;
1204       return e.s.result;
1205     }
1206   }
1207   
1208  }
1209
1210  public void cacheSubsumes(CacheToken cacheToken, Boolean b, boolean persistent) {
1211    if (cacheToken.key != null) {
1212      synchronized (lock) {      
1213        NamedCache nc = getNamedCache(cacheToken);
1214        CacheEntry e = new CacheEntry();
1215        e.request = cacheToken.request;
1216        e.persistent = persistent;
1217        e.s = new SubsumesResult(b);
1218        store(cacheToken, persistent, nc, e);
1219      }    
1220    }
1221  }
1222
1223
1224}