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