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