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