001package org.hl7.fhir.r5.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.text.SimpleDateFormat;
040import java.util.ArrayList;
041import java.util.Calendar;
042import java.util.Date;
043import java.util.GregorianCalendar;
044import java.util.HashSet;
045import java.util.List;
046import java.util.Locale;
047import java.util.Set;
048import java.util.TimeZone;
049import java.util.UUID;
050
051import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
052import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
053import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
054import org.hl7.fhir.exceptions.FHIRException;
055import org.hl7.fhir.r5.model.ContactDetail;
056import org.hl7.fhir.r5.model.ContactPoint;
057import org.hl7.fhir.r5.model.ContactPoint.ContactPointSystem;
058import org.hl7.fhir.r5.model.Enumeration;
059import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
060import org.hl7.fhir.r5.model.ImplementationGuide;
061import org.hl7.fhir.r5.model.ImplementationGuide.ImplementationGuideDependsOnComponent;
062import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
063import org.hl7.fhir.utilities.TextFile;
064import org.hl7.fhir.utilities.Utilities;
065import org.hl7.fhir.utilities.npm.NpmPackageIndexBuilder;
066import org.hl7.fhir.utilities.npm.ToolsVersion;
067import org.hl7.fhir.utilities.npm.PackageGenerator.PackageType;
068
069import com.google.gson.Gson;
070import com.google.gson.GsonBuilder;
071import com.google.gson.JsonArray;
072import com.google.gson.JsonObject;
073import com.google.gson.JsonPrimitive;
074
075public class NPMPackageGenerator {
076
077  public enum Category {
078    RESOURCE, EXAMPLE, OPENAPI, SCHEMATRON, RDF, OTHER, TOOL, TEMPLATE, JEKYLL;
079
080    private String getDirectory() {
081      switch (this) {
082      case RESOURCE: return "package/";
083      case EXAMPLE: return "package/example/";
084      case OPENAPI: return "package/openapi/";
085      case SCHEMATRON: return "package/xml/";
086      case RDF: return "package/rdf/";      
087      case OTHER: return "package/other/";      
088      case TEMPLATE: return "package/other/";      
089      case JEKYLL: return "package/jekyll/";      
090      case TOOL: return "package/bin/";      
091      }
092      return "/";
093    }
094  }
095
096  private String destFile;
097  private Set<String> created = new HashSet<String>();
098  private TarArchiveOutputStream tar;
099  private ByteArrayOutputStream OutputStream;
100  private BufferedOutputStream bufferedOutputStream;
101  private GzipCompressorOutputStream gzipOutputStream;
102  private JsonObject packageJ;
103  private JsonObject packageManifest;
104  private NpmPackageIndexBuilder indexer;
105  private String igVersion;
106
107
108  public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, boolean notForPublication) throws FHIRException, IOException {
109    super();
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, date, ig, fhirVersion, notForPublication);
116  }
117
118  public static NPMPackageGenerator subset(NPMPackageGenerator master, String destFile, String id, String name, Date date, boolean notForPublication) 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    if (notForPublication) {
127      p.addProperty("notForPublication", true);
128    }
129
130    return new NPMPackageGenerator(destFile, p, date, notForPublication);
131  }
132
133  public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException {
134    super();
135    this.destFile = destFile;
136    start();
137    buildPackageJson(canonical, kind, url, date, ig, fhirVersion, notForPublication);
138  }
139
140  public NPMPackageGenerator(String destFile, JsonObject npm, Date date, boolean notForPublication) throws FHIRException, IOException {
141    super();
142    String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date);
143    packageJ = npm;
144    packageManifest = new JsonObject();
145    packageManifest.addProperty("version", npm.get("version").getAsString());
146    packageManifest.addProperty("date", dt);
147    if (notForPublication) {
148      packageManifest.addProperty("notForPublication", true);
149    }
150    npm.addProperty("date", dt);
151    packageManifest.addProperty("name", npm.get("name").getAsString());
152    this.destFile = destFile;
153    start();
154    Gson gson = new GsonBuilder().setPrettyPrinting().create();
155    String json = gson.toJson(npm);
156    try {
157      addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8"));
158    } catch (UnsupportedEncodingException e) {
159    }
160  }
161
162  private void buildPackageJson(String canonical, PackageType kind, String web, Date date, ImplementationGuide ig, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException {
163    String dtHuman = new SimpleDateFormat("EEE, MMM d, yyyy HH:mmZ", new Locale("en", "US")).format(date);
164    String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date);
165
166    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
167    if (!ig.hasPackageId()) {
168      b.append("packageId");
169    }
170    if (!ig.hasVersion()) {
171      b.append("version");
172    }
173    if (!ig.hasFhirVersion()) {
174      b.append("fhirVersion");
175    }
176    if (!ig.hasLicense()) {
177      b.append("license");
178    }
179    for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) {
180      if (!d.hasVersion()) {
181        b.append("dependsOn.version("+d.getUri()+")");
182      }
183    }
184
185    JsonObject npm = new JsonObject();
186    npm.addProperty("name", ig.getPackageId());
187    npm.addProperty("version", ig.getVersion());
188    igVersion = ig.getVersion();
189    npm.addProperty("tools-version", ToolsVersion.TOOLS_VERSION);
190    npm.addProperty("type", kind.getCode());
191    npm.addProperty("date", dt);
192    if (ig.hasLicense()) {
193      npm.addProperty("license", ig.getLicense().toCode());
194    }
195    npm.addProperty("canonical", canonical);
196    if (notForPublication) {
197      npm.addProperty("notForPublication", true);
198    }
199    npm.addProperty("url", web);
200    if (ig.hasTitle()) {
201      npm.addProperty("title", ig.getTitle());
202    }
203    if (ig.hasDescription()) {
204      npm.addProperty("description", ig.getDescription()+ " (built "+dtHuman+timezone()+")");
205    }
206    JsonArray vl = new JsonArray();
207    
208    npm.add("fhirVersions", vl);
209    for (String v : fhirVersion) { 
210      vl.add(new JsonPrimitive(v));
211    }
212    
213    if (kind != PackageType.CORE) {
214      JsonObject dep = new JsonObject();
215      npm.add("dependencies", dep);
216      for (String v : fhirVersion) { 
217        String vp = packageForVersion(v);
218        if (vp != null ) {
219          dep.addProperty(vp, v);
220        }
221      }
222      for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) {
223        dep.addProperty(d.getPackageId(), d.getVersion());
224      }
225    }
226    if (ig.hasPublisher()) {
227      npm.addProperty("author", ig.getPublisher());
228    }
229    JsonArray m = new JsonArray();
230    for (ContactDetail t : ig.getContact()) {
231      String email = email(t.getTelecom());
232      String url = url(t.getTelecom());
233      if (t.hasName() & (email != null || url != null)) {
234        JsonObject md = new JsonObject();
235        m.add(md);
236        md.addProperty("name", t.getName());
237        if (email != null)
238          md.addProperty("email", email);
239        if (url != null)
240          md.addProperty("url", url);
241      }
242    }
243    if (m.size() > 0)
244      npm.add("maintainers", m);
245    if (ig.getManifest().hasRendering())
246      npm.addProperty("homepage", ig.getManifest().getRendering());
247    JsonObject dir = new JsonObject();
248    npm.add("directories", dir);
249    dir.addProperty("lib", "package");
250    dir.addProperty("example", "example");
251    Gson gson = new GsonBuilder().setPrettyPrinting().create();
252    String json = gson.toJson(npm);
253    try {
254      addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8"));
255    } catch (UnsupportedEncodingException e) {
256    }
257    packageJ = npm;
258
259    packageManifest = new JsonObject();
260    packageManifest.addProperty("version", ig.getVersion());
261    packageManifest.addProperty("fhirVersion", fhirVersion.toString());
262    packageManifest.addProperty("date", dt);
263    packageManifest.addProperty("name", ig.getPackageId());
264
265  }
266
267
268  private String packageForVersion(String v) {
269    if (v == null)
270      return null;
271    if (v.startsWith("1.0"))
272      return "hl7.fhir.r2.core";
273    if (v.startsWith("1.4"))
274      return "hl7.fhir.r2b.core";
275    if (v.startsWith("3.0"))
276      return "hl7.fhir.r3.core";
277    if (v.startsWith("4.0"))
278      return "hl7.fhir.r4.core";
279    if (v.startsWith("4.1") || v.startsWith("4.3"))
280      return "hl7.fhir.r4b.core";
281    return null;
282  }
283
284  private String timezone() {
285    TimeZone tz = TimeZone.getDefault();  
286    Calendar cal = GregorianCalendar.getInstance(tz);
287    int offsetInMillis = tz.getOffset(cal.getTimeInMillis());
288
289    String offset = String.format("%02d:%02d", Math.abs(offsetInMillis / 3600000), Math.abs((offsetInMillis / 60000) % 60));
290    offset = (offsetInMillis >= 0 ? "+" : "-") + offset;
291
292    return offset;
293  }
294
295
296  private String url(List<ContactPoint> telecom) {
297    for (ContactPoint cp : telecom) {
298      if (cp.getSystem() == ContactPointSystem.URL)
299        return cp.getValue();
300    }
301    return null;
302  }
303
304
305  private String email(List<ContactPoint> telecom) {
306    for (ContactPoint cp : telecom) {
307      if (cp.getSystem() == ContactPointSystem.EMAIL)
308        return cp.getValue();
309    }
310    return null;
311  }
312
313  private void start() throws IOException {
314    OutputStream = new ByteArrayOutputStream();
315    bufferedOutputStream = new BufferedOutputStream(OutputStream);
316    gzipOutputStream = new GzipCompressorOutputStream(bufferedOutputStream);
317    tar = new TarArchiveOutputStream(gzipOutputStream);
318    indexer = new NpmPackageIndexBuilder();
319    indexer.start();
320  }
321
322
323  public void addFile(Category cat, String name, byte[] content) throws IOException {
324    String path = cat.getDirectory()+name;
325    if (path.length() > 100) {
326      name = name.substring(0, name.indexOf("-"))+"-"+UUID.randomUUID().toString()+".json";
327      path = cat.getDirectory()+name;      
328    }
329      
330    if (created.contains(path)) {
331      System.out.println("Duplicate package file "+path);
332    } else {
333      created.add(path);
334      TarArchiveEntry entry = new TarArchiveEntry(path);
335      entry.setSize(content.length);
336      tar.putArchiveEntry(entry);
337      tar.write(content);
338      tar.closeArchiveEntry();
339      if(cat == Category.RESOURCE) {
340        indexer.seeFile(name, content);
341      }
342    }
343  }
344
345  public void finish() throws IOException {
346    buildIndexJson();
347    tar.finish();
348    tar.close();
349    gzipOutputStream.close();
350    bufferedOutputStream.close();
351    OutputStream.close();
352    TextFile.bytesToFile(OutputStream.toByteArray(), destFile);
353    // also, for cache management on current builds, generate a little manifest
354    Gson gson = new GsonBuilder().setPrettyPrinting().create();
355    String json = gson.toJson(packageManifest);
356    TextFile.stringToFile(json, Utilities.changeFileExt(destFile, ".manifest.json"), false);
357  }
358
359  private void buildIndexJson() throws IOException {
360    byte[] content = TextFile.stringToBytes(indexer.build(), false);
361    addFile(Category.RESOURCE, ".index.json", content); 
362  }
363
364  public String filename() {
365    return destFile;
366  }
367
368  public void loadDir(String rootDir, String name) throws IOException {
369    loadFiles(rootDir, new File(Utilities.path(rootDir, name)));
370  }
371
372  public void loadFiles(String root, File dir, String... noload) throws IOException {
373    for (File f : dir.listFiles()) {
374      if (!Utilities.existsInList(f.getName(), noload)) {
375        if (f.isDirectory()) {
376          loadFiles(root, f);
377        } else {
378          String path = f.getAbsolutePath().substring(root.length()+1);
379          byte[] content = TextFile.fileToBytes(f);
380          if (created.contains(path)) 
381            System.out.println("Duplicate package file "+path);
382          else {
383            created.add(path);
384            TarArchiveEntry entry = new TarArchiveEntry(path);
385            entry.setSize(content.length);
386            tar.putArchiveEntry(entry);
387            tar.write(content);
388            tar.closeArchiveEntry();
389          }
390        }
391      }
392    }
393  }
394
395  public String version() {
396    return igVersion;
397  }
398
399
400}