001package org.hl7.fhir.r4.utils;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are 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  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.io.BufferedOutputStream;
035import java.io.ByteArrayOutputStream;
036import java.io.File;
037import java.io.IOException;
038import java.io.UnsupportedEncodingException;
039import java.util.ArrayList;
040import java.util.Calendar;
041import java.util.GregorianCalendar;
042import java.util.HashSet;
043import java.util.List;
044import java.util.Set;
045import java.util.TimeZone;
046
047import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
048import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
049import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
050import org.hl7.fhir.exceptions.FHIRException;
051import org.hl7.fhir.r4.model.ContactDetail;
052import org.hl7.fhir.r4.model.ContactPoint;
053import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem;
054import org.hl7.fhir.r4.model.Enumeration;
055import org.hl7.fhir.r4.model.Enumerations.FHIRVersion;
056import org.hl7.fhir.r4.model.ImplementationGuide;
057import org.hl7.fhir.r4.model.ImplementationGuide.ImplementationGuideDependsOnComponent;
058import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
059import org.hl7.fhir.utilities.TextFile;
060import org.hl7.fhir.utilities.Utilities;
061import org.hl7.fhir.utilities.cache.PackageGenerator.PackageType;
062import org.hl7.fhir.utilities.cache.ToolsVersion;
063
064import com.google.gson.Gson;
065import com.google.gson.GsonBuilder;
066import com.google.gson.JsonArray;
067import com.google.gson.JsonObject;
068
069public class NPMPackageGenerator {
070
071  public enum Category {
072    RESOURCE, EXAMPLE, OPENAPI, SCHEMATRON, RDF, OTHER, TOOL, TEMPLATE, JEKYLL;
073    
074    private String getDirectory() {
075      switch (this) {
076      case RESOURCE: return "/package/";
077      case EXAMPLE: return "/example/";
078      case OPENAPI: return "/openapi/";
079      case SCHEMATRON: return "/xml/";
080      case RDF: return "/rdf/";      
081      case OTHER: return "/other/";      
082      case TEMPLATE: return "/other/";      
083      case JEKYLL: return "/jekyll/";      
084      case TOOL: return "/bin/";      
085      }
086      return "/";
087    }
088  }
089
090  private String destFile;
091  private Set<String> created = new HashSet<String>();
092  private TarArchiveOutputStream tar;
093  private ByteArrayOutputStream OutputStream;
094  private BufferedOutputStream bufferedOutputStream;
095  private GzipCompressorOutputStream gzipOutputStream;
096  private JsonObject packageJ;
097  
098  public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, String genDate) throws FHIRException, IOException {
099    super();
100    System.out.println("create package file at "+destFile);
101    this.destFile = destFile;
102    start();
103    List<String> fhirVersion = new ArrayList<>();
104    for (Enumeration<FHIRVersion> v : ig.getFhirVersion())
105      fhirVersion.add(v.asStringValue());
106    buildPackageJson(canonical, kind, url, genDate, ig, fhirVersion);
107  }
108  
109  public static NPMPackageGenerator subset(NPMPackageGenerator master, String destFile, String id, String name) throws FHIRException, IOException {
110    JsonObject p = master.packageJ.deepCopy();
111    p.remove("name");
112    p.addProperty("name", id);
113    p.remove("type");
114    p.addProperty("type", PackageType.SUBSET.getCode());    
115    p.remove("title");
116    p.addProperty("title", name);    
117    return new NPMPackageGenerator(destFile, p);
118  }
119  
120  public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, String genDate, List<String> fhirVersion) throws FHIRException, IOException {
121    super();
122    System.out.println("create package file at "+destFile);
123    this.destFile = destFile;
124    start();
125    buildPackageJson(canonical, kind, url, genDate, ig, fhirVersion);
126  }
127  
128  public NPMPackageGenerator(String destFile, JsonObject npm) throws FHIRException, IOException {
129    super();
130    System.out.println("create package file at "+destFile);
131    this.destFile = destFile;
132    start();
133    Gson gson = new GsonBuilder().setPrettyPrinting().create();
134    String json = gson.toJson(npm);
135    try {
136      addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8"));
137    } catch (UnsupportedEncodingException e) {
138    }
139    packageJ = npm;
140  }
141  
142  private void buildPackageJson(String canonical, PackageType kind, String web, String genDate, ImplementationGuide ig, List<String> fhirVersion) throws FHIRException, IOException {
143    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
144    if (!ig.hasPackageId())
145      b.append("packageId");
146    if (!ig.hasVersion())
147      b.append("version");
148    if (!ig.hasFhirVersion())
149      b.append("fhirVersion");
150    if (!ig.hasLicense())
151      b.append("license");
152    for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) {
153      if (!d.hasVersion()) {
154        b.append("dependsOn.version("+d.getUri()+")");
155      }
156    }
157
158    JsonObject npm = new JsonObject();
159    npm.addProperty("name", ig.getPackageId());
160    npm.addProperty("version", ig.getVersion());
161    npm.addProperty("tools-version", ToolsVersion.TOOLS_VERSION);
162    npm.addProperty("type", kind.getCode());
163    if (ig.hasLicense())
164      npm.addProperty("license", ig.getLicense().toCode());
165    npm.addProperty("canonical", canonical);
166    npm.addProperty("url", web);
167    if (ig.hasTitle())
168      npm.addProperty("title", ig.getTitle());
169    if (ig.hasDescription())
170      npm.addProperty("description", ig.getDescription()+ " (built "+genDate+timezone()+")");
171    if (kind != PackageType.CORE) {
172      JsonObject dep = new JsonObject();
173      npm.add("dependencies", dep);
174      for (String v : fhirVersion) { // TODO: fix for multiple versions
175        dep.addProperty("hl7.fhir.core", v);
176      }
177      for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) {
178        dep.addProperty(d.getPackageId(), d.getVersion());
179      }
180    }
181    if (ig.hasPublisher())
182      npm.addProperty("author", ig.getPublisher());
183    JsonArray m = new JsonArray();
184    for (ContactDetail t : ig.getContact()) {
185      String email = email(t.getTelecom());
186      String url = url(t.getTelecom());
187      if (t.hasName() & (email != null || url != null)) {
188        JsonObject md = new JsonObject();
189        m.add(md);
190        md.addProperty("name", t.getName());
191        if (email != null)
192          md.addProperty("email", email);
193        if (url != null)
194          md.addProperty("url", url);
195      }
196    }
197    if (m.size() > 0)
198      npm.add("maintainers", m);
199    if (ig.getManifest().hasRendering())
200      npm.addProperty("homepage", ig.getManifest().getRendering());
201    JsonObject dir = new JsonObject();
202    npm.add("directories", dir);
203    dir.addProperty("lib", "package");
204    dir.addProperty("example", "example");
205    Gson gson = new GsonBuilder().setPrettyPrinting().create();
206    String json = gson.toJson(npm);
207    try {
208      addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8"));
209    } catch (UnsupportedEncodingException e) {
210    }
211    packageJ = npm;
212  }
213
214
215  private String timezone() {
216    TimeZone tz = TimeZone.getDefault();  
217    Calendar cal = GregorianCalendar.getInstance(tz);
218    int offsetInMillis = tz.getOffset(cal.getTimeInMillis());
219
220    String offset = String.format("%02d:%02d", Math.abs(offsetInMillis / 3600000), Math.abs((offsetInMillis / 60000) % 60));
221    offset = (offsetInMillis >= 0 ? "+" : "-") + offset;
222
223    return offset;
224  }
225
226
227  private String url(List<ContactPoint> telecom) {
228    for (ContactPoint cp : telecom) {
229      if (cp.getSystem() == ContactPointSystem.URL)
230        return cp.getValue();
231    }
232    return null;
233  }
234
235
236  private String email(List<ContactPoint> telecom) {
237    for (ContactPoint cp : telecom) {
238      if (cp.getSystem() == ContactPointSystem.EMAIL)
239        return cp.getValue();
240    }
241    return null;
242  }
243
244  private void start() throws IOException {
245    OutputStream = new ByteArrayOutputStream();
246    bufferedOutputStream = new BufferedOutputStream(OutputStream);
247    gzipOutputStream = new GzipCompressorOutputStream(bufferedOutputStream);
248    tar = new TarArchiveOutputStream(gzipOutputStream);
249  }
250
251
252  public void addFile(Category cat, String name, byte[] content) throws IOException {
253    String path = cat.getDirectory()+name;
254    if (created.contains(path)) 
255      System.out.println("Duplicate package file "+path);
256    else {
257      created.add(path);
258      TarArchiveEntry entry = new TarArchiveEntry(path);
259      entry.setSize(content.length);
260      tar.putArchiveEntry(entry);
261      tar.write(content);
262      tar.closeArchiveEntry();
263    }
264  }
265  
266  public void finish() throws IOException {
267    tar.finish();
268    tar.close();
269    gzipOutputStream.close();
270    bufferedOutputStream.close();
271    OutputStream.close();
272    TextFile.bytesToFile(OutputStream.toByteArray(), destFile);
273  }
274
275  public String filename() {
276    return destFile;
277  }
278
279  public void loadDir(String rootDir, String name) throws IOException {
280    loadFiles(rootDir, new File(Utilities.path(rootDir, name)));
281  }
282
283  public void loadFiles(String root, File dir, String... noload) throws IOException {
284    for (File f : dir.listFiles()) {
285      if (!Utilities.existsInList(f.getName(), noload)) {
286        if (f.isDirectory()) {
287          loadFiles(root, f);
288        } else {
289          String path = f.getAbsolutePath().substring(root.length()+1);
290          byte[] content = TextFile.fileToBytes(f);
291          if (created.contains(path)) 
292            System.out.println("Duplicate package file "+path);
293          else {
294            created.add(path);
295            TarArchiveEntry entry = new TarArchiveEntry(path);
296            entry.setSize(content.length);
297            tar.putArchiveEntry(entry);
298            tar.write(content);
299            tar.closeArchiveEntry();
300          }
301        }
302      }
303    }
304  }
305  
306  
307}