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