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.json.model.JsonArray; 066import org.hl7.fhir.utilities.json.model.JsonObject; 067import org.hl7.fhir.utilities.json.model.JsonString; 068import org.hl7.fhir.utilities.json.parser.JsonParser; 069import org.hl7.fhir.utilities.npm.NpmPackageIndexBuilder; 070import org.hl7.fhir.utilities.npm.PackageGenerator.PackageType; 071import org.hl7.fhir.utilities.npm.ToolsVersion; 072 073public class NPMPackageGenerator { 074 075 public enum Category { 076 RESOURCE, EXAMPLE, OPENAPI, SCHEMATRON, RDF, OTHER, TOOL, TEMPLATE, JEKYLL; 077 078 private String getDirectory() { 079 switch (this) { 080 case RESOURCE: return "package/"; 081 case EXAMPLE: return "package/example/"; 082 case OPENAPI: return "package/openapi/"; 083 case SCHEMATRON: return "package/xml/"; 084 case RDF: return "package/rdf/"; 085 case OTHER: return "package/other/"; 086 case TEMPLATE: return "package/other/"; 087 case JEKYLL: return "package/jekyll/"; 088 case TOOL: return "package/bin/"; 089 } 090 return "/"; 091 } 092 } 093 094 private String destFile; 095 private Set<String> created = new HashSet<String>(); 096 private TarArchiveOutputStream tar; 097 private ByteArrayOutputStream OutputStream; 098 private BufferedOutputStream bufferedOutputStream; 099 private GzipCompressorOutputStream gzipOutputStream; 100 private JsonObject packageJ; 101 private JsonObject packageManifest; 102 private NpmPackageIndexBuilder indexer; 103 private String igVersion; 104 105 106 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, boolean notForPublication) throws FHIRException, IOException { 107 super(); 108 this.destFile = destFile; 109 start(); 110 List<String> fhirVersion = new ArrayList<>(); 111 for (Enumeration<FHIRVersion> v : ig.getFhirVersion()) 112 fhirVersion.add(v.asStringValue()); 113 buildPackageJson(canonical, kind, url, date, ig, fhirVersion, notForPublication); 114 } 115 116 public static NPMPackageGenerator subset(NPMPackageGenerator master, String destFile, String id, String name, Date date, boolean notForPublication) throws FHIRException, IOException { 117 JsonObject p = master.packageJ.deepCopy(); 118 p.remove("name"); 119 p.add("name", id); 120 p.remove("type"); 121 p.add("type", PackageType.CONFORMANCE.getCode()); 122 p.remove("title"); 123 p.add("title", name); 124 if (notForPublication) { 125 p.add("notForPublication", true); 126 } 127 128 return new NPMPackageGenerator(destFile, p, date, notForPublication); 129 } 130 131 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, Date date, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException { 132 super(); 133 this.destFile = destFile; 134 start(); 135 buildPackageJson(canonical, kind, url, date, ig, fhirVersion, notForPublication); 136 } 137 138 public NPMPackageGenerator(String destFile, JsonObject npm, Date date, boolean notForPublication) throws FHIRException, IOException { 139 super(); 140 String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date); 141 packageJ = npm; 142 packageManifest = new JsonObject(); 143 packageManifest.set("version", npm.asString("version")); 144 packageManifest.set("date", dt); 145 if (notForPublication) { 146 packageManifest.add("notForPublication", true); 147 } 148 npm.set("date", dt); 149 packageManifest.set("name", npm.asString("name")); 150 this.destFile = destFile; 151 start(); 152 String json = JsonParser.compose(npm, true); 153 try { 154 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 155 } catch (UnsupportedEncodingException e) { 156 } 157 } 158 159 private void buildPackageJson(String canonical, PackageType kind, String web, Date date, ImplementationGuide ig, List<String> fhirVersion, boolean notForPublication) throws FHIRException, IOException { 160 String dtHuman = new SimpleDateFormat("EEE, MMM d, yyyy HH:mmZ", new Locale("en", "US")).format(date); 161 String dt = new SimpleDateFormat("yyyyMMddHHmmss").format(date); 162 163 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 164 if (!ig.hasPackageId()) { 165 b.append("packageId"); 166 } 167 if (!ig.hasVersion()) { 168 b.append("version"); 169 } 170 if (!ig.hasFhirVersion()) { 171 b.append("fhirVersion"); 172 } 173 if (!ig.hasLicense()) { 174 b.append("license"); 175 } 176 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 177 if (!d.hasVersion()) { 178 b.append("dependsOn.version("+d.getUri()+")"); 179 } 180 } 181 182 JsonObject npm = new JsonObject(); 183 npm.add("name", ig.getPackageId()); 184 npm.add("version", ig.getVersion()); 185 igVersion = ig.getVersion(); 186 npm.add("tools-version", ToolsVersion.TOOLS_VERSION); 187 npm.add("type", kind.getCode()); 188 npm.add("date", dt); 189 if (ig.hasLicense()) { 190 npm.add("license", ig.getLicense().toCode()); 191 } 192 npm.add("canonical", canonical); 193 if (notForPublication) { 194 npm.add("notForPublication", true); 195 } 196 npm.add("url", web); 197 if (ig.hasTitle()) { 198 npm.add("title", ig.getTitle()); 199 } 200 if (ig.hasDescription()) { 201 npm.add("description", ig.getDescription()+ " (built "+dtHuman+timezone()+")"); 202 } 203 JsonArray vl = new JsonArray(); 204 205 npm.add("fhirVersions", vl); 206 for (String v : fhirVersion) { 207 vl.add(new JsonString(v)); 208 } 209 210 if (kind != PackageType.CORE) { 211 JsonObject dep = new JsonObject(); 212 npm.add("dependencies", dep); 213 for (String v : fhirVersion) { 214 String vp = packageForVersion(v); 215 if (vp != null ) { 216 dep.add(vp, v); 217 } 218 } 219 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 220 dep.add(d.getPackageId(), d.getVersion()); 221 } 222 } 223 if (ig.hasPublisher()) { 224 npm.add("author", ig.getPublisher()); 225 } 226 JsonArray m = new JsonArray(); 227 for (ContactDetail t : ig.getContact()) { 228 String email = email(t.getTelecom()); 229 String url = url(t.getTelecom()); 230 if (t.hasName() & (email != null || url != null)) { 231 JsonObject md = new JsonObject(); 232 m.add(md); 233 md.add("name", t.getName()); 234 if (email != null) 235 md.add("email", email); 236 if (url != null) 237 md.add("url", url); 238 } 239 } 240 if (m.size() > 0) 241 npm.add("maintainers", m); 242 if (ig.getManifest().hasRendering()) 243 npm.add("homepage", ig.getManifest().getRendering()); 244 JsonObject dir = new JsonObject(); 245 npm.add("directories", dir); 246 dir.add("lib", "package"); 247 dir.add("example", "example"); 248 String json = JsonParser.compose(npm, true); 249 try { 250 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 251 } catch (UnsupportedEncodingException e) { 252 } 253 packageJ = npm; 254 255 packageManifest = new JsonObject(); 256 packageManifest.add("version", ig.getVersion()); 257 JsonArray fv = new JsonArray(); 258 for (String v : fhirVersion) { 259 fv.add(v); 260 } 261 packageManifest.add("fhirVersion", fv); 262 packageManifest.add("date", dt); 263 packageManifest.add("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 addFile(String folder, String name, byte[] content) throws IOException { 346 if (!folder.equals("package")) { 347 folder = "package/"+folder; 348 } 349 String path = folder+"/"+name; 350 if (path.length() > 100) { 351 name = name.substring(0, name.indexOf("-"))+"-"+UUID.randomUUID().toString()+".json"; 352 path = folder+"/"+name; 353 } 354 355 if (created.contains(path)) { 356 System.out.println("Duplicate package file "+path); 357 } else { 358 created.add(path); 359 TarArchiveEntry entry = new TarArchiveEntry(path); 360 entry.setSize(content.length); 361 tar.putArchiveEntry(entry); 362 tar.write(content); 363 tar.closeArchiveEntry(); 364 if(folder == "package") { 365 indexer.seeFile(name, content); 366 } 367 } 368 } 369 370 public void finish() throws IOException { 371 buildIndexJson(); 372 tar.finish(); 373 tar.close(); 374 gzipOutputStream.close(); 375 bufferedOutputStream.close(); 376 OutputStream.close(); 377 TextFile.bytesToFile(OutputStream.toByteArray(), destFile); 378 // also, for cache management on current builds, generate a little manifest 379 String json = JsonParser.compose(packageManifest, true); 380 TextFile.stringToFile(json, Utilities.changeFileExt(destFile, ".manifest.json"), false); 381 } 382 383 private void buildIndexJson() throws IOException { 384 byte[] content = TextFile.stringToBytes(indexer.build(), false); 385 addFile(Category.RESOURCE, ".index.json", content); 386 } 387 388 public String filename() { 389 return destFile; 390 } 391 392 public void loadDir(String rootDir, String name) throws IOException { 393 loadFiles(rootDir, new File(Utilities.path(rootDir, name))); 394 } 395 396 public void loadFiles(String root, File dir, String... noload) throws IOException { 397 for (File f : dir.listFiles()) { 398 if (!Utilities.existsInList(f.getName(), noload)) { 399 if (f.isDirectory()) { 400 loadFiles(root, f); 401 } else { 402 String path = f.getAbsolutePath().substring(root.length()+1); 403 byte[] content = TextFile.fileToBytes(f); 404 if (created.contains(path)) 405 System.out.println("Duplicate package file "+path); 406 else { 407 created.add(path); 408 TarArchiveEntry entry = new TarArchiveEntry(path); 409 entry.setSize(content.length); 410 tar.putArchiveEntry(entry); 411 tar.write(content); 412 tar.closeArchiveEntry(); 413 } 414 } 415 } 416 } 417 } 418 419 public String version() { 420 return igVersion; 421 } 422 423 424}