001package org.hl7.fhir.r5.terminologies.client;
002
003import java.io.File;
004import java.io.IOException;
005import java.net.MalformedURLException;
006import java.net.URISyntaxException;
007import java.net.URL;
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.List;
013import java.util.Map;
014import java.util.Set;
015
016import org.hl7.fhir.exceptions.TerminologyServiceException;
017import org.hl7.fhir.r5.context.CanonicalResourceManager;
018import org.hl7.fhir.r5.model.Bundle;
019import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
020import org.hl7.fhir.r5.model.CodeSystem;
021import org.hl7.fhir.r5.model.Parameters;
022import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
023import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent;
024import org.hl7.fhir.r5.model.TerminologyCapabilities;
025import org.hl7.fhir.r5.model.ValueSet;
026import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
027import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
028import org.hl7.fhir.r5.terminologies.client.TerminologyClientContext.TerminologyClientContextUseType;
029import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager.ServerOptionList;
030import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache;
031import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedCodeSystem;
032import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedValueSet;
033import org.hl7.fhir.r5.utils.ToolingExtensions;
034import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
035import org.hl7.fhir.utilities.ToolingClientLogger;
036import org.hl7.fhir.utilities.Utilities;
037import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
038import org.hl7.fhir.utilities.json.model.JsonObject;
039import org.hl7.fhir.utilities.json.parser.JsonParser;
040
041public class TerminologyClientManager {
042  public class ServerOptionList {
043    private List<String> authoritative = new ArrayList<String>();
044    private List<String> candidates = new ArrayList<String>();
045    
046    public ServerOptionList(String address) {
047      candidates.add(address);
048    }
049    
050    public ServerOptionList() {
051    }
052
053    public ServerOptionList(List<String> auth, List<String> cand) {
054      authoritative.addAll(auth);
055      candidates.addAll(cand);
056    }
057
058    public void replace(String src, String dst) {
059      for (int i = 0; i < candidates.size(); i++) {
060        if (candidates.get(i).contains("://"+src)) {
061          candidates.set(i, candidates.get(i).replace("://"+src, "://"+dst));
062        }
063      }
064      for (int i = 0; i < authoritative.size(); i++) {
065        if (authoritative.get(i).contains("://"+src)) {
066          authoritative.set(i, authoritative.get(i).replace("://"+src, "://"+dst));
067        }
068      }      
069    }
070
071    @Override
072    public String toString() {
073      return "auth = " + CommaSeparatedStringBuilder.join("|", authoritative)+ ", candidates=" + CommaSeparatedStringBuilder.join("|", candidates);
074    }    
075    
076  }
077
078  public ITerminologyClientFactory getFactory() {
079    return factory;
080  }
081
082  public interface ITerminologyClientFactory {
083    ITerminologyClient makeClient(String id, String url, String userAgent, ToolingClientLogger logger) throws URISyntaxException;
084    String getVersion();
085  }
086  
087  public class InternalLogEvent {
088    private String message;
089    private String server;
090    private String vs;
091    private String systems;
092    private String choices;
093    protected InternalLogEvent(String message, String server, String vs, String systems, String choices) {
094      super();
095      this.message = message;
096      this.server = server;
097      this.vs = vs;
098      this.systems = systems;
099      this.choices = choices;
100    }
101    protected InternalLogEvent(String message) {
102      super();
103      this.message = message;
104    }
105    public String getMessage() {
106      return message;
107    }
108    public String getVs() {
109      return vs;
110    }
111    public String getSystems() {
112      return systems;
113    }
114    public String getChoices() {
115      return choices;
116    }
117    public String getServer() {
118      return server;
119    }
120    
121  }
122
123  public static final String UNRESOLVED_VALUESET = "--unknown--";
124
125  private static final boolean IGNORE_TX_REGISTRY = false;
126  
127  private ITerminologyClientFactory factory;
128  private String cacheId;
129  private List<TerminologyClientContext> serverList = new ArrayList<>(); // clients by server address
130  private Map<String, TerminologyClientContext> serverMap = new HashMap<>(); // clients by server address
131  private Map<String, ServerOptionList> resMap = new HashMap<>(); // client resolution list
132  private List<InternalLogEvent> internalLog = new ArrayList<>();
133  protected Parameters expParameters;
134
135  private TerminologyCache cache;
136
137  private File cacheFile;
138  private String usage;
139
140  private String monitorServiceURL;
141
142  private boolean useEcosystem;
143
144  public TerminologyClientManager(ITerminologyClientFactory factory, String cacheId) {
145    super();
146    this.factory = factory;
147    this.cacheId = cacheId;
148  }
149  
150  public String getCacheId() {
151    return cacheId; 
152  }
153  
154  public void copy(TerminologyClientManager other) {
155    cacheId = other.cacheId;  
156    serverList.addAll(other.serverList);
157    serverMap.putAll(other.serverMap);
158    resMap.putAll(other.resMap);
159    useEcosystem = other.useEcosystem;
160    monitorServiceURL = other.monitorServiceURL;
161    factory = other.factory;
162    usage = other.usage;
163    internalLog = other.internalLog;
164  }
165
166
167  public TerminologyClientContext chooseServer(ValueSet vs, Set<String> systems, boolean expand) throws TerminologyServiceException {
168    if (serverList.isEmpty()) {
169      return null;
170    }
171    if (systems.contains(UNRESOLVED_VALUESET) || systems.isEmpty()) {
172      return serverList.get(0);
173    }
174    
175    List<ServerOptionList> choices = new ArrayList<>();
176    for (String s : systems) {
177      choices.add(findServerForSystem(s, expand));
178    }    
179    
180    // first we look for a server that's authoritative for all of them
181    for (ServerOptionList ol : choices) {
182      for (String s : ol.authoritative) {
183        boolean ok = true;
184        for (ServerOptionList t : choices) {
185          if (!t.authoritative.contains(s)) {
186            ok = false;
187          }
188        }
189        if (ok) {
190          log(vs, s, systems, choices, "Found authoritative server "+s);
191          return findClient(s, systems, expand);
192        }
193      }
194    }
195    
196    // now we look for a server that's authoritative for one of them and a candidate for the others
197    for (ServerOptionList ol : choices) {
198      for (String s : ol.authoritative) {
199        boolean ok = true;
200        for (ServerOptionList t : choices) {
201          if (!t.authoritative.contains(s) && !t.candidates.contains(s)) {
202            ok = false;
203          }
204        }
205        if (ok) {
206          log(vs, s, systems, choices, "Found partially authoritative server "+s);
207          return findClient(s, systems, expand);
208        }
209      }
210    }
211
212    // now we look for a server that's a candidate for all of them
213    for (ServerOptionList ol : choices) {
214      for (String s : ol.candidates) {
215        boolean ok = true;
216        for (ServerOptionList t : choices) {
217          if (!t.candidates.contains(s)) {
218            ok = false;
219          }
220        }
221        if (ok) {
222          log(vs, s, systems, choices, "Found candidate server "+s);
223          return findClient(s, systems, expand);
224        }
225      }
226    }
227    
228    for (String sys : systems) {
229      String uri = sys.contains("|") ? sys.substring(0, sys.indexOf("|")) : sys;
230      // this list is the list of code systems that have special handling on tx.fhir.org, and might not be resolved above.
231      // we don't want them to go to secondary servers (e.g. VSAC) by accident (they might go deliberately above)
232      if (Utilities.existsInList(uri, "http://unitsofmeasure.org", "http://loinc.org", "http://snomed.info/sct",
233          "http://www.nlm.nih.gov/research/umls/rxnorm", "http://hl7.org/fhir/sid/cvx", "urn:ietf:bcp:13", "urn:ietf:bcp:47",
234          "urn:ietf:rfc:3986", "http://www.ama-assn.org/go/cpt", "urn:oid:1.2.36.1.2001.1005.17", "urn:iso:std:iso:3166", 
235          "http://varnomen.hgvs.org", "http://unstats.un.org/unsd/methods/m49/m49.htm", "urn:iso:std:iso:4217", 
236          "http://hl7.org/fhir/sid/ndc", "http://fhir.ohdsi.org/CodeSystem/concepts", "http://fdasis.nlm.nih.gov", "https://www.usps.com/")) {
237        log(vs, serverList.get(0).getAddress(), systems, choices, "Use primary server for "+uri);
238        return serverList.get(0);
239      }
240    }
241
242    // no agreement? Then what we do depends     
243    if (vs != null) {
244      if (vs.hasUserData("External.Link")) {
245        String el = vs.getUserString("External.Link");
246        if ("https://vsac.nlm.nih.gov".equals(el)) {
247          el = getMaster().getAddress();
248        }
249        if (systems.size() == 1) {
250          log(vs, el, systems, choices, "Not handled by any servers. Using source @ '"+el+"'");
251        } else {
252          log(vs, el, systems, choices, "Handled by multiple servers. Using source @ '"+el+"'");
253        }        
254        return findClient(el, systems, expand);
255      } else {
256        if (systems.size() == 1) {
257          log(vs, serverList.get(0).getAddress(), systems, choices, "System not handled by any servers. Using primary server");
258        } else {
259          log(vs, serverList.get(0).getAddress(), systems, choices, "Systems handled by multiple servers. Using primary server");
260        }
261        return findClient(serverList.get(0).getAddress(), systems, expand);
262      }      
263    } else {
264      if (systems.size() == 1) {
265        log(vs, serverList.get(0).getAddress(), systems, choices, "System not handled by any servers. Using primary server");
266      } else {
267        log(vs, serverList.get(0).getAddress(), systems, choices, "Systems handled by multiple servers. Using primary server");
268      }
269      log(vs, serverList.get(0).getAddress(), systems, choices, "Fallback: primary server");
270      return findClient(serverList.get(0).getAddress(), systems, expand);
271    }
272  }
273
274  private void log(ValueSet vs, String server, Set<String> systems, List<ServerOptionList> choices, String message) {
275    String svs = (vs == null ? "null" : vs.getVersionedUrl());
276    String sys = systems.isEmpty() ? "--" : systems.size() == 1 ? systems.iterator().next() : systems.toString();
277    String sch = choices.isEmpty() ? "--" : choices.size() == 1 ? choices.iterator().next().toString() : choices.toString();
278    internalLog.add(new InternalLogEvent(message, server, svs, sys, sch));
279  }
280
281  private TerminologyClientContext findClient(String server, Set<String> systems, boolean expand) {
282    TerminologyClientContext client = serverMap.get(server);
283    if (client == null) {
284      try {
285        client = new TerminologyClientContext(factory.makeClient("id"+(serverList.size()+1), server, getMasterClient().getUserAgent(), getMasterClient().getLogger()), cacheId, false);
286      } catch (URISyntaxException e) {
287        throw new TerminologyServiceException(e);
288      }
289      client.setTxCache(cache);
290      serverList.add(client);
291      serverMap.put(server, client);
292    }
293    client.seeUse(systems, expand ? TerminologyClientContextUseType.expand : TerminologyClientContextUseType.validate);
294    return client;
295  }
296
297  private ServerOptionList findServerForSystem(String s, boolean expand) throws TerminologyServiceException {
298    ServerOptionList serverList = resMap.get(s);
299    if (serverList == null) {
300      serverList = decideWhichServer(s);
301      // testing support
302      try {
303        serverList.replace("tx.fhir.org", new URL(getMasterClient().getAddress()).getHost());
304      } catch (MalformedURLException e) {
305      }
306      resMap.put(s, serverList);
307      save();
308    }
309    return serverList;
310  }
311
312  private ServerOptionList decideWhichServer(String url) {
313    if (IGNORE_TX_REGISTRY || !useEcosystem) {
314      return new ServerOptionList(getMasterClient().getAddress());
315    }
316    if (expParameters != null) {
317      if (!url.contains("|")) {
318        // the client hasn't specified an explicit version, but the expansion parameters might
319        for (ParametersParameterComponent p : expParameters.getParameter()) {
320          if (Utilities.existsInList(p.getName(), "system-version", "force-system-version") && p.hasValuePrimitive() && p.getValue().primitiveValue().startsWith(url+"|")) {
321            url = p.getValue().primitiveValue();
322          }
323        }
324      } else {
325        // the expansion parameters might override the version
326        for (ParametersParameterComponent p : expParameters.getParameter()) {
327          if (Utilities.existsInList(p.getName(), "force-system-version") && p.hasValueCanonicalType() && p.getValue().primitiveValue().startsWith(url+"|")) {
328            url = p.getValue().primitiveValue();
329          }
330        }
331      }
332    }
333    String request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&url="+Utilities.URLEncode(url));
334    if (usage != null) {
335      request = request + "&usage="+usage;
336    } 
337    try {
338      ServerOptionList ret = new ServerOptionList();
339      JsonObject json = JsonParser.parseObjectFromUrl(request);
340      for (JsonObject item : json.getJsonObjects("authoritative")) {
341        ret.authoritative.add(item.asString("url"));
342      }
343      for (JsonObject item : json.getJsonObjects("candidates")) {
344        ret.candidates.add(item.asString("url"));
345      }
346      return ret;
347    } catch (Exception e) {
348      String msg = "Error resolving system "+url+": "+e.getMessage()+" ("+request+")";
349      if (!hasMessage(msg)) {
350        internalLog.add(new InternalLogEvent(msg));
351      }
352      if (!monitorServiceURL.contains("tx.fhir.org")) {
353        e.printStackTrace();
354      }
355    }
356    return new ServerOptionList( getMasterClient().getAddress());
357    
358  }
359
360  private boolean hasMessage(String msg) {
361    for (InternalLogEvent log : internalLog) {
362      if (msg.equals(log.message)) {
363        return true;
364      }
365    }
366    return false;
367  }
368
369  public List<TerminologyClientContext> serverList() {
370    return serverList;
371  }
372  
373  public boolean hasClient() {
374    return !serverList.isEmpty();
375  }
376
377  public int getRetryCount() {
378    return hasClient() ? getMasterClient().getRetryCount() : 0;
379  }
380
381  public void setRetryCount(int value) {
382    if (hasClient()) {
383      getMasterClient().setRetryCount(value);
384    }
385  }
386
387  public void setUserAgent(String value) {
388    if (hasClient()) {
389      getMasterClient().setUserAgent(value);
390    }
391  }
392
393  public void setLogger(ToolingClientLogger txLog) {
394    if (hasClient()) {
395      getMasterClient().setLogger(txLog);
396    }
397  }
398
399  public TerminologyClientContext setMasterClient(ITerminologyClient client, boolean useEcosystem) {
400    this.useEcosystem = useEcosystem;
401    TerminologyClientContext details = new TerminologyClientContext(client, cacheId, true);
402    details.setTxCache(cache);
403    serverList.clear();
404    serverList.add(details);
405    serverMap.put(client.getAddress(), details);  
406    monitorServiceURL = Utilities.pathURL(Utilities.getDirectoryForURL(client.getAddress()), "tx-reg");
407    return details;
408  }
409  
410  public TerminologyClientContext getMaster() {
411    return serverList.isEmpty() ? null : serverList.get(0);
412  }
413
414  public ITerminologyClient getMasterClient() {
415    return serverList.isEmpty() ? null : serverList.get(0).getClient();
416  }
417
418  public Map<String, TerminologyClientContext> serverMap() {
419    Map<String, TerminologyClientContext> map = new HashMap<>();
420    for (TerminologyClientContext t : serverList) {
421      map.put(t.getClient().getAddress(), t);
422    }
423    return map;
424  }
425
426
427  public void setFactory(ITerminologyClientFactory factory) {
428    this.factory = factory;    
429  }
430
431  public void setCache(TerminologyCache cache) {
432    this.cache = cache;
433    this.cacheFile = null;
434
435    if (cache != null && cache.getFolder() != null) {
436      try {
437        cacheFile = ManagedFileAccess.file(Utilities.path(cache.getFolder(), "system-map.json"));
438        if (cacheFile.exists()) {
439          JsonObject json = JsonParser.parseObject(cacheFile);
440          for (JsonObject pair : json.getJsonObjects("systems")) {
441            if (pair.has("server")) {
442              resMap.put(pair.asString("system"), new ServerOptionList(pair.asString("server")));
443            } else {
444              resMap.put(pair.asString("system"), new ServerOptionList(pair.getStrings("authoritative"), pair.getStrings("candidates")));
445            }
446          }
447        }
448      } catch (Exception e) {
449        e.printStackTrace();
450      }
451    }
452  }
453
454  private void save() {
455    if (cacheFile != null && cache.getFolder() != null) {
456      JsonObject json = new JsonObject();
457      for (String s : Utilities.sorted(resMap.keySet())) {
458        JsonObject si = new JsonObject();
459        json.forceArray("systems").add(si);
460        si.add("system", s);
461        si.add("authoritative", resMap.get(s).authoritative);
462        si.add("candidates", resMap.get(s).candidates);
463      }
464      try {
465        JsonParser.compose(json, cacheFile, true);
466      } catch (IOException e) {
467      }
468    }
469  }
470
471  public List<TerminologyClientContext> getServerList() {
472    return serverList;
473  }
474
475  public Map<String, TerminologyClientContext> getServerMap() {
476    return serverMap;
477  }
478
479  public String getMonitorServiceURL() {
480    return monitorServiceURL;
481  }
482
483  public Parameters getExpansionParameters() {
484    return expParameters;
485  }
486
487  public void setExpansionParameters(Parameters expParameters) {
488    this.expParameters = expParameters;
489  }
490
491  public String getUsage() {
492    return usage;
493  }
494
495  public void setUsage(String usage) {
496    this.usage = usage;
497  }
498
499  public SourcedValueSet findValueSetOnServer(String canonical) {
500    if (IGNORE_TX_REGISTRY || getMasterClient() == null) {
501      return null;
502    }
503    String request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&valueSet="+Utilities.URLEncode(canonical));
504    String server = null;
505    try {
506      if (!useEcosystem) {
507        server = getMasterClient().getAddress();
508      } else {
509        if (usage != null) {
510          request = request + "&usage="+usage;
511        }
512        JsonObject json = JsonParser.parseObjectFromUrl(request);
513        for (JsonObject item : json.getJsonObjects("authoritative")) {
514          if (server == null) {
515            server = item.asString("url");
516          }
517        }
518        for (JsonObject item : json.getJsonObjects("candidates")) {
519          if (server == null) {
520            server = item.asString("url");
521          }
522        }
523        if (server == null) {
524          return null;
525        }
526        if (server.contains("://tx.fhir.org")) {
527          try {
528            server = server.replace("tx.fhir.org", new URL(getMasterClient().getAddress()).getHost());
529          } catch (MalformedURLException e) {
530          }
531        }
532      }
533      TerminologyClientContext client = serverMap.get(server);
534      if (client == null) {
535        try {
536          client = new TerminologyClientContext(factory.makeClient("id"+(serverList.size()+1), server, getMasterClient().getUserAgent(), getMasterClient().getLogger()), cacheId, false);
537        } catch (URISyntaxException e) {
538          throw new TerminologyServiceException(e);
539        }
540        client.setTxCache(cache);
541        serverList.add(client);
542        serverMap.put(server, client);
543      }
544      client.seeUse(canonical, TerminologyClientContextUseType.readVS);
545      String criteria = canonical.contains("|") ? 
546          "?_format=json&url="+Utilities.URLEncode(canonical.substring(0, canonical.lastIndexOf("|")))+"&version="+Utilities.URLEncode(canonical.substring(canonical.lastIndexOf("|")+1)): 
547            "?_format=json&url="+Utilities.URLEncode(canonical);
548      request = Utilities.pathURL(client.getAddress(), "ValueSet"+ criteria);
549      Bundle bnd = client.getClient().search("ValueSet", criteria);
550      String rid = null;
551      if (bnd.getEntry().size() == 0) {
552        return null;
553      } else if (bnd.getEntry().size() > 1) {
554        List<ValueSet> vslist = new ArrayList<>();
555        for (BundleEntryComponent be : bnd.getEntry()) {
556          if (be.hasResource() && be.getResource() instanceof ValueSet) {
557            vslist.add((ValueSet) be.getResource());
558          }
559        }
560        Collections.sort(vslist, new ValueSetUtilities.ValueSetSorter());
561        rid = vslist.get(vslist.size()-1).getIdBase();
562      } else {
563        if (bnd.getEntryFirstRep().hasResource() && bnd.getEntryFirstRep().getResource() instanceof ValueSet) {
564          rid = bnd.getEntryFirstRep().getResource().getIdBase();
565        }
566      }
567      if (rid == null) {
568        return null;
569      }
570      ValueSet vs = (ValueSet) client.getClient().read("ValueSet", rid);
571      return new SourcedValueSet(server, vs);
572    } catch (Exception e) {
573      e.printStackTrace();
574      String msg = "Error resolving valueSet "+canonical+": "+e.getMessage()+" ("+request+")";
575      if (!hasMessage(msg)) {
576        internalLog.add(new InternalLogEvent(msg));
577      }
578      e.printStackTrace();
579      return null;
580    }
581  }
582
583  public SourcedCodeSystem findCodeSystemOnServer(String canonical) {
584    if (IGNORE_TX_REGISTRY || getMasterClient() == null || !useEcosystem) {
585      return null;
586    }
587    String request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&url="+Utilities.URLEncode(canonical));
588    if (usage != null) {
589      request = request + "&usage="+usage;
590    }
591    String server = null;
592    try {
593      JsonObject json = JsonParser.parseObjectFromUrl(request);
594      for (JsonObject item : json.getJsonObjects("authoritative")) {
595        if (server == null) {
596          server = item.asString("url");
597        }
598      }
599      for (JsonObject item : json.getJsonObjects("candidates")) {
600        if (server == null) {
601          server = item.asString("url");
602        }
603      }
604      if (server == null) {
605        return null;
606      }
607      if (server.contains("://tx.fhir.org")) {
608        try {
609          server = server.replace("tx.fhir.org", new URL(getMasterClient().getAddress()).getHost());
610        } catch (MalformedURLException e) {
611        }
612      }
613      TerminologyClientContext client = serverMap.get(server);
614      if (client == null) {
615        try {
616          client = new TerminologyClientContext(factory.makeClient("id"+(serverList.size()+1), server, getMasterClient().getUserAgent(), getMasterClient().getLogger()), cacheId, false);
617        } catch (URISyntaxException e) {
618          throw new TerminologyServiceException(e);
619        }
620        client.setTxCache(cache);
621        serverList.add(client);
622        serverMap.put(server, client);
623      }
624      client.seeUse(canonical, TerminologyClientContextUseType.readCS);
625      String criteria = canonical.contains("|") ? 
626          "?_format=json&url="+Utilities.URLEncode(canonical.substring(0, canonical.lastIndexOf("|")))+"&version="+Utilities.URLEncode(canonical.substring(canonical.lastIndexOf("|")+1)): 
627            "?_format=json&url="+Utilities.URLEncode(canonical);
628      request = Utilities.pathURL(client.getAddress(), "CodeSystem"+ criteria);
629      Bundle bnd = client.getClient().search("CodeSystem", criteria);
630      String rid = null;
631      if (bnd.getEntry().size() == 0) {
632        return null;
633      } else if (bnd.getEntry().size() > 1) {
634        List<CodeSystem> cslist = new ArrayList<>();
635        for (BundleEntryComponent be : bnd.getEntry()) {
636          if (be.hasResource() && be.getResource() instanceof CodeSystem) {
637            cslist.add((CodeSystem) be.getResource());
638          }
639        }
640        Collections.sort(cslist, new CodeSystemUtilities.CodeSystemSorter());
641        rid = cslist.get(cslist.size()-1).getIdBase();
642      } else {
643        if (bnd.getEntryFirstRep().hasResource() && bnd.getEntryFirstRep().getResource() instanceof CodeSystem) {
644          rid = bnd.getEntryFirstRep().getResource().getIdBase();
645        }
646      }
647      if (rid == null) {
648        return null;
649      }
650      CodeSystem vs = (CodeSystem) client.getClient().read("CodeSystem", rid);
651      return new SourcedCodeSystem(server, vs);
652    } catch (Exception e) {
653      e.printStackTrace();
654      String msg = "Error resolving valueSet "+canonical+": "+e.getMessage()+" ("+request+")";
655      if (!hasMessage(msg)) {
656        internalLog.add(new InternalLogEvent(msg));
657      }
658      e.printStackTrace();
659      return null;
660    }
661  }
662
663  public boolean supportsSystem(String system) throws IOException {
664    for (TerminologyClientContext client : serverList) {
665      if (client.supportsSystem(system)) {
666        return true;
667      }
668    }
669    return false;
670  }
671
672  public List<InternalLogEvent> getInternalLog() {
673    return internalLog;
674  }
675}