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