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}