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}