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}