
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}