001package org.hl7.fhir.r5.utils.sql; 002 003import java.util.ArrayList; 004import java.util.Collection; 005import java.util.HashSet; 006import java.util.List; 007import java.util.Set; 008 009import javax.annotation.Nonnull; 010 011import org.hl7.fhir.exceptions.FHIRException; 012import org.hl7.fhir.r5.context.IWorkerContext; 013import org.hl7.fhir.r5.fhirpath.ExpressionNode; 014import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; 015import org.hl7.fhir.r5.fhirpath.TypeDetails; 016import org.hl7.fhir.r5.formats.JsonParser; 017import org.hl7.fhir.r5.model.Base64BinaryType; 018import org.hl7.fhir.r5.model.BooleanType; 019import org.hl7.fhir.r5.model.CanonicalType; 020import org.hl7.fhir.r5.model.CodeType; 021import org.hl7.fhir.r5.model.DateTimeType; 022import org.hl7.fhir.r5.model.DateType; 023import org.hl7.fhir.r5.model.DecimalType; 024import org.hl7.fhir.r5.model.IdType; 025import org.hl7.fhir.r5.model.InstantType; 026import org.hl7.fhir.r5.model.Integer64Type; 027import org.hl7.fhir.r5.model.IntegerType; 028import org.hl7.fhir.r5.model.OidType; 029import org.hl7.fhir.r5.model.PositiveIntType; 030import org.hl7.fhir.r5.model.PrimitiveType; 031import org.hl7.fhir.r5.model.StringType; 032import org.hl7.fhir.r5.model.TimeType; 033import org.hl7.fhir.r5.model.UnsignedIntType; 034import org.hl7.fhir.r5.model.UriType; 035import org.hl7.fhir.r5.model.UrlType; 036import org.hl7.fhir.r5.model.UuidType; 037import org.hl7.fhir.r5.utils.sql.Validator.TrueFalseOrUnknown; 038import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus; 039import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IssueMessage; 040import org.hl7.fhir.utilities.Utilities; 041import org.hl7.fhir.utilities.json.model.JsonArray; 042import org.hl7.fhir.utilities.json.model.JsonBoolean; 043import org.hl7.fhir.utilities.json.model.JsonElement; 044import org.hl7.fhir.utilities.json.model.JsonNumber; 045import org.hl7.fhir.utilities.json.model.JsonObject; 046import org.hl7.fhir.utilities.json.model.JsonProperty; 047import org.hl7.fhir.utilities.json.model.JsonString; 048import org.hl7.fhir.utilities.validation.ValidationMessage; 049import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 050import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 051import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 052 053public class Validator { 054 055 public enum TrueFalseOrUnknown { 056 TRUE, FALSE, UNKNOWN 057 } 058 059 private IWorkerContext context; 060 private FHIRPathEngine fpe; 061 private List<String> prohibitedNames = new ArrayList<String>(); 062 private List<ValidationMessage> issues = new ArrayList<ValidationMessage>(); 063 private TrueFalseOrUnknown supportsArrays; 064 private TrueFalseOrUnknown supportsComplexTypes; 065 private TrueFalseOrUnknown supportsNeedsName; 066 067 private String resourceName; 068 private String name; 069 070 public Validator(IWorkerContext context, FHIRPathEngine fpe, List<String> prohibitedNames, @Nonnull TrueFalseOrUnknown supportsArrays, @Nonnull TrueFalseOrUnknown supportsComplexTypes, @Nonnull TrueFalseOrUnknown supportsNeedsName) { 071 super(); 072 this.context = context; 073 this.fpe = fpe; 074 this.prohibitedNames = prohibitedNames; 075 this.supportsArrays = supportsArrays; 076 this.supportsComplexTypes = supportsComplexTypes; 077 this.supportsNeedsName = supportsNeedsName; 078 } 079 080 public String getResourceName() { 081 return resourceName; 082 } 083 084 085 public void checkViewDefinition(String path, JsonObject viewDefinition) { 086 checkProperties(viewDefinition, path, "resourceType", "url", "identifier", "name", "version", "title", "status", "experimental", "date", "publisher", "contact", "description", "useContext", "copyright", "resource", "constant", "select", "where"); 087 088 JsonElement nameJ = viewDefinition.get("name"); 089 if (nameJ == null) { 090 if (supportsNeedsName == null) { 091 hint(path, viewDefinition, "No name provided. A name is required in many contexts where a ViewDefinition is used"); 092 } else if (supportsNeedsName == TrueFalseOrUnknown.TRUE) { 093 error(path, viewDefinition, "No name provided", IssueType.REQUIRED); 094 } 095 } else if (!(nameJ instanceof JsonString)) { 096 error(path, viewDefinition, "name must be a string", IssueType.INVALID); 097 } else { 098 name = nameJ.asString(); 099 if (!isValidName(name)) { 100 error(path+".name", nameJ, "The name '"+name+"' is not valid", IssueType.INVARIANT); 101 } 102 if (prohibitedNames.contains(name)) { 103 error(path, nameJ, "The name '"+name+"' on the viewDefinition is not allowed in this context", IssueType.BUSINESSRULE); 104 } 105 } 106 107 List<Column> columns = new ArrayList<>(); 108 viewDefinition.setUserData("columns", columns); 109 110 JsonElement resourceNameJ = viewDefinition.get("resource"); 111 if (resourceNameJ == null) { 112 error(path, viewDefinition, "No resource specified", IssueType.REQUIRED); 113 } else if (!(resourceNameJ instanceof JsonString)) { 114 error(path, viewDefinition, "resource must be a string", IssueType.INVALID); 115 } else { 116 resourceName = resourceNameJ.asString(); 117 if (!context.getResourceNamesAsSet().contains(resourceName)) { 118 error(path+".name", nameJ, "The name '"+resourceName+"' is not a valid resource", IssueType.BUSINESSRULE); 119 } else { 120 int i = 0; 121 if (checkAllObjects(path, viewDefinition, "constant")) { 122 for (JsonObject constant : viewDefinition.getJsonObjects("constant")) { 123 checkConstant(path+".constant["+i+"]", constant); 124 i++; 125 } 126 } 127 i = 0; 128 if (checkAllObjects(path, viewDefinition, "where")) { 129 for (JsonObject where : viewDefinition.getJsonObjects("where")) { 130 checkWhere(viewDefinition, path+".where["+i+"]", where); 131 i++; 132 } 133 } 134 TypeDetails t = new TypeDetails(CollectionStatus.SINGLETON, resourceName); 135 136 i = 0; 137 if (checkAllObjects(path, viewDefinition, "select")) { 138 for (JsonObject select : viewDefinition.getJsonObjects("select")) { 139 columns.addAll(checkSelect(viewDefinition, path+".select["+i+"]", select, t)); 140 i++; 141 } 142 if (i == 0) { 143 error(path, viewDefinition, "No select statements found", IssueType.REQUIRED); 144 } 145 } 146 } 147 } 148 } 149 150 private List<Column> checkSelect(JsonObject vd, String path, JsonObject select, TypeDetails t) { 151 List<Column> columns = new ArrayList<>(); 152 select.setUserData("columns", columns); 153 checkProperties(select, path, "column", "select", "forEach", "forEachOrNull", "unionAll"); 154 155 if (select.has("forEach")) { 156 t = checkForEach(vd, path, select, select.get("forEach"), t); 157 } else if (select.has("forEachOrNull")) { 158 t = checkForEachOrNull(vd, path, select, select.get("forEachOrNull"), t); 159 } 160 161 if (t != null) { 162 163 if (select.has("column")) { 164 JsonElement a = select.get("column"); 165 if (!(a instanceof JsonArray)) { 166 error(path+".column", a, "column is not an array", IssueType.INVALID); 167 } else { 168 int i = 0; 169 for (JsonElement e : ((JsonArray) a)) { 170 if (!(e instanceof JsonObject)) { 171 error(path+".column["+i+"]", a, "column["+i+"] is a "+e.type().toName()+" not an object", IssueType.INVALID); 172 } else { 173 columns.add(checkColumn(vd, path+".column["+i+"]", (JsonObject) e, t)); 174 } 175 } 176 } 177 } 178 179 if (select.has("select")) { 180 JsonElement a = select.get("select"); 181 if (!(a instanceof JsonArray)) { 182 error(path+".select", a, "select is not an array", IssueType.INVALID); 183 } else { 184 int i = 0; 185 for (JsonElement e : ((JsonArray) a)) { 186 if (!(e instanceof JsonObject)) { 187 error(path+".select["+i+"]", e, "select["+i+"] is not an object", IssueType.INVALID); 188 } else { 189 columns.addAll(checkSelect(vd, path+".select["+i+"]", (JsonObject) e, t)); 190 } 191 } 192 } 193 } 194 195 if (select.has("unionAll")) { 196 columns.addAll(checkUnion(vd, path, select, select.get("unionAll"), t)); 197 } 198 if (columns.isEmpty()) { 199 error(path, select, "The select has no columns or selects", IssueType.REQUIRED); 200 } else { 201 checkColumnNamesUnique(select, path, columns); 202 } 203 } 204 return columns; 205 } 206 207 208 private void checkColumnNamesUnique(JsonObject select, String path, List<Column> columns) { 209 Set<String> names = new HashSet<>(); 210 for (Column col : columns) { 211 if (col != null) { 212 if (!names.contains(col.getName())) { 213 names.add(col.getName()); 214 } else if (!col.isDuplicateReported()) { 215 col.setDuplicateReported(true); 216 error(path, select, "Duplicate Column Name '"+col.getName()+"'", IssueType.BUSINESSRULE); 217 } 218 } 219 } 220 } 221 222 private List<Column> checkUnion(JsonObject vd, String path, JsonObject focus, JsonElement expression, TypeDetails t) { 223 JsonElement a = focus.get("unionAll"); 224 if (!(a instanceof JsonArray)) { 225 error(path+".unionAll", a, "union is not an array", IssueType.INVALID); 226 return null; 227 } else { 228 List<List<Column>> unionColumns = new ArrayList<>(); 229 int i = 0; 230 for (JsonElement e : ((JsonArray) a)) { 231 if (!(e instanceof JsonObject)) { 232 error(path+".unionAll["+i+"]", e, "unionAll["+i+"] is not an object", IssueType.INVALID); 233 } else { 234 unionColumns.add(checkSelect(vd, path+".unionAll["+i+"]", (JsonObject) e, t)); 235 } 236 i++; 237 } 238 if (i < 2) { 239 warning(path+".unionAll", a, "unionAll should have more than one item"); 240 } 241 if (unionColumns.size() > 1) { 242 List<Column> columns = unionColumns.get(0); 243 for (int ic = 1; ic < unionColumns.size(); ic++) { 244 String diff = columnDiffs(columns, unionColumns.get(ic)); 245 if (diff != null) { 246 error(path+".unionAll["+i+"]", ((JsonArray) a).get(ic), "unionAll["+i+"] column definitions do not match: "+diff, IssueType.INVALID); 247 } 248 } 249 a.setUserData("colunms", columns); 250 return columns; 251 } 252 } 253 return null; 254 } 255 256 private String columnDiffs(List<Column> list1, List<Column> list2) { 257 if (list1.size() == list2.size()) { 258 for (int i = 0; i < list1.size(); i++) { 259 if (list1.get(i) == null || list2.get(i) == null) { 260 return null; // just suppress any addition errors 261 } 262 String diff = list1.get(i).diff(list2.get(i)); 263 if (diff != null) { 264 return diff+" at #"+i; 265 } 266 } 267 return null; 268 } else { 269 return "Column counts differ: "+list1.size()+" vs "+list2.size(); 270 } 271 } 272 273 private Column checkColumn(JsonObject vd, String path, JsonObject column, TypeDetails t) { 274 checkProperties(column, path, "path", "name", "description", "collection", "type", "tag"); 275 276 if (!column.has("path")) { 277 error(path, column, "no path found", IssueType.INVALID); 278 } else { 279 JsonElement expression = column.get("path"); 280 if (!(expression instanceof JsonString)) { 281 error(path+".forEach", expression, "forEach is not a string", IssueType.INVALID); 282 } else { 283 String expr = expression.asString(); 284 285 List<IssueMessage> warnings = new ArrayList<>(); 286 TypeDetails td = null; 287 ExpressionNode node = null; 288 try { 289 node = fpe.parse(expr); 290 column.setUserData("path", node); 291 td = fpe.checkOnTypes(vd, resourceName, t, node, warnings); 292 } catch (Exception e) { 293 error(path, expression, e.getMessage(), IssueType.INVALID); 294 } 295 if (td != null && node != null) { 296 for (IssueMessage s : warnings) { 297 warning(path+".path", expression, s.getMessage()); 298 } 299 String columnName = null; 300 JsonElement nameJ = column.get("name"); 301 if (nameJ != null) { 302 if (nameJ instanceof JsonString) { 303 columnName = nameJ.asString(); 304 if (!isValidName(columnName)) { 305 error(path+".name", nameJ, "The name '"+columnName+"' is not valid", IssueType.VALUE); 306 } 307 } else { 308 error(path+".name", nameJ, "name must be a string", IssueType.INVALID); 309 } 310 } 311 if (columnName == null) { 312 List<String> names = node.getDistalNames(); 313 if (names.size() == 1 && names.get(0) != null) { 314 columnName = names.get(0); 315 if (!isValidName(columnName)) { 316 error(path+".path", expression, "A column name is required. The natural name to chose is '"+columnName+"' (from the path)", IssueType.INVARIANT); 317 } else { 318 error(path, column, "A column name is required", IssueType.REQUIRED); 319 } 320 } else { 321 error(path, column, "A column name is required", IssueType.REQUIRED); 322 } 323 } 324 // ok, name is sorted! 325 if (columnName != null) { 326 column.setUserData("name", columnName); 327 boolean isColl = false; 328 if (column.has("collection")) { 329 JsonElement collectionJ = column.get("collection"); 330 if (!(collectionJ instanceof JsonBoolean)) { 331 error(path+".collection", collectionJ, "collection is not a boolean", IssueType.INVALID); 332 } else { 333 boolean collection = collectionJ.asJsonBoolean().asBoolean(); 334 if (collection) { 335 isColl = true; 336 } 337 } 338 } 339 if (isColl) { 340 if (td.getCollectionStatus() == CollectionStatus.SINGLETON) { 341 hint(path, column, "collection is true, but the path statement(s) ('"+expr+"') can only return single values for the column '"+columnName+"'"); 342 } 343 if (supportsArrays == TrueFalseOrUnknown.UNKNOWN) { 344 warning(path, expression, "The column '"+columnName+"' is defined as a collection, but collections are not supported in all execution contexts"); 345 } else if (supportsArrays == TrueFalseOrUnknown.FALSE) { 346 if (td.getCollectionStatus() == CollectionStatus.SINGLETON) { 347 warning(path, expression, "The column '"+columnName+"' is defined as a collection, but this is not allowed in the current execution context. Note that the path '"+expr+"' can only return a single value"); 348 } else { 349 warning(path, expression, "The column '"+columnName+"' is defined as a collection, but this is not allowed in the current execution context. Note that the path '"+expr+"' can return a collection of values"); 350 } 351 } 352 } else { 353 if (td.getCollectionStatus() != CollectionStatus.SINGLETON) { 354 warning(path, column, "This column is not defined as a collection, but the path statement '"+expr+"' might return multiple values for the column '"+columnName+"' for some inputs"); 355 } 356 } 357 Set<String> types = new HashSet<>(); 358 if (node.isNullSet()) { 359 types.add("null"); 360 } else { 361 // ok collection is sorted 362 for (String type : td.getTypes()) { 363 types.add(simpleType(type)); 364 } 365 366 JsonElement typeJ = column.get("type"); 367 if (typeJ != null) { 368 if (typeJ instanceof JsonString) { 369 String type = typeJ.asString(); 370 if (!td.hasType(type)) { 371 error(path+".type", typeJ, "The path expression ('"+expr+"') does not return a value of the type '"+type+"' - found "+td.describe(), IssueType.VALUE); 372 } else { 373 types.clear(); 374 types.add(simpleType(type)); 375 } 376 } else { 377 error(path+".type", typeJ, "type must be a string", IssueType.INVALID); 378 } 379 } 380 } 381 if (types.size() != 1) { 382 error(path, column, "Unable to determine a type (found "+td.describe()+")", IssueType.BUSINESSRULE); 383 } else { 384 String type = types.iterator().next(); 385 boolean ok = false; 386 if (!isSimpleType(type) && !"null".equals(type)) { 387 if (supportsComplexTypes == TrueFalseOrUnknown.UNKNOWN) { 388 warning(path, expression, "Column from path '"+expr+"' is a complex type ('"+type+"'). This is not supported in some Runners"); 389 } else if (supportsComplexTypes == TrueFalseOrUnknown.FALSE) { 390 error(path, expression, "Column from path '"+expr+"' is a complex type ('"+type+"') but this is not allowed in this context", IssueType.BUSINESSRULE); 391 } else { 392 ok = true; 393 } 394 } else { 395 ok = true; 396 } 397 if (ok) { 398 Column col = new Column(columnName, isColl, type, kindForType(type)); 399 column.setUserData("column", col); 400 return col; 401 } 402 } 403 } 404 } 405 } 406 } 407 return null; 408 } 409 410 private ColumnKind kindForType(String type) { 411 switch (type) { 412 case "null": return ColumnKind.Null; 413 case "dateTime": return ColumnKind.DateTime; 414 case "boolean": return ColumnKind.Boolean; 415 case "integer": return ColumnKind.Integer; 416 case "decimal": return ColumnKind.Decimal; 417 case "string": return ColumnKind.String; 418 case "canonical": return ColumnKind.String; 419 case "url": return ColumnKind.String; 420 case "uri": return ColumnKind.String; 421 case "oid": return ColumnKind.String; 422 case "uuid": return ColumnKind.String; 423 case "id": return ColumnKind.String; 424 case "code": return ColumnKind.String; 425 case "base64Binary": return ColumnKind.Binary; 426 case "time": return ColumnKind.Time; 427 default: return ColumnKind.Complex; 428 } 429 } 430 431 private boolean isSimpleType(String type) { 432 return Utilities.existsInList(type, "dateTime", "boolean", "integer", "decimal", "string", "base64Binary", "id", "code", "date", "time", "canonical"); 433 } 434 435 private String simpleType(String type) { 436 type = type.replace("http://hl7.org/fhirpath/System.", "").replace("http://hl7.org/fhir/StructureDefinition/", ""); 437 if (Utilities.existsInList(type, "date", "dateTime", "instant")) { 438 return "dateTime"; 439 } 440 if (Utilities.existsInList(type, "Boolean", "boolean")) { 441 return "boolean"; 442 } 443 if (Utilities.existsInList(type, "Integer", "integer", "integer64")) { 444 return "integer"; 445 } 446 if (Utilities.existsInList(type, "Decimal", "decimal")) { 447 return "decimal"; 448 } 449 if (Utilities.existsInList(type, "String", "string", "code")) { 450 return "string"; 451 } 452 if (Utilities.existsInList(type, "Time", "time")) { 453 return "time"; 454 } 455 if (Utilities.existsInList(type, "base64Binary")) { 456 return "base64Binary"; 457 } 458 return type; 459 } 460 461 private TypeDetails checkForEach(JsonObject vd, String path, JsonObject focus, JsonElement expression, TypeDetails t) { 462 if (!(expression instanceof JsonString)) { 463 error(path+".forEach", expression, "forEach is not a string", IssueType.INVALID); 464 return null; 465 } else { 466 String expr = expression.asString(); 467 468 List<IssueMessage> warnings = new ArrayList<>(); 469 TypeDetails td = null; 470 try { 471 ExpressionNode n = fpe.parse(expr); 472 focus.setUserData("forEach", n); 473 td = fpe.checkOnTypes(vd, resourceName, t, n, warnings); 474 } catch (Exception e) { 475 error(path, expression, e.getMessage(), IssueType.INVALID); 476 } 477 if (td != null) { 478 for (IssueMessage s : warnings) { 479 warning(path+".forEach", expression, s.getMessage()); 480 } 481 } 482 return td; 483 } 484 } 485 486 private TypeDetails checkForEachOrNull(JsonObject vd, String path, JsonObject focus, JsonElement expression, TypeDetails t) { 487 if (!(expression instanceof JsonString)) { 488 error(path+".forEachOrNull", expression, "forEachOrNull is not a string", IssueType.INVALID); 489 return null; 490 } else { 491 String expr = expression.asString(); 492 493 List<IssueMessage> warnings = new ArrayList<>(); 494 TypeDetails td = null; 495 try { 496 ExpressionNode n = fpe.parse(expr); 497 focus.setUserData("forEachOrNull", n); 498 td = fpe.checkOnTypes(vd, resourceName, t, n, warnings); 499 } catch (Exception e) { 500 error(path, expression, e.getMessage(), IssueType.INVALID); 501 } 502 if (td != null) { 503 for (IssueMessage s : warnings) { 504 warning(path+".forEachOrNull", expression, s.getMessage()); 505 } 506 } 507 return td; 508 } 509 } 510 511 private void checkConstant(String path, JsonObject constant) { 512 checkProperties(constant, path, "name", "valueBase64Binary", "valueBoolean", "valueCanonical", "valueCode", "valueDate", "valueDateTime", "valueDecimal", "valueId", "valueInstant", "valueInteger", "valueInteger64", "valueOid", "valueString", "valuePositiveInt", "valueTime", "valueUnsignedInt", "valueUri", "valueUrl", "valueUuid"); 513 JsonElement nameJ = constant.get("name"); 514 if (nameJ == null) { 515 error(path, constant, "No name provided", IssueType.REQUIRED); 516 } else if (!(nameJ instanceof JsonString)) { 517 error(path, constant, "Name must be a string", IssueType.INVALID); 518 } else { 519 String name = constant.asString("name"); 520 if (!isValidName(name)) { 521 error(path+".name", nameJ, "The name '"+name+"' is not valid", IssueType.INVARIANT); 522 } 523 } 524 if (constant.has("valueBase64Binary")) { 525 checkIsString(path, constant, "valueBase64Binary", new Base64BinaryType()); 526 } else if (constant.has("valueBoolean")) { 527 checkIsBoolean(path, constant, "valueBoolean", new BooleanType()); 528 } else if (constant.has("valueCanonical")) { 529 checkIsString(path, constant, "valueCanonical", new CanonicalType()); 530 } else if (constant.has("valueCode")) { 531 checkIsString(path, constant, "valueCode", new CodeType()); 532 } else if (constant.has("valueDate")) { 533 checkIsString(path, constant, "valueDate", new DateType()); 534 } else if (constant.has("valueDateTime")) { 535 checkIsString(path, constant, "valueDateTime", new DateTimeType()); 536 } else if (constant.has("valueDecimal")) { 537 checkIsNumber(path, constant, "valueDecimal", new DecimalType()); 538 } else if (constant.has("valueId")) { 539 checkIsString(path, constant, "valueId", new IdType()); 540 } else if (constant.has("valueInstant")) { 541 checkIsString(path, constant, "valueInstant", new InstantType()); 542 } else if (constant.has("valueInteger")) { 543 checkIsNumber(path, constant, "valueInteger", new IntegerType()); 544 } else if (constant.has("valueInteger64")) { 545 checkIsNumber(path, constant, "valueInteger64", new Integer64Type()); 546 } else if (constant.has("valueOid")) { 547 checkIsString(path, constant, "valueOid", new OidType()); 548 } else if (constant.has("valueString")) { 549 checkIsString(path, constant, "valueString", new StringType()); 550 } else if (constant.has("valuePositiveInt")) { 551 checkIsNumber(path, constant, "valuePositiveInt", new PositiveIntType()); 552 } else if (constant.has("valueTime")) { 553 checkIsString(path, constant, "valueTime", new TimeType()); 554 } else if (constant.has("valueUnsignedInt")) { 555 checkIsNumber(path, constant, "valueUnsignedInt", new UnsignedIntType()); 556 } else if (constant.has("valueUri")) { 557 checkIsString(path, constant, "valueUri", new UriType()); 558 } else if (constant.has("valueUrl")) { 559 checkIsString(path, constant, "valueUrl", new UrlType()); 560 } else if (constant.has("valueUuid")) { 561 checkIsString(path, constant, "valueUuid", new UuidType()); 562 } else { 563 error(path, constant, "No value found", IssueType.REQUIRED); 564 } 565 } 566 567 private void checkIsString(String path, JsonObject constant, String name, PrimitiveType<?> value) { 568 JsonElement j = constant.get(name); 569 if (!(j instanceof JsonString)) { 570 error(path+"."+name, j, name+" must be a string", IssueType.INVALID); 571 } else { 572 value.setValueAsString(j.asString()); 573 constant.setUserData("value", value); 574 } 575 } 576 577 private void checkIsBoolean(String path, JsonObject constant, String name, PrimitiveType<?> value) { 578 JsonElement j = constant.get(name); 579 if (!(j instanceof JsonBoolean)) { 580 error(path+"."+name, j, name+" must be a boolean", IssueType.INVALID); 581 } else { 582 value.setValueAsString(j.asString()); 583 constant.setUserData("value", value); 584 } 585 } 586 587 private void checkIsNumber(String path, JsonObject constant, String name, PrimitiveType<?> value) { 588 JsonElement j = constant.get(name); 589 if (!(j instanceof JsonNumber)) { 590 error(path+"."+name, j, name+" must be a number", IssueType.INVALID); 591 } else { 592 value.setValueAsString(j.asString()); 593 constant.setUserData("value", value); 594 } 595 } 596 597 private void checkWhere(JsonObject vd, String path, JsonObject where) { 598 checkProperties(where, path, "path", "description"); 599 600 String expr = where.asString("path"); 601 if (expr == null) { 602 error(path, where, "No path provided", IssueType.REQUIRED); 603 } 604 List<String> types = new ArrayList<>(); 605 List<IssueMessage> warnings = new ArrayList<>(); 606 types.add(resourceName); 607 TypeDetails td = null; 608 try { 609 ExpressionNode n = fpe.parse(expr); 610 where.setUserData("path", n); 611 td = fpe.checkOnTypes(vd, resourceName, types, n, warnings); 612 } catch (Exception e) { 613 error(path, where.get("path"), e.getMessage(), IssueType.INVALID); 614 } 615 if (td != null) { 616 if (td.getCollectionStatus() != CollectionStatus.SINGLETON || td.getTypes().size() != 1 || !td.hasType("boolean")) { 617 error(path+".path", where.get("path"), "A where path must return a boolean, but the expression "+expr+" returns a "+td.describe(), IssueType.BUSINESSRULE); 618 } else { 619 for (IssueMessage s : warnings) { 620 warning(path+".path", where.get("path"), s.getMessage()); 621 } 622 } 623 } 624 } 625 626 private void checkProperties(JsonObject obj, String path, String... names) { 627 for (JsonProperty p : obj.getProperties()) { 628 boolean nameOk = "extension".equals(p.getName()); 629 for (String name : names) { 630 nameOk = nameOk || name.equals(p.getName()); 631 } 632 if (!nameOk) { 633 error(path+"."+p.getName(), p.getValue(), "Unknown JSON property "+p.getName(), IssueType.UNKNOWN); 634 } 635 } 636 637 } 638 639 private boolean isValidName(String name) { 640 boolean first = true; 641 for (char c : name.toCharArray()) { 642 if (!(Character.isAlphabetic(c) || Character.isDigit(c) || (!first && c == '_'))) { 643 return false; 644 } 645 first = false; 646 } 647 return true; 648 } 649 650 651 private boolean checkAllObjects(String path, JsonObject focus, String name) { 652 if (!focus.has(name)) { 653 return true; 654 } else if (!(focus.get(name) instanceof JsonArray)) { 655 error(path+"."+name, focus.get(name), name+" must be an array", IssueType.INVALID); 656 return false; 657 } else { 658 JsonArray arr = focus.getJsonArray(name); 659 int i = 0; 660 boolean ok = true; 661 for (JsonElement e : arr) { 662 if (!(e instanceof JsonObject)) { 663 error(path+"."+name+"["+i+"]", e, name+"["+i+"] must be an object", IssueType.INVALID); 664 ok = false; 665 } 666 } 667 return ok; 668 } 669 } 670 671 private void error(String path, JsonElement e, String issue, IssueType type) { 672 ValidationMessage vm = new ValidationMessage(Source.InstanceValidator, type, e.getStart().getLine(), e.getStart().getCol(), path, issue, IssueSeverity.ERROR); 673 issues.add(vm); 674 675 } 676 677 private void warning(String path, JsonElement e, String issue) { 678 ValidationMessage vm = new ValidationMessage(Source.InstanceValidator, IssueType.BUSINESSRULE, e.getStart().getLine(), e.getStart().getCol(), path, issue, IssueSeverity.WARNING); 679 issues.add(vm); 680 } 681 682 private void hint(String path, JsonElement e, String issue) { 683 ValidationMessage vm = new ValidationMessage(Source.InstanceValidator, IssueType.BUSINESSRULE, e.getStart().getLine(), e.getStart().getCol(), path, issue, IssueSeverity.INFORMATION); 684 issues.add(vm); 685 } 686 687 public void dump() { 688 for (ValidationMessage vm : issues) { 689 System.out.println(vm.summary()); 690 } 691 692 } 693 694 public void check() { 695 if (!isOk()) { 696 throw new FHIRException("View Definition is not valid"); 697 } 698 699 } 700 701 public String getName() { 702 return name; 703 } 704 705 public List<ValidationMessage> getIssues() { 706 return issues; 707 } 708 709 public boolean isOk() { 710 boolean ok = true; 711 for (ValidationMessage vm : issues) { 712 if (vm.isError()) { 713 ok = false; 714 } 715 } 716 return ok; 717 } 718}