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