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