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.Coding; 056import org.hl7.fhir.r5.model.ContactDetail; 057import org.hl7.fhir.r5.model.ContactPoint; 058import org.hl7.fhir.r5.model.ContactPoint.ContactPointSystem; 059import org.hl7.fhir.r5.model.Enumeration; 060import org.hl7.fhir.r5.model.Enumerations.FHIRVersion; 061import org.hl7.fhir.r5.model.ImplementationGuide; 062import org.hl7.fhir.r5.model.ImplementationGuide.ImplementationGuideDependsOnComponent; 063import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 064import org.hl7.fhir.utilities.TextFile; 065import org.hl7.fhir.utilities.Utilities; 066import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 067import org.hl7.fhir.utilities.json.model.JsonArray; 068import org.hl7.fhir.utilities.json.model.JsonObject; 069import org.hl7.fhir.utilities.json.model.JsonString; 070import org.hl7.fhir.utilities.json.parser.JsonParser; 071import org.hl7.fhir.utilities.npm.NpmPackageIndexBuilder; 072import org.hl7.fhir.utilities.npm.PackageGenerator.PackageType; 073import org.hl7.fhir.utilities.npm.ToolsVersion; 074 075public class NPMPackageGenerator { 076 077 public enum Category { 078 RESOURCE, EXAMPLE, OPENAPI, SCHEMATRON, RDF, OTHER, TOOL, TEMPLATE, JEKYLL, TEST; 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 TEST: return "package/tests/"; 091 case TOOL: return "package/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 private JsonObject packageManifest; 105 private NpmPackageIndexBuilder indexer; 106 private String igVersion; 107 private String indexdb; 108 109 110 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, boolean notForPublication) throws FHIRException, IOException { 111 super(); 112 this.destFile = destFile; 113 start(); 114 List<String> fhirVersion = new ArrayList<>(); 115 for (Enumeration<FHIRVersion> v : ig.getFhirVersion()) 116 fhirVersion.add(v.asStringValue()); 117 buildPackageJson(canonical, kind, url, date, ig, fhirVersion, notForPublication); 118 } 119 120 public static NPMPackageGenerator subset(NPMPackageGenerator master, String destFile, String id, String name, Date date, boolean notForPublication) throws FHIRException, IOException { 121 JsonObject p = master.packageJ.deepCopy(); 122 p.remove("name"); 123 p.add("name", id); 124 p.remove("type"); 125 p.add("type", PackageType.CONFORMANCE.getCode()); 126 p.remove("title"); 127 p.add("title", name); 128 if (notForPublication) { 129 p.add("notForPublication", true); 130 } 131 132 return new NPMPackageGenerator(destFile, p, date, notForPublication); 133 } 134 135 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException { 136 super(); 137 this.destFile = destFile; 138 start(); 139 buildPackageJson(canonical, kind, url, date, ig, fhirVersion, notForPublication); 140 } 141 142 public NPMPackageGenerator(String destFile, JsonObject npm, Date date, boolean notForPublication) throws FHIRException, IOException { 143 super(); 144 String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date); 145 packageJ = npm; 146 packageManifest = new JsonObject(); 147 packageManifest.set("version", npm.asString("version")); 148 packageManifest.set("date", dt); 149 if (notForPublication) { 150 packageManifest.add("notForPublication", true); 151 } 152 npm.set("date", dt); 153 packageManifest.set("name", npm.asString("name")); 154 this.destFile = destFile; 155 start(); 156 String json = JsonParser.compose(npm, true); 157 try { 158 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 159 } catch (UnsupportedEncodingException e) { 160 } 161 } 162 163 private void buildPackageJson(String canonical, PackageType kind, String web, Date date, ImplementationGuide ig, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException { 164 String dtHuman = new SimpleDateFormat("EEE, MMM d, yyyy HH:mmZ", new Locale("en", "US")).format(date); 165 String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date); 166 167 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 168 if (!ig.hasPackageId()) { 169 b.append("packageId"); 170 } 171 if (!ig.hasVersion()) { 172 b.append("version"); 173 } 174 if (!ig.hasFhirVersion()) { 175 b.append("fhirVersion"); 176 } 177 if (!ig.hasLicense()) { 178 b.append("license"); 179 } 180 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 181 if (!d.hasVersion()) { 182 b.append("dependsOn.version("+d.getUri()+")"); 183 } 184 } 185 186 JsonObject npm = new JsonObject(); 187 npm.add("name", ig.getPackageId()); 188 npm.add("version", ig.getVersion()); 189 igVersion = ig.getVersion(); 190 npm.add("tools-version", ToolsVersion.TOOLS_VERSION); 191 npm.add("type", kind.getCode()); 192 npm.add("date", dt); 193 if (ig.hasLicense()) { 194 npm.add("license", ig.getLicense().toCode()); 195 } 196 npm.add("canonical", canonical); 197 if (notForPublication) { 198 npm.add("notForPublication", true); 199 } 200 npm.add("url", web); 201 if (ig.hasTitle()) { 202 npm.add("title", ig.getTitle()); 203 } 204 if (ig.hasDescription()) { 205 npm.add("description", ig.getDescription()+ " (built "+dtHuman+timezone()+")"); 206 } 207 JsonArray vl = new JsonArray(); 208 209 npm.add("fhirVersions", vl); 210 for (String v : fhirVersion) { 211 vl.add(new JsonString(v)); 212 } 213 214 if (kind != PackageType.CORE) { 215 JsonObject dep = new JsonObject(); 216 npm.add("dependencies", dep); 217 for (String v : fhirVersion) { 218 String vp = packageForVersion(v); 219 if (vp != null ) { 220 dep.add(vp, v); 221 } 222 } 223 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 224 dep.add(d.getPackageId(), d.getVersion()); 225 } 226 } 227 if (ig.hasPublisher()) { 228 npm.add("author", ig.getPublisher()); 229 } 230 JsonArray m = new JsonArray(); 231 for (ContactDetail t : ig.getContact()) { 232 String email = email(t.getTelecom()); 233 String url = url(t.getTelecom()); 234 if (t.hasName() & (email != null || url != null)) { 235 JsonObject md = new JsonObject(); 236 m.add(md); 237 md.add("name", t.getName()); 238 if (email != null) 239 md.add("email", email); 240 if (url != null) 241 md.add("url", url); 242 } 243 } 244 if (m.size() > 0) 245 npm.add("maintainers", m); 246 if (ig.getManifest().hasRendering()) 247 npm.add("homepage", ig.getManifest().getRendering()); 248 JsonObject dir = new JsonObject(); 249 npm.add("directories", dir); 250 dir.add("lib", "package"); 251 dir.add("example", "example"); 252 if (ig.hasJurisdiction() && ig.getJurisdiction().size() == 1 && ig.getJurisdictionFirstRep().getCoding().size() == 1) { 253 Coding c = ig.getJurisdictionFirstRep().getCodingFirstRep(); 254 npm.add("jurisdiction", c.getSystem()+"#"+c.getCode()); 255 } 256 String json = JsonParser.compose(npm, true); 257 try { 258 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 259 } catch (UnsupportedEncodingException e) { 260 } 261 packageJ = npm; 262 263 packageManifest = new JsonObject(); 264 packageManifest.add("version", ig.getVersion()); 265 JsonArray fv = new JsonArray(); 266 for (String v : fhirVersion) { 267 fv.add(v); 268 } 269 packageManifest.add("fhirVersion", fv); 270 packageManifest.add("date", dt); 271 packageManifest.add("name", ig.getPackageId()); 272 if (ig.hasJurisdiction() && ig.getJurisdiction().size() == 1 && ig.getJurisdictionFirstRep().getCoding().size() == 1) { 273 Coding c = ig.getJurisdictionFirstRep().getCodingFirstRep(); 274 packageManifest.add("jurisdiction", c.getSystem()+"#"+c.getCode()); 275 } 276 } 277 278 279 private String packageForVersion(String v) { 280 if (v == null) 281 return null; 282 if (v.startsWith("1.0")) 283 return "hl7.fhir.r2.core"; 284 if (v.startsWith("1.4")) 285 return "hl7.fhir.r2b.core"; 286 if (v.startsWith("3.0")) 287 return "hl7.fhir.r3.core"; 288 if (v.startsWith("4.0")) 289 return "hl7.fhir.r4.core"; 290 if (v.startsWith("4.1") || v.startsWith("4.3")) 291 return "hl7.fhir.r4b.core"; 292 return null; 293 } 294 295 private String timezone() { 296 TimeZone tz = TimeZone.getDefault(); 297 Calendar cal = GregorianCalendar.getInstance(tz); 298 int offsetInMillis = tz.getOffset(cal.getTimeInMillis()); 299 300 String offset = String.format("%02d:%02d", Math.abs(offsetInMillis / 3600000), Math.abs((offsetInMillis / 60000) % 60)); 301 offset = (offsetInMillis >= 0 ? "+" : "-") + offset; 302 303 return offset; 304 } 305 306 307 private String url(List<ContactPoint> telecom) { 308 for (ContactPoint cp : telecom) { 309 if (cp.getSystem() == ContactPointSystem.URL) 310 return cp.getValue(); 311 } 312 return null; 313 } 314 315 316 private String email(List<ContactPoint> telecom) { 317 for (ContactPoint cp : telecom) { 318 if (cp.getSystem() == ContactPointSystem.EMAIL) 319 return cp.getValue(); 320 } 321 return null; 322 } 323 324 private void start() throws IOException { 325 OutputStream = new ByteArrayOutputStream(); 326 bufferedOutputStream = new BufferedOutputStream(OutputStream); 327 gzipOutputStream = new GzipCompressorOutputStream(bufferedOutputStream); 328 tar = new TarArchiveOutputStream(gzipOutputStream); 329 indexdb = Utilities.path("[tmp]", "tmp-"+UUID.randomUUID().toString()+".db"); 330 indexer = new NpmPackageIndexBuilder(); 331 indexer.start(indexdb); 332 } 333 334 335 public void addFile(Category cat, String name, byte[] content) throws IOException { 336 String path = cat.getDirectory()+name; 337 if (path.length() > 100) { 338 name = name.substring(0, name.indexOf("-"))+"-"+UUID.randomUUID().toString()+".json"; 339 path = cat.getDirectory()+name; 340 } 341 342 if (created.contains(path)) { 343 System.out.println("Duplicate package file "+path); 344 } else { 345 created.add(path); 346 TarArchiveEntry entry = new TarArchiveEntry(path); 347 entry.setSize(content.length); 348 tar.putArchiveEntry(entry); 349 tar.write(content); 350 tar.closeArchiveEntry(); 351 if(cat == Category.RESOURCE) { 352 indexer.seeFile(name, content); 353 } 354 } 355 } 356 357 public void addFile(String folder, String name, byte[] content) throws IOException { 358 if (!folder.equals("package")) { 359 folder = "package/"+folder; 360 } 361 String path = folder+"/"+name; 362 if (path.length() > 100) { 363 name = name.substring(0, name.indexOf("-"))+"-"+UUID.randomUUID().toString()+".json"; 364 path = folder+"/"+name; 365 } 366 367 if (created.contains(path)) { 368 System.out.println("Duplicate package file "+path); 369 } else { 370 created.add(path); 371 TarArchiveEntry entry = new TarArchiveEntry(path); 372 entry.setSize(content.length); 373 tar.putArchiveEntry(entry); 374 tar.write(content); 375 tar.closeArchiveEntry(); 376 if(folder == "package") { 377 indexer.seeFile(name, content); 378 } 379 } 380 } 381 382 public void finish() throws IOException { 383 buildIndexJson(); 384 tar.finish(); 385 tar.close(); 386 gzipOutputStream.close(); 387 bufferedOutputStream.close(); 388 OutputStream.close(); 389 TextFile.bytesToFile(OutputStream.toByteArray(), destFile); 390 // also, for cache management on current builds, generate a little manifest 391 String json = JsonParser.compose(packageManifest, true); 392 TextFile.stringToFile(json, Utilities.changeFileExt(destFile, ".manifest.json")); 393 } 394 395 private void buildIndexJson() throws IOException { 396 byte[] content = TextFile.stringToBytes(indexer.build()); 397 addFile(Category.RESOURCE, ".index.json", content); 398 content = TextFile.fileToBytes(indexdb); 399 ManagedFileAccess.file(indexdb).delete(); 400 addFile(Category.RESOURCE, ".index.db", content); 401 } 402 403 public String filename() { 404 return destFile; 405 } 406 407 public void loadDir(String rootDir, String name) throws IOException { 408 loadFiles(rootDir, ManagedFileAccess.file(Utilities.path(rootDir, name))); 409 } 410 411 public void loadFiles(String root, File dir, String... noload) throws IOException { 412 for (File f : dir.listFiles()) { 413 if (!Utilities.existsInList(f.getName(), noload)) { 414 if (f.isDirectory()) { 415 loadFiles(root, f); 416 } else { 417 String path = f.getAbsolutePath().substring(root.length()+1); 418 byte[] content = TextFile.fileToBytes(f); 419 if (created.contains(path)) 420 System.out.println("Duplicate package file "+path); 421 else { 422 created.add(path); 423 TarArchiveEntry entry = new TarArchiveEntry(path); 424 entry.setSize(content.length); 425 tar.putArchiveEntry(entry); 426 tar.write(content); 427 tar.closeArchiveEntry(); 428 } 429 } 430 } 431 } 432 } 433 434 public String version() { 435 return igVersion; 436 } 437 438 439}