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