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.model.ExpansionProfile;
043import org.hl7.fhir.r4.model.MetadataResource;
044import org.hl7.fhir.r4.model.OperationOutcome;
045import org.hl7.fhir.r4.model.Resource;
046import org.hl7.fhir.r4.model.ValueSet;
047import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
048import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
049import org.hl7.fhir.r4.utils.ToolingExtensions;
050import org.hl7.fhir.exceptions.FHIRFormatError;
051import org.hl7.fhir.utilities.Utilities;
052import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
053
054public class ValueSetExpansionCache implements ValueSetExpanderFactory {
055
056  public class CacheAwareExpander implements ValueSetExpander {
057
058          @Override
059          public ValueSetExpansionOutcome expand(ValueSet source, ExpansionProfile profile) throws ETooCostly, IOException {
060            String cacheKey = makeCacheKey(source, profile);
061                if (expansions.containsKey(cacheKey))
062                        return expansions.get(cacheKey);
063                ValueSetExpander vse = new ValueSetExpanderSimple(context, ValueSetExpansionCache.this);
064                ValueSetExpansionOutcome vso = vse.expand(source, profile);
065                if (vso.getError() != null) {
066                  // well, we'll see if the designated server can expand it, and if it can, we'll cache it locally
067                        vso = context.expandVS(source, false, profile == null || !profile.getExcludeNested());
068                        if (cacheFolder != null) {
069                          FileOutputStream s = new FileOutputStream(Utilities.path(cacheFolder, makeFileName(source.getUrl())));
070                          context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).compose(s, vso.getValueset());
071                          s.close();
072                        }
073                }
074                if (vso.getValueset() != null)
075                  expansions.put(cacheKey, vso);
076                return vso;
077          }
078
079    private String makeCacheKey(ValueSet source, ExpansionProfile profile) {
080      return profile == null ? source.getUrl() : source.getUrl() + " " + profile.getUrl()+" "+profile.getExcludeNested(); 
081    }
082
083  }
084
085  private static final String VS_ID_EXT = "http://tools/cache";
086
087  private final Map<String, ValueSetExpansionOutcome> expansions = new HashMap<String, ValueSetExpansionOutcome>();
088  private final Map<String, MetadataResource> canonicals = new HashMap<String, MetadataResource>();
089  private final IWorkerContext context;
090  private final String cacheFolder;
091
092  private Object lock;
093        
094        public ValueSetExpansionCache(IWorkerContext context, Object lock) {
095    super();
096    cacheFolder = null;
097    this.lock = lock;
098    this.context = context;
099  }
100  
101        public ValueSetExpansionCache(IWorkerContext context, String cacheFolder, Object lock) throws FHIRFormatError, IOException {
102    super();
103    this.context = context;
104    this.cacheFolder = cacheFolder;
105    this.lock = lock;
106    if (this.cacheFolder != null)
107      loadCache();
108  }
109  
110  private String makeFileName(String url) {
111    return url.replace("$", "").replace(":", "").replace("|", ".").replace("//", "/").replace("/", "_")+".xml";
112  }
113  
114  private void loadCache() throws FHIRFormatError, IOException {
115    File[] files = new File(cacheFolder).listFiles();
116    for (File f : files) {
117      if (f.getName().endsWith(".xml")) {
118        final FileInputStream is = new FileInputStream(f);
119        try {      
120          Resource r = context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).parse(is);
121          if (r instanceof OperationOutcome) {
122            OperationOutcome oo = (OperationOutcome) r;
123            expansions.put(ToolingExtensions.getExtension(oo,VS_ID_EXT).getValue().toString(),
124                new ValueSetExpansionOutcome(new XhtmlComposer(XhtmlComposer.XML, false).composePlainText(oo.getText().getDiv()), TerminologyServiceErrorClass.UNKNOWN));
125          } else if (r instanceof ValueSet) {
126            ValueSet vs = (ValueSet) r;
127            if (vs.hasExpansion())
128              expansions.put(vs.getUrl(), new ValueSetExpansionOutcome(vs));
129            else {
130              canonicals.put(vs.getUrl(), vs);
131              if (vs.hasVersion())
132                canonicals.put(vs.getUrl()+"|"+vs.getVersion(), vs);
133            }
134          } else if (r instanceof MetadataResource) {
135            MetadataResource md = (MetadataResource) r;
136            canonicals.put(md.getUrl(), md);
137            if (md.hasVersion())
138              canonicals.put(md.getUrl()+"|"+md.getVersion(), md);
139          }
140        } finally {
141          IOUtils.closeQuietly(is);
142        }
143      }
144    }
145  }
146
147  @Override
148        public ValueSetExpander getExpander() {
149                return new CacheAwareExpander();
150                // return new ValueSetExpander(valuesets, codesystems);
151        }
152
153  public MetadataResource getStoredResource(String canonicalUri) {
154    synchronized (lock) {
155    return canonicals.get(canonicalUri);
156    }
157  }
158
159  public void storeResource(MetadataResource md) throws IOException {
160    synchronized (lock) {
161      canonicals.put(md.getUrl(), md);
162      if (md.hasVersion())
163        canonicals.put(md.getUrl()+"|"+md.getVersion(), md);    
164    }
165    if (cacheFolder != null) {
166      FileOutputStream s = new FileOutputStream(Utilities.path(cacheFolder, makeFileName(md.getUrl()+"|"+md.getVersion())));
167      context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).compose(s, md);
168      s.close();
169    }
170  }
171
172
173}