001/*
002Copyright (c) 2011+, HL7, Inc
003All rights reserved.
004
005Redistribution and use in source and binary forms, with or without modification, 
006are permitted provided that the following conditions are met:
007
008 * Redistributions of source code must retain the above copyright notice, this 
009   list of conditions and the following disclaimer.
010 * Redistributions in binary form must reproduce the above copyright notice, 
011   this list of conditions and the following disclaimer in the documentation 
012   and/or other materials provided with the distribution.
013 * Neither the name of HL7 nor the names of its contributors may be used to 
014   endorse or promote products derived from this software without specific 
015   prior written permission.
016
017THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
018ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
019WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
020IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
021INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
022NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
023PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
024WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
025ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
026POSSIBILITY OF SUCH DAMAGE.
027
028 */
029package org.hl7.fhir.r4.test.utils;
030
031/*-
032 * #%L
033 * org.hl7.fhir.r4
034 * %%
035 * Copyright (C) 2014 - 2019 Health Level 7
036 * %%
037 * Licensed under the Apache License, Version 2.0 (the "License");
038 * you may not use this file except in compliance with the License.
039 * You may obtain a copy of the License at
040 * 
041 *      http://www.apache.org/licenses/LICENSE-2.0
042 * 
043 * Unless required by applicable law or agreed to in writing, software
044 * distributed under the License is distributed on an "AS IS" BASIS,
045 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
046 * See the License for the specific language governing permissions and
047 * limitations under the License.
048 * #L%
049 */
050
051
052import java.io.BufferedInputStream;
053import java.io.ByteArrayInputStream;
054import java.io.ByteArrayOutputStream;
055import java.io.File;
056import java.io.FileInputStream;
057import java.io.FileOutputStream;
058import java.io.IOException;
059import java.io.InputStream;
060import java.net.URL;
061import java.util.Collection;
062import java.util.HashMap;
063import java.util.List;
064import java.util.Map;
065import java.util.zip.ZipEntry;
066import java.util.zip.ZipInputStream;
067
068import org.apache.commons.io.FileUtils;
069import org.apache.commons.io.IOUtils;
070import org.apache.commons.lang3.NotImplementedException;
071import org.hl7.fhir.exceptions.FHIRException;
072import org.hl7.fhir.exceptions.FHIRFormatError;
073import org.hl7.fhir.r4.context.SimpleWorkerContext;
074import org.hl7.fhir.r4.formats.IParser.OutputStyle;
075import org.hl7.fhir.r4.formats.JsonParser;
076import org.hl7.fhir.r4.formats.RdfParser;
077import org.hl7.fhir.r4.formats.RdfParserBase;
078import org.hl7.fhir.r4.formats.XmlParser;
079import org.hl7.fhir.r4.model.Constants;
080import org.hl7.fhir.r4.model.Resource;
081import org.hl7.fhir.utilities.CSFile;
082import org.hl7.fhir.utilities.CSFileInputStream;
083import org.hl7.fhir.utilities.TextFile;
084import org.hl7.fhir.utilities.Utilities;
085import org.xmlpull.v1.XmlPullParser;
086import org.xmlpull.v1.XmlPullParserException;
087import org.xmlpull.v1.XmlPullParserFactory;
088
089public class ToolsHelper {
090
091        public static void main(String[] args) {   
092                try {
093                        ToolsHelper self = new ToolsHelper();
094                        if (args.length == 0) 
095                                throw new FHIRException("Missing Command Parameter. Valid Commands: round, json, version, fragments, snapshot-maker");
096                        if (args[0].equals("round")) 
097                                self.executeRoundTrip(args);
098                        else if (args[0].equals("test")) 
099                                self.executeTest(args);
100                        else if (args[0].equals("examples")) 
101                                self.executeExamples(args);
102                        else if (args[0].equals("json")) 
103                                self.executeJson(args);
104                        else if (args[0].equals("cxml")) 
105                                self.executeCanonicalXml(args);
106                        else if (args[0].equals("version")) 
107                                self.executeVersion(args);
108                        else if (args[0].equals("fragments")) 
109                                self.executeFragments(args);
110                        else if (args[0].equals("snapshot-maker")) 
111                                self.generateSnapshots(args);
112                        else 
113                                throw new FHIRException("Unknown command '"+args[0]+"'. Valid Commands: round, test, examples, json, cxml, version, fragments, snapshot-maker");
114                } catch (Throwable e) {
115                        try {
116                                e.printStackTrace();
117                                TextFile.stringToFile(e.toString(), (args.length == 0 ? "tools" : args[0])+".err");
118                        } catch (Exception e1) {
119                                e1.printStackTrace();
120                        }
121                }
122        }
123
124        private void executeExamples(String[] args) throws IOException {
125                try {
126                        @SuppressWarnings("unchecked")
127                        List<String> lines = FileUtils.readLines(new File(args[1]), "UTF-8");
128                        String srcDir = lines.get(0);
129                        lines.remove(0);
130                        processExamples(srcDir, lines);
131                        TextFile.stringToFile("ok", Utilities.changeFileExt(args[1], ".out"));
132                } catch (Exception e) {
133                        TextFile.stringToFile(e.getMessage(), Utilities.changeFileExt(args[1], ".out"));                        
134                }
135        }
136
137        private void generateSnapshots(String[] args) throws IOException, FHIRException {
138                if (args.length == 1) {
139                        System.out.println("tools.jar snapshot-maker [source] -defn [definitions]");
140                        System.out.println("");
141                        System.out.println("Generates a snapshot from a differential. The nominated profile must have a single struture that has a differential");
142                        System.out.println("");
143                        System.out.println("source - the profile to generate the snapshot for. Maybe a file name, or a URL reference to a server running FHIR RESTful API");
144                        System.out.println("definitions - filename for local copy of the validation.zip file");                 
145                }
146                String address = args[1];
147                String definitions = args[3];
148
149                SimpleWorkerContext context = SimpleWorkerContext.fromDefinitions(getDefinitions(definitions));
150
151                //    if (address.startsWith("http:") || address.startsWith("http:")) {
152                //      // this is on a restful interface
153                //      String[] parts = address.split("\\/Profile\\/");
154                //      if (parts.length != 2)
155                //              throw new FHIRException("Unable to understand address of profile");
156                //      StructureDefinition profile = context.fetchResource(StructureDefinition.class, parts[1]);
157                //      ProfileUtilities utils = new ProfileUtilities(context);
158                //      StructureDefinition base = utils.getProfile(profile, profile.getBase());
159                //      if (base == null)
160                //              throw new FHIRException("Unable to resolve profile "+profile.getBase());
161                //      utils.generateSnapshot(base, profile, address, profile.getName(), null, null);
162                //      // client.update(StructureDefinition.class, profile, parts[1]);
163                //    } else {
164                throw new NotImplementedException("generating snapshots not done yet (address = "+address+")");
165                //    }
166        }
167
168        private Map<String, byte[]> getDefinitions(String definitions) throws IOException, FHIRException {
169                Map<String, byte[]> results = new HashMap<String, byte[]>();
170                readDefinitions(results, loadDefinitions(definitions));
171                return results;
172        }
173
174        private void readDefinitions(Map<String, byte[]> map, byte[] defn) throws IOException {
175                ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(defn));
176                ZipEntry ze;
177                while ((ze = zip.getNextEntry()) != null) {
178                        if (!ze.getName().endsWith(".zip") && !ze.getName().endsWith(".jar") ) { // skip saxon .zip
179                                String name = ze.getName();
180                                InputStream in = zip;
181                                ByteArrayOutputStream b = new ByteArrayOutputStream();
182                                int n;
183                                byte[] buf = new byte[1024];
184                                while ((n = in.read(buf, 0, 1024)) > -1) {
185                                        b.write(buf, 0, n);
186                                }        
187                                map.put(name, b.toByteArray());
188                        }
189                        zip.closeEntry();
190                }
191                zip.close();    
192        }
193
194        private byte[] loadDefinitions(String definitions) throws FHIRException, IOException {
195                byte[] defn;
196                //    if (Utilities.noString(definitions)) {
197                //      defn = loadFromUrl(MASTER_SOURCE);
198                //    } else 
199                if (definitions.startsWith("https:") || definitions.startsWith("http:")) {
200                        defn = loadFromUrl(definitions);
201                } else if (new File(definitions).exists()) {
202                        defn = loadFromFile(definitions);      
203                } else
204                        throw new FHIRException("Unable to find FHIR validation Pack (source = "+definitions+")");
205                return defn;
206        }
207
208        private byte[] loadFromUrl(String src) throws IOException {
209                URL url = new URL(src);
210                byte[] str = IOUtils.toByteArray(url.openStream());
211                return str;
212        }
213
214        private byte[] loadFromFile(String src) throws IOException {
215                FileInputStream in = new FileInputStream(src);
216                byte[] b = new byte[in.available()];
217                in.read(b);
218                in.close();
219                return b;
220        }
221
222
223        protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
224                BufferedInputStream input = new BufferedInputStream(stream);
225                XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
226                factory.setNamespaceAware(true);
227                XmlPullParser xpp = factory.newPullParser();
228                xpp.setInput(input, "UTF-8");
229                xpp.next();
230                return xpp;
231        }
232
233        protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
234                int eventType = xpp.getEventType();
235                while (eventType == XmlPullParser.TEXT && xpp.isWhitespace())
236                        eventType = xpp.next();
237                return eventType;
238        }
239
240        public void executeFragments(String[] args) throws IOException {
241                try {
242                        File source = new CSFile(args[1]);
243                        if (!source.exists())        
244                                throw new FHIRException("Source File \""+source.getAbsolutePath()+"\" not found");
245                        XmlPullParser xpp = loadXml(new FileInputStream(source));
246                        nextNoWhitespace(xpp);
247                        if (!xpp.getName().equals("tests"))
248                                throw new FHIRFormatError("Unable to parse file - starts with "+xpp.getName());
249                        xpp.next();
250                        nextNoWhitespace(xpp);
251                        StringBuilder s = new StringBuilder();
252                        s.append("<results>\r\n");
253                        int fail = 0;
254                        while (xpp.getEventType() == XmlPullParser.START_TAG && xpp.getName().equals("test")) {
255                                String id = xpp.getAttributeValue(null, "id");
256                                String type = xpp.getAttributeValue(null, "type");
257                                // test
258                                xpp.next();
259                                nextNoWhitespace(xpp);
260                                // pre
261                                xpp.next();
262                                nextNoWhitespace(xpp);
263                                XmlParser p = new XmlParser();
264                                try {
265                                        p.parseFragment(xpp, type);
266                                        s.append("<result id=\""+id+"\" outcome=\"ok\"/>\r\n");
267                                        nextNoWhitespace(xpp);
268                                } catch (Exception e) {
269                                        s.append("<result id=\""+id+"\" outcome=\"error\" msg=\""+Utilities.escapeXml(e.getMessage())+"\"/>\r\n");
270                                        fail++;
271                                }
272                                while (xpp.getEventType() != XmlPullParser.END_TAG || !xpp.getName().equals("pre")) 
273                                        xpp.next();
274                                xpp.next();
275                                nextNoWhitespace(xpp);
276                                xpp.next();
277                                nextNoWhitespace(xpp);
278                        }
279                        s.append("</results>\r\n");
280
281                        TextFile.stringToFile(s.toString(), args[2]);
282                } catch (Exception e) {
283                        e.printStackTrace();
284                        TextFile.stringToFile(e.getMessage(), args[2]);
285                }
286        }
287
288        public void executeRoundTrip(String[] args) throws IOException, FHIRException {
289                FileInputStream in;
290                File source = new CSFile(args[1]);
291                File dest = new CSFile(args[2]);
292                if (args.length >= 4) {
293                        Utilities.copyFile(args[1], args[3]);
294                }
295
296                if (!source.exists())        
297                        throw new FHIRException("Source File \""+source.getAbsolutePath()+"\" not found");
298                in = new CSFileInputStream(source);
299                XmlParser p = new XmlParser();
300                JsonParser parser = new JsonParser();
301                JsonParser pj = parser;
302                Resource rf = p.parse(in);
303                ByteArrayOutputStream json = new ByteArrayOutputStream();
304                parser.setOutputStyle(OutputStyle.PRETTY);
305                parser.compose(json, rf);
306                json.close();
307                TextFile.stringToFile(new String(json.toByteArray()), Utilities.changeFileExt(dest.getAbsolutePath(), ".json"));
308                rf = pj.parse(new ByteArrayInputStream(json.toByteArray()));
309                FileOutputStream s = new FileOutputStream(dest);
310                new XmlParser().compose(s, rf, true);
311                s.close();
312        }
313
314        public String executeJson(String[] args) throws IOException, FHIRException {
315                FileInputStream in;
316                File source = new CSFile(args[1]);
317                File dest = new CSFile(args[2]);
318                File destc = new CSFile(Utilities.changeFileExt(args[2], ".canonical.json"));
319                File destt = new CSFile(args[2]+".tmp");
320                File destr = new CSFile(Utilities.changeFileExt(args[2], ".ttl"));
321
322                if (!source.exists())        
323                        throw new FHIRException("Source File \""+source.getAbsolutePath()+"\" not found");
324                in = new CSFileInputStream(source);
325                XmlParser p = new XmlParser();
326                Resource rf = p.parse(in);
327                JsonParser json = new JsonParser();
328                json.setOutputStyle(OutputStyle.PRETTY);
329                FileOutputStream s = new FileOutputStream(dest);
330                json.compose(s, rf);
331                s.close();
332                json.setOutputStyle(OutputStyle.CANONICAL);
333                s = new FileOutputStream(destc);
334                json.compose(s, rf);
335                s.close();
336                json.setSuppressXhtml("Snipped for Brevity");
337                json.setOutputStyle(OutputStyle.PRETTY);
338                s = new FileOutputStream(destt);
339                json.compose(s, rf);
340                s.close();    
341
342                RdfParserBase rdf = new RdfParser();
343                s = new FileOutputStream(destr);
344                rdf.compose(s, rf);
345                s.close();
346                
347                return TextFile.fileToString(destt.getAbsolutePath());
348        }
349
350        public void executeCanonicalXml(String[] args) throws FHIRException, IOException {
351                FileInputStream in;
352                File source = new CSFile(args[1]);
353                File dest = new CSFile(args[2]);
354
355                if (!source.exists())        
356                        throw new FHIRException("Source File \""+source.getAbsolutePath()+"\" not found");
357                in = new CSFileInputStream(source);
358                XmlParser p = new XmlParser();
359                Resource rf = p.parse(in);
360                XmlParser cxml = new XmlParser();
361                cxml.setOutputStyle(OutputStyle.NORMAL);
362                cxml.compose(new FileOutputStream(dest), rf);
363        }
364
365        private void executeVersion(String[] args) throws IOException {
366                TextFile.stringToFile(org.hl7.fhir.r4.utils.Version.VERSION+":"+Constants.VERSION, args[1]);
367        }
368
369        public void processExamples(String rootDir, Collection<String> list) throws FHIRException  {
370                for (String n : list) {
371                        try {
372                                String filename = rootDir + n + ".xml";
373                                // 1. produce canonical XML
374                                CSFileInputStream source = new CSFileInputStream(filename);
375                                FileOutputStream dest = new FileOutputStream(Utilities.changeFileExt(filename, ".canonical.xml"));
376                                XmlParser p = new XmlParser();
377                                Resource r = p.parse(source);
378                                XmlParser cxml = new XmlParser();
379                                cxml.setOutputStyle(OutputStyle.CANONICAL);
380                                cxml.compose(dest, r);
381
382                                // 2. produce JSON
383                                source = new CSFileInputStream(filename);
384                                dest = new FileOutputStream(Utilities.changeFileExt(filename, ".json"));
385                                r = p.parse(source);
386                                JsonParser json = new JsonParser();
387                                json.setOutputStyle(OutputStyle.PRETTY);
388                                json.compose(dest, r);
389                                json = new JsonParser();
390                                json.setOutputStyle(OutputStyle.CANONICAL);
391                                dest = new FileOutputStream(Utilities.changeFileExt(filename, ".canonical.json"));
392                                json.compose(dest, r);
393                                
394                                // 2. produce JSON
395                                dest = new FileOutputStream(Utilities.changeFileExt(filename, ".ttl"));
396                                RdfParserBase rdf = new RdfParser();
397                                rdf.compose(dest, r);
398                        } catch (Exception e) {
399                                e.printStackTrace();
400                                throw new FHIRException("Error Processing "+n+".xml: "+e.getMessage(), e);
401                        }
402                }
403        }
404
405        public void testRoundTrip(String rootDir, String tmpDir, Collection<String> names) throws Throwable {
406                try {
407                        System.err.println("Round trip from "+rootDir+" to "+tmpDir+":"+Integer.toString(names.size())+" files");
408                        for (String n : names) {
409                                System.err.print("  "+n);
410                                String source = rootDir + n + ".xml";
411                                // String tmpJson = tmpDir + n + ".json";
412                                String tmp = tmpDir + n.replace(File.separator, "-") + ".tmp";
413                                String dest = tmpDir + n.replace(File.separator, "-") + ".java.xml";
414
415                                FileInputStream in = new FileInputStream(source);
416                                XmlParser xp = new XmlParser();
417                                Resource r = xp.parse(in);
418                                System.err.print(".");
419                                JsonParser jp = new JsonParser();
420                                FileOutputStream out = new FileOutputStream(tmp);
421                                jp.setOutputStyle(OutputStyle.PRETTY);
422                                jp.compose(out, r);
423                                out.close();
424                                r = null;
425                                System.err.print(".");
426
427                                in = new FileInputStream(tmp);
428                                System.err.print(",");
429                                r = jp.parse(in);
430                                System.err.print(".");
431                                out = new FileOutputStream(dest);
432                                new XmlParser().compose(out, r, true);
433                                System.err.println("!");
434                                out.close();
435                                r = null;
436                                System.gc();
437                        }
438                } catch (Throwable e) {
439                        System.err.println("Error: "+e.getMessage());
440                        throw e;
441                }
442        }
443
444        private void executeTest(String[] args) throws Throwable {
445                try {
446                        @SuppressWarnings("unchecked")
447                        List<String> lines = FileUtils.readLines(new File(args[1]), "UTF-8");
448                        String srcDir = lines.get(0);
449                        lines.remove(0);
450                        String dstDir = lines.get(0).trim();
451                        lines.remove(0);
452                        testRoundTrip(srcDir, dstDir, lines);
453                        TextFile.stringToFile("ok", Utilities.changeFileExt(args[1], ".out"));
454                } catch (Exception e) {
455                        TextFile.stringToFile(e.getMessage(), Utilities.changeFileExt(args[1], ".out"));                        
456                }
457        }
458}