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