001package org.hl7.fhir.r4.terminologies; 002 003import java.util.ArrayList; 004import java.util.List; 005 006import org.hl7.fhir.exceptions.FHIRException; 007import org.hl7.fhir.r4.context.IWorkerContext; 008import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult; 009import org.hl7.fhir.r4.model.CodeSystem; 010import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; 011import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; 012import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent; 013import org.hl7.fhir.r4.model.CodeableConcept; 014import org.hl7.fhir.r4.model.Coding; 015import org.hl7.fhir.r4.model.UriType; 016import org.hl7.fhir.r4.model.ValueSet; 017import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 018import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 019import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent; 020import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 021import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 022import org.hl7.fhir.r4.utils.EOperationOutcome; 023import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 025 026public class ValueSetCheckerSimple implements ValueSetChecker { 027 028 private ValueSet valueset; 029 private IWorkerContext context; 030 031 public ValueSetCheckerSimple(ValueSet source, IWorkerContext context) { 032 this.valueset = source; 033 this.context = context; 034 } 035 036 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 037 // first, we validate the codings themselves 038 List<String> errors = new ArrayList<String>(); 039 List<String> warnings = new ArrayList<String>(); 040 for (Coding c : code.getCoding()) { 041 if (!c.hasSystem()) 042 warnings.add("Coding has no system"); 043 CodeSystem cs = context.fetchCodeSystem(c.getSystem()); 044 if (cs == null) 045 throw new FHIRException("Unsupported system "+c.getSystem()+" - system is not specified or implicit"); 046 if (cs.getContent() != CodeSystemContentMode.COMPLETE) 047 throw new FHIRException("Unable to resolve system "+c.getSystem()+" - system is not complete"); 048 ValidationResult res = validateCode(c, cs); 049 if (!res.isOk()) 050 errors.add(res.getMessage()); 051 else if (res.getMessage() != null) 052 warnings.add(res.getMessage()); 053 } 054 if (valueset != null) { 055 boolean ok = false; 056 for (Coding c : code.getCoding()) { 057 ok = ok || codeInValueSet(c.getSystem(), c.getCode()); 058 } 059 if (!ok) 060 errors.add(0, "None of the provided codes are in the value set "+valueset.getUrl()); 061 } 062 if (errors.size() > 0) 063 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 064 else if (warnings.size() > 0) 065 return new ValidationResult(IssueSeverity.WARNING, warnings.toString()); 066 else 067 return new ValidationResult(IssueSeverity.INFORMATION, null); 068 } 069 070 public ValidationResult validateCode(Coding code) throws FHIRException { 071 // first, we validate the concept itself 072 String system = code.hasSystem() ? code.getSystem() : getValueSetSystem(); 073 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 074 system = systemForCodeInValueSet(code.getCode()); 075 } 076 CodeSystem cs = context.fetchCodeSystem(system); 077 if (cs == null) 078 throw new FHIRException("Unable to resolve system "+system+" - system is not specified or implicit"); 079 if (cs.getContent() != CodeSystemContentMode.COMPLETE) 080 throw new FHIRException("Unable to resolve system "+system+" - system is not complete"); 081 ValidationResult res = validateCode(code, cs); 082 083 // then, if we have a value set, we check it's in the value set 084 if (res.isOk() && valueset != null && !codeInValueSet(system, code.getCode())) 085 res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 086 return res; 087 } 088 089 090 private ValidationResult validateCode(Coding code, CodeSystem cs) { 091 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode()); 092 if (cc == null) 093 return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl()); 094 if (code.getDisplay() == null) 095 return new ValidationResult(cc); 096 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 097 if (cc.hasDisplay()) { 098 b.append(cc.getDisplay()); 099 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) 100 return new ValidationResult(cc); 101 } 102 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 103 b.append(ds.getValue()); 104 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) 105 return new ValidationResult(cc); 106 } 107 return new ValidationResult(IssueSeverity.WARNING, "Display Name for "+code+" must be one of '"+b.toString()+"'", cc); 108 } 109 110 private String getValueSetSystem() throws FHIRException { 111 if (valueset == null) 112 throw new FHIRException("Unable to resolve system - no value set"); 113 if (valueset.getCompose().hasExclude()) 114 throw new FHIRException("Unable to resolve system - value set has excludes"); 115 if (valueset.getCompose().getInclude().size() == 0) 116 throw new FHIRException("Unable to resolve system - value set has no includes"); 117 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 118 if (inc.hasValueSet()) 119 throw new FHIRException("Unable to resolve system - value set has imports"); 120 if (!inc.hasSystem()) 121 throw new FHIRException("Unable to resolve system - value set has include with no system"); 122 } 123 if (valueset.getCompose().getInclude().size() == 1) 124 return valueset.getCompose().getInclude().get(0).getSystem(); 125 126 return null; 127 } 128 129 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 130 for (ConceptDefinitionComponent cc : concept) { 131 if (code.equals(cc.getCode())) 132 return cc; 133 ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code); 134 if (c != null) 135 return c; 136 } 137 return null; 138 } 139 140 141 private String systemForCodeInValueSet(String code) { 142 String sys = null; 143 if (valueset.hasCompose()) { 144 if (valueset.getCompose().hasExclude()) 145 return null; 146 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 147 if (vsi.hasValueSet()) 148 return null; 149 if (!vsi.hasSystem()) 150 return null; 151 if (vsi.hasFilter()) 152 return null; 153 CodeSystem cs = context.fetchCodeSystem(vsi.getSystem()); 154 if (cs == null) 155 return null; 156 if (vsi.hasConcept()) { 157 for (ConceptReferenceComponent cc : vsi.getConcept()) { 158 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 159 if (match) { 160 if (sys == null) 161 sys = vsi.getSystem(); 162 else if (!sys.equals(vsi.getSystem())) 163 return null; 164 } 165 } 166 } else { 167 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 168 if (cc != null) { 169 if (sys == null) 170 sys = vsi.getSystem(); 171 else if (!sys.equals(vsi.getSystem())) 172 return null; 173 } 174 } 175 } 176 } 177 178 return sys; 179 } 180 181 @Override 182 public boolean codeInValueSet(String system, String code) throws FHIRException { 183 if (valueset.hasCompose()) { 184 boolean ok = false; 185 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 186 ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 187 } 188 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 189 ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 190 } 191 return ok; 192 } 193 194 return false; 195 } 196 197 private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException { 198 for (UriType uri : vsi.getValueSet()) { 199 if (inImport(uri.getValue(), system, code)) 200 return true; 201 } 202 203 if (!vsi.hasSystem()) 204 return false; 205 206 if (only && system == null) { 207 // whether we know the system or not, we'll accept the stated codes at face value 208 for (ConceptReferenceComponent cc : vsi.getConcept()) 209 if (cc.getCode().equals(code)) 210 return true; 211 } 212 213 if (!system.equals(vsi.getSystem())) 214 return false; 215 if (vsi.hasFilter()) 216 throw new FHIRException("Filters - not done yet"); 217 218 CodeSystem def = context.fetchCodeSystem(system); 219 if (def.getContent() != CodeSystemContentMode.COMPLETE) 220 throw new FHIRException("Unable to resolve system "+vsi.getSystem()+" - system is not complete"); 221 222 List<ConceptDefinitionComponent> list = def.getConcept(); 223 return validateCodeInConceptList(code, def, list); 224 } 225 226 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 227 if (def.getCaseSensitive()) { 228 for (ConceptDefinitionComponent cc : list) { 229 if (cc.getCode().equals(code)) 230 return true; 231 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 232 return true; 233 } 234 } else { 235 for (ConceptDefinitionComponent cc : list) { 236 if (cc.getCode().equalsIgnoreCase(code)) 237 return true; 238 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 239 return true; 240 } 241 } 242 return false; 243 } 244 245 private boolean inImport(String uri, String system, String code) throws FHIRException { 246 ValueSet vs = context.fetchResource(ValueSet.class, uri); 247 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(vs, context); 248 return vsc.codeInValueSet(system, code); 249 } 250 251}