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