001package org.hl7.fhir.r5.elementmodel;
002
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.OutputStream;
008import java.net.UnknownHostException;
009import java.text.ParseException;
010import java.time.Duration;
011import java.time.LocalDateTime;
012import java.time.ZoneId;
013import java.time.ZoneOffset;
014import java.util.ArrayList;
015import java.util.Arrays;
016import java.util.Base64;
017import java.util.HashMap;
018import java.util.List;
019import java.util.Map;
020import java.util.Map.Entry;
021import java.util.zip.DataFormatException;
022import java.util.Date;
023import java.util.zip.Inflater;
024
025import org.hl7.fhir.exceptions.DefinitionException;
026import org.hl7.fhir.exceptions.FHIRException;
027import org.hl7.fhir.exceptions.FHIRFormatError;
028import org.hl7.fhir.r5.context.IWorkerContext;
029import org.hl7.fhir.r5.formats.IParser.OutputStyle;
030import org.hl7.fhir.r5.test.utils.TestingUtilities;
031import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
032import org.hl7.fhir.utilities.TextFile;
033import org.hl7.fhir.utilities.Utilities;
034import org.hl7.fhir.utilities.VersionUtilities;
035import org.hl7.fhir.utilities.http.HTTPResult;
036import org.hl7.fhir.utilities.http.ManagedWebAccess;
037import org.hl7.fhir.utilities.json.model.JsonArray;
038import org.hl7.fhir.utilities.json.model.JsonElement;
039import org.hl7.fhir.utilities.json.model.JsonElementType;
040import org.hl7.fhir.utilities.json.model.JsonObject;
041import org.hl7.fhir.utilities.json.model.JsonPrimitive;
042import org.hl7.fhir.utilities.json.model.JsonProperty;
043import org.hl7.fhir.utilities.validation.ValidationMessage;
044import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
045import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
046
047import com.nimbusds.jose.JWEObject;
048import com.nimbusds.jose.Payload;
049import com.nimbusds.jose.crypto.DirectDecrypter;
050
051/**
052 * this class is actually a smart health link retreiver. 
053 * It's going to parse the link, check it, and then return
054 * 2 items, the link with validation information in an Element, and the 
055 * parsed whatever that the link pointed to
056 * 
057 * Error locations in the first item are in the decoded JSON file the URL contains, or 0,0 if not in the json file
058 * Error locations in the second item are in the decoded payload
059 * 
060 * @author grahame
061 *
062 */
063public class SHLParser extends ParserBase {
064  private static boolean testMode;
065  
066  private boolean post = true;
067  private String url = null;
068  private byte[] key = null;
069  private String ct;
070
071  public SHLParser(IWorkerContext context) {
072    super(context);
073  }
074
075  public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRFormatError, DefinitionException, FHIRException {
076    byte[] content = TextFile.streamToBytes(inStream);
077    
078    List<ValidatedFragment> res = new ArrayList<>();
079    ValidatedFragment shl = addNamedElement(res, "shl", "txt", content);
080    String src = TextFile.bytesToString(content);
081    
082    if (src.startsWith("shlink:/")) {
083      src = src.substring(8);
084    } else if (src.contains("#shlink:/")) {
085      String pfx = src.substring(0, src.indexOf("#shlink:/"));
086      src = src.substring(src.indexOf("#shlink:/")+9);
087      if (!Utilities.isAbsoluteUrlLinkable(pfx)) {        
088        logError(shl.getErrors(), "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "if a prefix is present, it must be a URL, not "+pfx, IssueSeverity.ERROR);                
089      }
090    } else {
091      logError(shl.getErrors(), "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "This content does not appear to be an Smart Health Link", IssueSeverity.ERROR);
092      src = null;
093    }
094    if (src != null) {
095      byte[] cntin = Base64.getUrlDecoder().decode(src);
096      ValidatedFragment json = addNamedElement(res, "json", "json", cntin);
097      JsonObject j = null;
098      try {
099        j = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(cntin);
100      } catch (Exception e) {
101        logError(json.getErrors(), "202-08-31", 1, 1, "shl.json", IssueType.STRUCTURE, "The JSON is not valid: "+e.getMessage(), IssueSeverity.ERROR);        
102      }
103      if (j != null) {
104        byte[] cntout = org.hl7.fhir.utilities.json.parser.JsonParser.composeBytes(j, false);
105        if (!Arrays.equals(cntin, cntout)) {
106          logError(shl.getErrors(), "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "The JSON does not seem to be minified properly", IssueSeverity.ERROR);        
107        }
108        if (checkJson(json.getErrors(), j)) {
109          HTTPResult cnt = null;
110          if (post) {
111            try {
112              cnt = fetchManifest();
113            } catch (UnknownHostException e) {
114              logError(json.getErrors(), "202-08-31", 1, 1, "shl.json", IssueType.STRUCTURE, "The manifest could not be fetched because the host "+e.getMessage()+" is unknown", IssueSeverity.ERROR);
115            } catch (Exception e) {
116              logError(json.getErrors(), "202-08-31", 1, 1, "shl.json", IssueType.STRUCTURE, "The manifest could not be fetched: "+e.getMessage(), IssueSeverity.ERROR);
117            }
118            if (cnt != null) {
119              if (cnt.getContentType() == null) {
120                logError(json.getErrors(), "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.NOTFOUND, "The server did not return a Content-Type header - should be 'application/json'", IssueSeverity.WARNING);
121              } else if (!"application/json".equals(cnt.getContentType())) {
122                logError(json.getErrors(), "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.STRUCTURE, "The server returned the wrong Content-Type header '"+cnt.getContentType()+"' - must be 'application/json'", IssueSeverity.ERROR);
123              }
124              checkManifest(res, cnt);
125            } 
126          } else {
127            try {
128              cnt = fetchFile(url+"?recipient=FHIR%20Validator", "application/jose"); 
129            } catch (Exception e) {
130              logError(json.getErrors(), "202-08-31", 1, 1, "shl,json.url", IssueType.STRUCTURE, "The document could not be fetched: "+e.getMessage(), IssueSeverity.ERROR);
131            }
132            if (cnt != null) {
133              if (cnt.getContentType() == null) {
134                logError(json.getErrors(), "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.NOTFOUND, "The server did not return a Content-Type header - should be 'application/jose'", IssueSeverity.WARNING);
135              } else if (!"application/json".equals(cnt.getContentType())) {
136                logError(json.getErrors(), "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.STRUCTURE, "The server returned the wrong Content-Type header '"+cnt.getContentType()+"' - must be 'application/jose'", IssueSeverity.ERROR);
137              }
138              processContent(res, json.getErrors(), "shl.url.fetched()", "document", cnt.getContentAsString(), ct);
139            } 
140          }
141        }
142      }
143    }
144    return res;
145  }
146  
147
148  private void checkManifest(List<ValidatedFragment> res, HTTPResult cnt) throws IOException {
149    ValidatedFragment manifest = addNamedElement(res, "manifest", "json", cnt.getContent());
150    
151    if (!cnt.getContentType().equals("application/json")) {
152      logError(manifest.getErrors(), "202-08-31", 1, 1, "manifest", IssueType.STRUCTURE, "The mime type should be application/json not "+cnt.getContentType(), IssueSeverity.ERROR);
153    } else {
154      JsonObject j = null;
155      try {
156        j = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(cnt.getContent());
157      } catch (Exception e) {
158        logError(manifest.getErrors(), "202-08-31", 1, 1, "manifest", IssueType.STRUCTURE, "The JSON is not valid: "+e.getMessage(), IssueSeverity.ERROR);        
159      }
160      if (j != null) {
161        for (JsonProperty p : j.getProperties()) {
162         if (!p.getName().equals("files")) {
163            logError(manifest.getErrors(), "202-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "manifest."+p.getName(), 
164                IssueType.STRUCTURE, "Unexpected property name "+p.getName(), IssueSeverity.WARNING);  
165          }
166        }
167      }
168      if (j.has("files")) {
169        JsonElement f = j.get("files");
170        if (f.isJsonArray()) {
171          int i = 0;
172          for (JsonElement e : f.asJsonArray()) {
173            if (e.isJsonObject()) {
174              processManifestEntry(res, manifest.getErrors(), e.asJsonObject(), "manifest.files["+i+"]", "files["+i+"]");
175            } else {
176              logError(manifest.getErrors(), "202-08-31", e.getStart().getLine(), e.getStart().getCol(), "manifest.files["+i+"]", 
177                  IssueType.STRUCTURE, "files must be an object, not a "+f.type().name(), IssueSeverity.ERROR);  
178            }
179          }
180        } else {
181          logError(manifest.getErrors(), "202-08-31", f.getStart().getLine(), f.getStart().getCol(), "manifest.files", 
182              IssueType.STRUCTURE, "files must be an array, not a "+f.type().name(), IssueSeverity.ERROR);  
183        }
184      } else {
185        logError(manifest.getErrors(), "202-08-31", j.getStart().getLine(), j.getStart().getCol(), "manifest", 
186            IssueType.STRUCTURE, "files not found", IssueSeverity.WARNING);  
187      }
188    }
189  }
190
191  private void processManifestEntry(List<ValidatedFragment> res, List<ValidationMessage> errors, JsonObject j, String path, String name) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
192    for (JsonProperty p : j.getProperties()) {
193      if (!Utilities.existsInList(p.getName(), "contentType", "location", "embedded")) {
194        logError(errors, "202-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "manifest."+p.getName(), 
195            IssueType.STRUCTURE, "Unexpected property "+p.getName(), IssueSeverity.WARNING);  
196      }
197    }
198    JsonElement cte = j.get("contentType");
199    JsonElement loce = j.get("location");
200    JsonElement embe = j.get("embedded");
201    String ct = null;
202    if (cte == null) {
203      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path, IssueType.STRUCTURE, "contentType not found", IssueSeverity.ERROR);  
204    } else if (!cte.isJsonString()) {
205      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path+".contentType", IssueType.STRUCTURE, "contentType must be a string not a "+cte.type().name(), IssueSeverity.ERROR);       
206    } else { 
207      ct = cte.asString();
208      if (!Utilities.existsInList(ct, "application/smart-health-card", "application/smart-api-access", "application/fhir+json")) {
209        logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path+".contentType", IssueType.STRUCTURE, "contentType must be one of application/smart-health-card, application/smart-api-access or application/fhir+json", IssueSeverity.ERROR);       
210        ct = null;
211      }
212    }
213    if (loce != null && !loce.isJsonString()) {
214      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path+".location", IssueType.STRUCTURE, "location must be a string not a "+loce.type().name(), IssueSeverity.ERROR);       
215    } 
216    if (embe != null && !embe.isJsonString()) {
217      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path+".embedded", IssueType.STRUCTURE, "embedded must be a string not a "+embe.type().name(), IssueSeverity.ERROR);       
218    }
219    if (loce == null && embe == null) {
220      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path, IssueType.STRUCTURE, "Found neither a location nor an embedded property", IssueSeverity.ERROR);  
221    } else if (loce != null && embe != null) {
222      logError(errors, "202-08-31", j.getStart().getLine(), j.getStart().getCol(), path, IssueType.STRUCTURE, "Found both a location nor an embedded property - only one can be present", IssueSeverity.ERROR);  
223    } else if (ct != null) {
224      if (embe != null) {
225        processContent(res, errors, path+".embedded", name, embe.asString(), ct);
226      } else if (loce != null) { // it will be, just removes a warning
227        HTTPResult cnt = null;
228        try {
229          cnt = fetchFile(loce.asString(), "application/jose"); 
230        } catch (Exception e) {
231          logError(errors, "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "The document could not be fetched: "+e.getMessage(), IssueSeverity.ERROR);
232        }
233        if (cnt != null) {
234          if (cnt.getContentType() == null) {
235            logError(errors, "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.NOTFOUND, "The server did not return a Content-Type header - should be 'application/jose'", IssueSeverity.WARNING);
236          } else if (!"application/json".equals(cnt.getContentType())) {
237            logError(errors, "202-08-31", 1, 1, "shl.json.url.fetch()", IssueType.STRUCTURE, "The server returned the wrong Content-Type header '"+cnt.getContentType()+"' - must be 'application/jose'", IssueSeverity.ERROR);
238          }
239          processContent(res, errors, path+".url.fetch()", name, cnt.getContentAsString(), ct);            
240        } 
241      } 
242    }
243  }
244
245  private void processContent(List<ValidatedFragment> res, List<ValidationMessage> errors, String path, String name, String jose, String ct) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
246    ValidatedFragment bin = addNamedElement(res, "encrypted", "jose", TextFile.stringToBytes(jose));
247    byte[] cnt = null;
248    JWEObject jwe;
249    try {
250      jwe = JWEObject.parse(jose);
251      jwe.decrypt(new DirectDecrypter(key));
252      cnt = jwe.getPayload().toBytes();
253    } catch (Exception e) {
254      logError(bin.getErrors(), "202-08-31", 1, 1, path, IssueType.STRUCTURE, "Decruption failed: "+e.getMessage(), IssueSeverity.ERROR);    
255    }
256    if (cnt != null) {
257      switch (ct) {
258      case "application/smart-health-card":
259        //a JSON file with a .verifiableCredential array containing SMART Health Card JWS strings, as specified by https://spec.smarthealth.cards#via-file-download.
260        SHCParser shc = new SHCParser(context);
261        res.addAll(shc.parse(new ByteArrayInputStream(cnt)));
262        break;
263      case "application/fhir+json": 
264        ValidatedFragment doc = addNamedElement(res, name, "json", cnt);
265        // a JSON file containing any FHIR resource (e.g., an individual resource or a Bundle of resources). Generally this format may not be tamper-proof.
266        logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "Processing content of type 'application/smart-api-access' is not done yet", IssueSeverity.INFORMATION);
267        break;
268      case "application/smart-api-access":
269        doc = addNamedElement(res, name, "api.json", cnt);
270        // a JSON file with a SMART Access Token Response (see SMART App Launch). Two additional properties are defined:
271        // aud Required string indicating the FHIR Server Base URL where this token can be used (e.g., "https://server.example.org/fhir")
272        // query: Optional array of strings acting as hints to the client, indicating queries it might want to make (e.g., ["Coverage?patient=123&_tag=family-insurance"])
273        logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "Processing content of type 'application/smart-api-access' is not done yet", IssueSeverity.INFORMATION);
274        break;
275      default: 
276        doc = addNamedElement(res, name, "bin", cnt);
277        logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "The Content-Type '"+ct+"' is not known", IssueSeverity.INFORMATION);
278      }
279    }
280  }
281
282  private ValidatedFragment addNamedElement(List<ValidatedFragment> res, String name, String type, byte[] content) {
283    ValidatedFragment result = new ValidatedFragment(name, type, content, true);
284    res.add(result);
285    return result;
286  }
287
288
289  private HTTPResult fetchFile(String url, String ct) throws IOException {
290    HTTPResult res = ManagedWebAccess.get(url, ct);
291    res.checkThrowException();
292    return res;
293  }
294  
295  private HTTPResult fetchManifest() throws IOException {
296    if (testMode) {
297      return new HTTPResult(url, 200, "OK", "application/json", TextFile.streamToBytes(TestingUtilities.loadTestResourceStream("validator", "shlink.manifest.json")));
298    }
299
300    JsonObject j = new JsonObject();
301    j.add("recipient", "FHIR Validator");
302    HTTPResult res = ManagedWebAccess.post(url, org.hl7.fhir.utilities.json.parser.JsonParser.composeBytes(j), "application/json", "application/json");        
303    res.checkThrowException();
304    return res;
305  }
306
307  private boolean checkJson(List<ValidationMessage> errors, JsonObject j) {
308    boolean ok = true;
309    boolean fUrl = false;
310    boolean fKey = false;
311    boolean fCty = false;
312    boolean hp = false;
313    boolean hu = false;
314    for (JsonProperty p : j.getProperties()) {
315      switch (p.getName()) {
316      case "url":
317        fUrl = true;
318        if (!p.getValue().isJsonString()) {
319          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
320              IssueType.STRUCTURE, "url must be a string", IssueSeverity.ERROR);   
321          ok = false;
322        } else if (!Utilities.isAbsoluteUrlLinkable(p.getValue().asString())) {
323          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
324              IssueType.STRUCTURE, "url is not valid: "+p.getValue().asString(), IssueSeverity.ERROR);  
325          ok = false;
326        } else {
327          url = p.getValue().asString();
328        }
329        break;
330      case "key":
331        fKey = true;
332        if (!p.getValue().isJsonString()) {
333          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
334              IssueType.STRUCTURE, "key must be a string", IssueSeverity.ERROR);   
335          ok = false;
336        } else if (p.getValue().asString().length() != 43) {
337          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
338              IssueType.STRUCTURE, "key must contain 43 chars", IssueSeverity.ERROR);  
339          ok = false;
340        } else {
341          key = Base64.getUrlDecoder().decode(p.getValue().asString());
342        }
343        break;
344      case "exp":
345        if (!p.getValue().isJsonNumber()) {
346          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
347              IssueType.STRUCTURE, "exp must be a number", IssueSeverity.ERROR);   
348        } else if (!Utilities.isDecimal(p.getValue().asJsonNumber().getValue(), false)) {
349          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
350              IssueType.STRUCTURE, "exp must be a valid number", IssueSeverity.ERROR);   
351        } else {
352          String v = p.getValue().asJsonNumber().getValue();
353          if (v.contains(".")) {
354            v = v.substring(0, v.indexOf("."));
355          }
356
357          long epochSecs = Long.valueOf(v);
358          LocalDateTime date = LocalDateTime.ofEpochSecond(epochSecs, 0, ZoneOffset.UTC);
359          LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
360          Duration duration = Duration.between(date, now);
361        
362          if (date.isBefore(now)) {          
363            logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
364              IssueType.STRUCTURE, "The content has expired (by "+Utilities.describeDuration(duration)+")", IssueSeverity.WARNING);  
365          }
366        }
367        break;
368      case "flag":
369        if (!p.getValue().isJsonString()) {
370          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
371              IssueType.STRUCTURE, "flag must be a string", IssueSeverity.ERROR);   
372        } else {
373          String flag = p.getValue().asString();
374          for (char c : flag.toCharArray()) {
375            switch (c) {
376            case 'L': // ok
377              break;
378            case 'P':
379              hp = true;
380              break;
381            case 'U':
382              hu = true;
383              break;
384            default:
385              logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
386                  IssueType.STRUCTURE, "Illegal Character "+c+" in flag", IssueSeverity.ERROR);   
387            }
388          }
389          if (hu && hp) {
390            logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
391                IssueType.STRUCTURE, "Illegal combination in flag: both P and U are present", IssueSeverity.ERROR);               
392          }
393          if (hp) {
394            logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
395                IssueType.BUSINESSRULE, "The validator is unable to retrieve the content referred to by the URL because a password is required", IssueSeverity.INFORMATION);  
396            ok = false;
397          }
398          if (hu) {
399            post = false;
400          }
401        }
402        break;
403      case "label":
404        if (!p.getValue().isJsonString()) {
405          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
406              IssueType.STRUCTURE, "label must be a string", IssueSeverity.ERROR);   
407        } else if (p.getValue().asString().length() > 80) {
408          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
409              IssueType.STRUCTURE, "label must be no longer than 80 chars", IssueSeverity.ERROR);   
410        }
411        break;
412      case "cty" :
413        if (!p.getValue().isJsonString()) {
414          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
415              IssueType.STRUCTURE, "cty must be a string", IssueSeverity.ERROR);   
416        } else if (!Utilities.existsInList(p.getValue().asString(), "application/smart-health-card", "application/smart-api-access", "application/fhir+json")) {
417          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
418              IssueType.STRUCTURE, "cty must be one of 'application/smart-health-card/, 'application/smart-api-access', 'application/fhir+json'", IssueSeverity.ERROR);   
419        } else {
420          ct = p.getValue().asString();
421        }
422        break;
423      case "v":
424        if (!p.getValue().isJsonString()) {
425          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
426              IssueType.STRUCTURE, "v must be a string", IssueSeverity.ERROR);   
427        } else if (p.getValue().asString().length() <= 80) {
428          logError(errors, "2023-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
429              IssueType.STRUCTURE, "if present, v must be '1'", IssueSeverity.ERROR);   
430        }
431        break;
432      default:
433        logError(errors, "202-08-31", p.getValue().getStart().getLine(), p.getValue().getStart().getCol(), "shl."+p.getName(), 
434            IssueType.STRUCTURE, "Illegal property name "+p.getName(), IssueSeverity.ERROR);  
435      }
436    }
437    if (hu && !fCty) {
438      logError(errors, "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "Flag 'U' found, but no 'cty' header which is required for the U flag", IssueSeverity.ERROR);  
439      ok = false;
440    }
441    if (!fUrl) {
442      logError(errors, "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "No url found", IssueSeverity.ERROR);  
443      ok = false;
444    }
445    if (!fKey) {
446      logError(errors, "202-08-31", 1, 1, "shl", IssueType.STRUCTURE, "No key found", IssueSeverity.ERROR);  
447      ok = false;
448    }
449    return ok;
450  }
451
452  public void compose(Element e, OutputStream destination, OutputStyle style, String base)  throws FHIRException, IOException {
453    throw new FHIRFormatError("Writing resources is not supported for the SHL format");
454    // because then we'd have to try to sign, and we're just not going to be doing that from the element model
455  }
456
457  public static boolean isTestMode() {
458    return testMode;
459  }
460
461  public static void setTestMode(boolean testMode) {
462    SHLParser.testMode = testMode;
463  }
464
465
466}