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}