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