001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.FhirContext; 004import ca.uhn.fhir.context.RuntimeResourceDefinition; 005import ca.uhn.fhir.context.support.IValidationSupport; 006import ca.uhn.fhir.context.support.ValidationSupportContext; 007import ca.uhn.fhir.util.ILockable; 008import jakarta.annotation.Nonnull; 009import jakarta.annotation.Nullable; 010import org.apache.commons.compress.utils.Sets; 011import org.apache.commons.lang3.Validate; 012import org.hl7.fhir.instance.model.api.IBase; 013import org.hl7.fhir.instance.model.api.IBaseResource; 014import org.hl7.fhir.instance.model.api.IPrimitiveType; 015import org.hl7.fhir.r4.model.CodeSystem; 016import org.hl7.fhir.r4.model.StructureDefinition; 017import org.hl7.fhir.r4.model.ValueSet; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import static org.apache.commons.lang3.StringUtils.isNotBlank; 031 032/** 033 * This class is an implementation of {@link IValidationSupport} which may be pre-populated 034 * with a collection of validation resources to be used by the validator. 035 */ 036public class PrePopulatedValidationSupport extends BaseValidationSupport implements IValidationSupport, ILockable { 037 038 private final Map<String, IBaseResource> myUrlToCodeSystems; 039 private final Map<String, IBaseResource> myUrlToStructureDefinitions; 040 private final Map<String, IBaseResource> myUrlToSearchParameters; 041 private final Map<String, IBaseResource> myUrlToValueSets; 042 private final List<IBaseResource> myCodeSystems; 043 private final List<IBaseResource> myStructureDefinitions; 044 private final List<IBaseResource> mySearchParameters; 045 private final List<IBaseResource> myValueSets; 046 private final Map<String, byte[]> myBinaries; 047 private boolean myLocked; 048 049 /** 050 * Constructor 051 */ 052 public PrePopulatedValidationSupport(FhirContext theContext) { 053 this(theContext, new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); 054 } 055 056 @Override 057 public String getName() { 058 return getFhirContext().getVersion().getVersion() + " Pre-populated Validation Support"; 059 } 060 061 /** 062 * Constructor 063 * 064 * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and 065 * values are the resource itself. 066 * @param theUrlToValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are 067 * the resource itself. 068 * @param theUrlToCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are 069 * the resource itself. 070 **/ 071 public PrePopulatedValidationSupport( 072 FhirContext theFhirContext, 073 Map<String, IBaseResource> theUrlToStructureDefinitions, 074 Map<String, IBaseResource> theUrlToValueSets, 075 Map<String, IBaseResource> theUrlToCodeSystems) { 076 this( 077 theFhirContext, 078 theUrlToStructureDefinitions, 079 theUrlToValueSets, 080 theUrlToCodeSystems, 081 new HashMap<>(), 082 new HashMap<>()); 083 } 084 085 /** 086 * Constructor 087 * 088 * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and 089 * values are the resource itself. 090 * @param theUrlToValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are 091 * the resource itself. 092 * @param theUrlToCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are 093 * the resource itself. 094 * @param theBinaries The binary files to be returned by this module. Keys are the unique filename for the binary, and values 095 * are the contents of the file as a byte array. 096 */ 097 public PrePopulatedValidationSupport( 098 FhirContext theFhirContext, 099 Map<String, IBaseResource> theUrlToStructureDefinitions, 100 Map<String, IBaseResource> theUrlToValueSets, 101 Map<String, IBaseResource> theUrlToCodeSystems, 102 Map<String, IBaseResource> theUrlToSearchParameters, 103 Map<String, byte[]> theBinaries) { 104 super(theFhirContext); 105 Validate.notNull(theFhirContext, "theFhirContext must not be null"); 106 Validate.notNull(theUrlToStructureDefinitions, "theStructureDefinitions must not be null"); 107 Validate.notNull(theUrlToValueSets, "theValueSets must not be null"); 108 Validate.notNull(theUrlToCodeSystems, "theCodeSystems must not be null"); 109 Validate.notNull(theUrlToSearchParameters, "theSearchParameters must not be null"); 110 Validate.notNull(theBinaries, "theBinaries must not be null"); 111 myUrlToStructureDefinitions = theUrlToStructureDefinitions; 112 myStructureDefinitions = 113 theUrlToStructureDefinitions.values().stream().distinct().collect(Collectors.toList()); 114 115 myUrlToValueSets = theUrlToValueSets; 116 myValueSets = theUrlToValueSets.values().stream().distinct().collect(Collectors.toList()); 117 118 myUrlToCodeSystems = theUrlToCodeSystems; 119 myCodeSystems = theUrlToCodeSystems.values().stream().distinct().collect(Collectors.toList()); 120 121 myUrlToSearchParameters = theUrlToSearchParameters; 122 mySearchParameters = 123 theUrlToSearchParameters.values().stream().distinct().collect(Collectors.toList()); 124 125 myBinaries = theBinaries; 126 } 127 128 public void addBinary(byte[] theBinary, String theBinaryKey) { 129 validateNotLocked(); 130 Validate.notNull(theBinary, "theBinaryKey must not be null"); 131 Validate.notNull(theBinary, "the" + theBinaryKey + " must not be null"); 132 myBinaries.put(theBinaryKey, theBinary); 133 } 134 135 private synchronized void validateNotLocked() { 136 Validate.isTrue(myLocked == false, "Can not add to validation support, module is locked"); 137 } 138 139 /** 140 * Add a new CodeSystem resource which will be available to the validator. Note that 141 * {@link CodeSystem#getUrl() the URL field) in this resource must contain a value as this 142 * value will be used as the logical URL. 143 * <p> 144 * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension), 145 * it will be stored in three ways: 146 * <ul> 147 * <li>Extension</li> 148 * <li>StructureDefinition/Extension</li> 149 * <li>http://hl7.org/StructureDefinition/Extension</li> 150 * </ul> 151 * </p> 152 */ 153 public void addCodeSystem(IBaseResource theCodeSystem) { 154 validateNotLocked(); 155 Set<String> urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem"); 156 addToMap(theCodeSystem, myCodeSystems, myUrlToCodeSystems, urls); 157 } 158 159 private Set<String> processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) { 160 Validate.notNull(theResource, "the" + theResourceName + " must not be null"); 161 RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(theResource); 162 String actualResourceName = resourceDef.getName(); 163 Validate.isTrue( 164 actualResourceName.equals(theResourceName), 165 "the" + theResourceName + " must be a " + theResourceName + " - Got: " + actualResourceName); 166 167 Optional<IBase> urlValue = 168 resourceDef.getChildByName("url").getAccessor().getFirstValueOrNull(theResource); 169 String url = 170 urlValue.map(t -> (((IPrimitiveType<?>) t).getValueAsString())).orElse(null); 171 172 Validate.notNull(url, "the" + theResourceName + ".getUrl() must not return null"); 173 Validate.notBlank(url, "the" + theResourceName + ".getUrl() must return a value"); 174 175 String urlWithoutVersion; 176 int pipeIdx = url.indexOf('|'); 177 if (pipeIdx != -1) { 178 urlWithoutVersion = url.substring(0, pipeIdx); 179 } else { 180 urlWithoutVersion = url; 181 } 182 183 HashSet<String> retVal = Sets.newHashSet(url, urlWithoutVersion); 184 185 Optional<IBase> versionValue = 186 resourceDef.getChildByName("version").getAccessor().getFirstValueOrNull(theResource); 187 String version = versionValue 188 .map(t -> (((IPrimitiveType<?>) t).getValueAsString())) 189 .orElse(null); 190 if (isNotBlank(version)) { 191 retVal.add(urlWithoutVersion + "|" + version); 192 } 193 194 return retVal; 195 } 196 197 /** 198 * Add a new StructureDefinition resource which will be available to the validator. Note that 199 * {@link StructureDefinition#getUrl() the URL field) in this resource must contain a value as this 200 * value will be used as the logical URL. 201 * <p> 202 * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension), 203 * it will be stored in three ways: 204 * <ul> 205 * <li>Extension</li> 206 * <li>StructureDefinition/Extension</li> 207 * <li>http://hl7.org/StructureDefinition/Extension</li> 208 * </ul> 209 * </p> 210 */ 211 public void addStructureDefinition(IBaseResource theStructureDefinition) { 212 validateNotLocked(); 213 Set<String> url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition"); 214 addToMap(theStructureDefinition, myStructureDefinitions, myUrlToStructureDefinitions, url); 215 } 216 217 public void addSearchParameter(IBaseResource theSearchParameter) { 218 validateNotLocked(); 219 Set<String> url = processResourceAndReturnUrls(theSearchParameter, "SearchParameter"); 220 addToMap(theSearchParameter, mySearchParameters, myUrlToSearchParameters, url); 221 } 222 223 private <T extends IBaseResource> void addToMap( 224 T theResource, List<T> theList, Map<String, T> theMap, Collection<String> theUrls) { 225 theList.add(theResource); 226 for (String urls : theUrls) { 227 if (isNotBlank(urls)) { 228 theMap.put(urls, theResource); 229 230 int lastSlashIdx = urls.lastIndexOf('/'); 231 if (lastSlashIdx != -1) { 232 theMap.put(urls.substring(lastSlashIdx + 1), theResource); 233 int previousSlashIdx = urls.lastIndexOf('/', lastSlashIdx - 1); 234 if (previousSlashIdx != -1) { 235 theMap.put(urls.substring(previousSlashIdx + 1), theResource); 236 } 237 } 238 } 239 } 240 } 241 242 /** 243 * Add a new ValueSet resource which will be available to the validator. Note that 244 * {@link ValueSet#getUrl() the URL field) in this resource must contain a value as this 245 * value will be used as the logical URL. 246 * <p> 247 * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension), 248 * it will be stored in three ways: 249 * <ul> 250 * <li>Extension</li> 251 * <li>StructureDefinition/Extension</li> 252 * <li>http://hl7.org/StructureDefinition/Extension</li> 253 * </ul> 254 * </p> 255 */ 256 public void addValueSet(IBaseResource theValueSet) { 257 validateNotLocked(); 258 Set<String> urls = processResourceAndReturnUrls(theValueSet, "ValueSet"); 259 addToMap(theValueSet, myValueSets, myUrlToValueSets, urls); 260 } 261 262 /** 263 * @param theResource The resource. This method delegates to the type-specific methods (e.g. {@link #addCodeSystem(IBaseResource)}) 264 * and will do nothing if the resource type is not supported by this class. 265 * @since 5.5.0 266 */ 267 public void addResource(@Nonnull IBaseResource theResource) { 268 validateNotLocked(); 269 Validate.notNull(theResource, "theResource must not be null"); 270 271 switch (getFhirContext().getResourceType(theResource)) { 272 case "SearchParameter": 273 addSearchParameter(theResource); 274 break; 275 case "StructureDefinition": 276 addStructureDefinition(theResource); 277 break; 278 case "CodeSystem": 279 addCodeSystem(theResource); 280 break; 281 case "ValueSet": 282 addValueSet(theResource); 283 break; 284 } 285 } 286 287 @Override 288 public List<IBaseResource> fetchAllConformanceResources() { 289 ArrayList<IBaseResource> retVal = new ArrayList<>(); 290 retVal.addAll(myCodeSystems); 291 retVal.addAll(myStructureDefinitions); 292 retVal.addAll(myValueSets); 293 return retVal; 294 } 295 296 @SuppressWarnings("unchecked") 297 @Nullable 298 @Override 299 public <T extends IBaseResource> List<T> fetchAllSearchParameters() { 300 return (List<T>) Collections.unmodifiableList(mySearchParameters); 301 } 302 303 @SuppressWarnings("unchecked") 304 @Override 305 public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { 306 return (List<T>) Collections.unmodifiableList(myStructureDefinitions); 307 } 308 309 @Override 310 public IBaseResource fetchCodeSystem(String theSystem) { 311 return myUrlToCodeSystems.get(theSystem); 312 } 313 314 @Override 315 public IBaseResource fetchValueSet(String theUri) { 316 return myUrlToValueSets.get(theUri); 317 } 318 319 @Override 320 public IBaseResource fetchStructureDefinition(String theUrl) { 321 return myUrlToStructureDefinitions.get(theUrl); 322 } 323 324 @Override 325 public byte[] fetchBinary(String theBinaryKey) { 326 return myBinaries.get(theBinaryKey); 327 } 328 329 @Override 330 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 331 return myUrlToCodeSystems.containsKey(theSystem); 332 } 333 334 @Override 335 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 336 return myUrlToValueSets.containsKey(theValueSetUrl); 337 } 338 339 /** 340 * Returns a count of all known resources 341 */ 342 public int countAll() { 343 return myBinaries.size() 344 + myCodeSystems.size() 345 + myStructureDefinitions.size() 346 + myValueSets.size() 347 + myStructureDefinitions.size(); 348 } 349 350 @Override 351 public synchronized void lock() { 352 myLocked = true; 353 } 354}