001package org.hl7.fhir.r4.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 032import java.io.BufferedOutputStream; 033import java.io.ByteArrayOutputStream; 034import java.io.File; 035import java.io.IOException; 036import java.io.UnsupportedEncodingException; 037import java.util.ArrayList; 038import java.util.Calendar; 039import java.util.GregorianCalendar; 040import java.util.HashSet; 041import java.util.List; 042import java.util.Set; 043import java.util.TimeZone; 044 045import org.apache.commons.compress.archivers.tar.TarArchiveEntry; 046import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; 047import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; 048import org.hl7.fhir.exceptions.FHIRException; 049import org.hl7.fhir.r4.model.ContactDetail; 050import org.hl7.fhir.r4.model.ContactPoint; 051import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; 052import org.hl7.fhir.r4.model.Enumeration; 053import org.hl7.fhir.r4.model.Enumerations.FHIRVersion; 054import org.hl7.fhir.r4.model.ImplementationGuide; 055import org.hl7.fhir.r4.model.ImplementationGuide.ImplementationGuideDependsOnComponent; 056import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 057import org.hl7.fhir.utilities.TextFile; 058import org.hl7.fhir.utilities.Utilities; 059import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 060import org.hl7.fhir.utilities.npm.PackageGenerator.PackageType; 061import org.hl7.fhir.utilities.npm.ToolsVersion; 062 063import com.google.gson.Gson; 064import com.google.gson.GsonBuilder; 065import com.google.gson.JsonArray; 066import com.google.gson.JsonObject; 067 068public class NPMPackageGenerator { 069 070 public enum Category { 071 RESOURCE, EXAMPLE, OPENAPI, SCHEMATRON, RDF, OTHER, TOOL, TEMPLATE, JEKYLL; 072 073 private String getDirectory() { 074 switch (this) { 075 case RESOURCE: 076 return "/package/"; 077 case EXAMPLE: 078 return "/example/"; 079 case OPENAPI: 080 return "/openapi/"; 081 case SCHEMATRON: 082 return "/xml/"; 083 case RDF: 084 return "/rdf/"; 085 case OTHER: 086 return "/other/"; 087 case TEMPLATE: 088 return "/other/"; 089 case JEKYLL: 090 return "/jekyll/"; 091 case TOOL: 092 return "/bin/"; 093 } 094 return "/"; 095 } 096 } 097 098 private String destFile; 099 private Set<String> created = new HashSet<String>(); 100 private TarArchiveOutputStream tar; 101 private ByteArrayOutputStream OutputStream; 102 private BufferedOutputStream bufferedOutputStream; 103 private GzipCompressorOutputStream gzipOutputStream; 104 private JsonObject packageJ; 105 106 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, 107 String genDate) throws FHIRException, IOException { 108 super(); 109 System.out.println("create package file at " + destFile); 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, genDate, ig, fhirVersion); 116 } 117 118 public static NPMPackageGenerator subset(NPMPackageGenerator master, String destFile, String id, String name) 119 throws FHIRException, IOException { 120 JsonObject p = master.packageJ.deepCopy(); 121 p.remove("name"); 122 p.addProperty("name", id); 123 p.remove("type"); 124 p.addProperty("type", PackageType.CONFORMANCE.getCode()); 125 p.remove("title"); 126 p.addProperty("title", name); 127 return new NPMPackageGenerator(destFile, p); 128 } 129 130 public NPMPackageGenerator(String destFile, String canonical, String url, PackageType kind, ImplementationGuide ig, 131 String genDate, List<String> fhirVersion) throws FHIRException, IOException { 132 super(); 133 System.out.println("create package file at " + destFile); 134 this.destFile = destFile; 135 start(); 136 buildPackageJson(canonical, kind, url, genDate, ig, fhirVersion); 137 } 138 139 public NPMPackageGenerator(String destFile, JsonObject npm) throws FHIRException, IOException { 140 super(); 141 System.out.println("create package file at " + destFile); 142 this.destFile = destFile; 143 start(); 144 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 145 String json = gson.toJson(npm); 146 try { 147 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 148 } catch (UnsupportedEncodingException e) { 149 } 150 packageJ = npm; 151 } 152 153 private void buildPackageJson(String canonical, PackageType kind, String web, String genDate, ImplementationGuide ig, 154 List<String> fhirVersion) throws FHIRException, IOException { 155 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 156 if (!ig.hasPackageId()) 157 b.append("packageId"); 158 if (!ig.hasVersion()) 159 b.append("version"); 160 if (!ig.hasFhirVersion()) 161 b.append("fhirVersion"); 162 if (!ig.hasLicense()) 163 b.append("license"); 164 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 165 if (!d.hasVersion()) { 166 b.append("dependsOn.version(" + d.getUri() + ")"); 167 } 168 } 169 170 JsonObject npm = new JsonObject(); 171 npm.addProperty("name", ig.getPackageId()); 172 npm.addProperty("version", ig.getVersion()); 173 npm.addProperty("tools-version", ToolsVersion.TOOLS_VERSION); 174 npm.addProperty("type", kind.getCode()); 175 if (ig.hasLicense()) 176 npm.addProperty("license", ig.getLicense().toCode()); 177 npm.addProperty("canonical", canonical); 178 npm.addProperty("url", web); 179 if (ig.hasTitle()) 180 npm.addProperty("title", ig.getTitle()); 181 if (ig.hasDescription()) 182 npm.addProperty("description", ig.getDescription() + " (built " + genDate + timezone() + ")"); 183 if (kind != PackageType.CORE) { 184 JsonObject dep = new JsonObject(); 185 npm.add("dependencies", dep); 186 for (String v : fhirVersion) { // TODO: fix for multiple versions 187 dep.addProperty("hl7.fhir.core", v); 188 } 189 for (ImplementationGuideDependsOnComponent d : ig.getDependsOn()) { 190 dep.addProperty(d.getPackageId(), d.getVersion()); 191 } 192 } 193 if (ig.hasPublisher()) 194 npm.addProperty("author", ig.getPublisher()); 195 JsonArray m = new JsonArray(); 196 for (ContactDetail t : ig.getContact()) { 197 String email = email(t.getTelecom()); 198 String url = url(t.getTelecom()); 199 if (t.hasName() & (email != null || url != null)) { 200 JsonObject md = new JsonObject(); 201 m.add(md); 202 md.addProperty("name", t.getName()); 203 if (email != null) 204 md.addProperty("email", email); 205 if (url != null) 206 md.addProperty("url", url); 207 } 208 } 209 if (m.size() > 0) 210 npm.add("maintainers", m); 211 if (ig.getManifest().hasRendering()) 212 npm.addProperty("homepage", ig.getManifest().getRendering()); 213 JsonObject dir = new JsonObject(); 214 npm.add("directories", dir); 215 dir.addProperty("lib", "package"); 216 dir.addProperty("example", "example"); 217 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 218 String json = gson.toJson(npm); 219 try { 220 addFile(Category.RESOURCE, "package.json", json.getBytes("UTF-8")); 221 } catch (UnsupportedEncodingException e) { 222 } 223 packageJ = npm; 224 } 225 226 private String timezone() { 227 TimeZone tz = TimeZone.getDefault(); 228 Calendar cal = GregorianCalendar.getInstance(tz); 229 int offsetInMillis = tz.getOffset(cal.getTimeInMillis()); 230 231 String offset = String.format("%02d:%02d", Math.abs(offsetInMillis / 3600000), 232 Math.abs((offsetInMillis / 60000) % 60)); 233 offset = (offsetInMillis >= 0 ? "+" : "-") + offset; 234 235 return offset; 236 } 237 238 private String url(List<ContactPoint> telecom) { 239 for (ContactPoint cp : telecom) { 240 if (cp.getSystem() == ContactPointSystem.URL) 241 return cp.getValue(); 242 } 243 return null; 244 } 245 246 private String email(List<ContactPoint> telecom) { 247 for (ContactPoint cp : telecom) { 248 if (cp.getSystem() == ContactPointSystem.EMAIL) 249 return cp.getValue(); 250 } 251 return null; 252 } 253 254 private void start() throws IOException { 255 OutputStream = new ByteArrayOutputStream(); 256 bufferedOutputStream = new BufferedOutputStream(OutputStream); 257 gzipOutputStream = new GzipCompressorOutputStream(bufferedOutputStream); 258 tar = new TarArchiveOutputStream(gzipOutputStream); 259 } 260 261 public void addFile(Category cat, String name, byte[] content) throws IOException { 262 String path = cat.getDirectory() + name; 263 if (created.contains(path)) 264 System.out.println("Duplicate package file " + path); 265 else { 266 created.add(path); 267 TarArchiveEntry entry = new TarArchiveEntry(path); 268 entry.setSize(content.length); 269 tar.putArchiveEntry(entry); 270 tar.write(content); 271 tar.closeArchiveEntry(); 272 } 273 } 274 275 public void finish() throws IOException { 276 tar.finish(); 277 tar.close(); 278 gzipOutputStream.close(); 279 bufferedOutputStream.close(); 280 OutputStream.close(); 281 TextFile.bytesToFile(OutputStream.toByteArray(), destFile); 282 } 283 284 public String filename() { 285 return destFile; 286 } 287 288 public void loadDir(String rootDir, String name) throws IOException { 289 loadFiles(rootDir, ManagedFileAccess.file(Utilities.path(rootDir, name))); 290 } 291 292 public void loadFiles(String root, File dir, String... noload) throws IOException { 293 for (File f : dir.listFiles()) { 294 if (!Utilities.existsInList(f.getName(), noload)) { 295 if (f.isDirectory()) { 296 loadFiles(root, f); 297 } else { 298 String path = f.getAbsolutePath().substring(root.length() + 1); 299 byte[] content = TextFile.fileToBytes(f); 300 if (created.contains(path)) 301 System.out.println("Duplicate package file " + path); 302 else { 303 created.add(path); 304 TarArchiveEntry entry = new TarArchiveEntry(path); 305 entry.setSize(content.length); 306 tar.putArchiveEntry(entry); 307 tar.write(content); 308 tar.closeArchiveEntry(); 309 } 310 } 311 } 312 } 313 } 314 315}