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