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