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