001package org.hl7.fhir.r4.terminologies;
002
003/*
004Copyright (c) 2011+, HL7, Inc
005All rights reserved.
006
007Redistribution and use in source and binary forms, with or without modification, 
008are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011   list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013   this list of conditions and the following disclaimer in the documentation 
014   and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016   endorse or promote products derived from this software without specific 
017   prior written permission.
018
019THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028POSSIBILITY OF SUCH DAMAGE.
029
030*/
031
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.FileOutputStream;
035import java.io.IOException;
036import java.util.HashMap;
037import java.util.Map;
038
039import org.apache.commons.io.IOUtils;
040import org.hl7.fhir.r4.context.IWorkerContext;
041import org.hl7.fhir.r4.formats.IParser.OutputStyle;
042import org.hl7.fhir.r4.formats.JsonParser;
043import org.hl7.fhir.r4.model.Parameters;
044import org.hl7.fhir.r4.model.MetadataResource;
045import org.hl7.fhir.r4.model.OperationOutcome;
046import org.hl7.fhir.r4.model.Resource;
047import org.hl7.fhir.r4.model.ValueSet;
048import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
049import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
050import org.hl7.fhir.r4.utils.ToolingExtensions;
051import org.hl7.fhir.exceptions.FHIRFormatError;
052import org.hl7.fhir.utilities.Utilities;
053import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
054
055public class ValueSetExpansionCache implements ValueSetExpanderFactory {
056
057  public class CacheAwareExpander implements ValueSetExpander {
058
059          @Override
060          public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) throws ETooCostly, IOException {
061            String cacheKey = makeCacheKey(source, expParams);
062                if (expansions.containsKey(cacheKey))
063                        return expansions.get(cacheKey);
064                ValueSetExpander vse = new ValueSetExpanderSimple(context);
065                ValueSetExpansionOutcome vso = vse.expand(source, expParams);
066                if (vso.getError() != null) {
067                  // well, we'll see if the designated server can expand it, and if it can, we'll cache it locally
068                        vso = context.expandVS(source, false, expParams == null || !expParams.getParameterBool("excludeNested"));
069                        if (cacheFolder != null) {
070                          FileOutputStream s = new FileOutputStream(Utilities.path(cacheFolder, makeFileName(source.getUrl())));
071                          context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).compose(s, vso.getValueset());
072                          s.close();
073                        }
074                }
075                if (vso.getValueset() != null)
076                  expansions.put(cacheKey, vso);
077                return vso;
078          }
079
080    private String makeCacheKey(ValueSet source, Parameters expParams) throws IOException {
081      if (expParams == null)
082        return source.getUrl();
083      JsonParser p = new JsonParser();
084      String r = p.composeString(expParams);
085      return source.getUrl() + " " + r.hashCode(); 
086    }
087
088  }
089
090  private static final String VS_ID_EXT = "http://tools/cache";
091
092  private final Map<String, ValueSetExpansionOutcome> expansions = new HashMap<String, ValueSetExpansionOutcome>();
093  private final Map<String, MetadataResource> canonicals = new HashMap<String, MetadataResource>();
094  private final IWorkerContext context;
095  private final String cacheFolder;
096
097  private Object lock;
098        
099        public ValueSetExpansionCache(IWorkerContext context, Object lock) {
100    super();
101    cacheFolder = null;
102    this.lock = lock;
103    this.context = context;
104  }
105  
106        public ValueSetExpansionCache(IWorkerContext context, String cacheFolder, Object lock) throws FHIRFormatError, IOException {
107    super();
108    this.context = context;
109    this.cacheFolder = cacheFolder;
110    this.lock = lock;
111    if (this.cacheFolder != null)
112      loadCache();
113  }
114  
115  private String makeFileName(String url) {
116    return url.replace("$", "").replace(":", "").replace("|", ".").replace("//", "/").replace("/", "_")+".xml";
117  }
118  
119  private void loadCache() throws FHIRFormatError, IOException {
120    File[] files = new File(cacheFolder).listFiles();
121    for (File f : files) {
122      if (f.getName().endsWith(".xml")) {
123        final FileInputStream is = new FileInputStream(f);
124        try {      
125          Resource r = context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).parse(is);
126          if (r instanceof OperationOutcome) {
127            OperationOutcome oo = (OperationOutcome) r;
128            expansions.put(ToolingExtensions.getExtension(oo,VS_ID_EXT).getValue().toString(),
129                new ValueSetExpansionOutcome(new XhtmlComposer(XhtmlComposer.XML, false).composePlainText(oo.getText().getDiv()), TerminologyServiceErrorClass.UNKNOWN));
130          } else if (r instanceof ValueSet) {
131            ValueSet vs = (ValueSet) r;
132            if (vs.hasExpansion())
133              expansions.put(vs.getUrl(), new ValueSetExpansionOutcome(vs));
134            else {
135              canonicals.put(vs.getUrl(), vs);
136              if (vs.hasVersion())
137                canonicals.put(vs.getUrl()+"|"+vs.getVersion(), vs);
138            }
139          } else if (r instanceof MetadataResource) {
140            MetadataResource md = (MetadataResource) r;
141            canonicals.put(md.getUrl(), md);
142            if (md.hasVersion())
143              canonicals.put(md.getUrl()+"|"+md.getVersion(), md);
144          }
145        } finally {
146          IOUtils.closeQuietly(is);
147        }
148      }
149    }
150  }
151
152  @Override
153        public ValueSetExpander getExpander() {
154                return new CacheAwareExpander();
155                // return new ValueSetExpander(valuesets, codesystems);
156        }
157
158  public MetadataResource getStoredResource(String canonicalUri) {
159    synchronized (lock) {
160    return canonicals.get(canonicalUri);
161    }
162  }
163
164  public void storeResource(MetadataResource md) throws IOException {
165    synchronized (lock) {
166      canonicals.put(md.getUrl(), md);
167      if (md.hasVersion())
168        canonicals.put(md.getUrl()+"|"+md.getVersion(), md);    
169    }
170    if (cacheFolder != null) {
171      FileOutputStream s = new FileOutputStream(Utilities.path(cacheFolder, makeFileName(md.getUrl()+"|"+md.getVersion())));
172      context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).compose(s, md);
173      s.close();
174    }
175  }
176
177
178}