
001package ca.uhn.fhir.jpa.packages; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 025import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 026import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 027import ca.uhn.fhir.context.FhirContext; 028import ca.uhn.fhir.context.FhirVersionEnum; 029import ca.uhn.fhir.context.support.IValidationSupport; 030import ca.uhn.fhir.context.support.ValidationSupportContext; 031import ca.uhn.fhir.interceptor.model.RequestPartitionId; 032import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 033import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 034import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 035import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; 036import ca.uhn.fhir.jpa.model.config.PartitionSettings; 037import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; 038import ca.uhn.fhir.jpa.partition.SystemRequestDetails; 039import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 040import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistryController; 041import ca.uhn.fhir.rest.api.server.IBundleProvider; 042import ca.uhn.fhir.rest.param.StringParam; 043import ca.uhn.fhir.rest.param.TokenParam; 044import ca.uhn.fhir.rest.param.UriParam; 045import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 046import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 047import ca.uhn.fhir.util.FhirTerser; 048import ca.uhn.fhir.util.SearchParameterUtil; 049import com.google.common.annotations.VisibleForTesting; 050import com.google.common.collect.Lists; 051import com.google.gson.Gson; 052import com.google.gson.JsonElement; 053import org.apache.commons.lang3.Validate; 054import org.hl7.fhir.instance.model.api.IBase; 055import org.hl7.fhir.instance.model.api.IBaseResource; 056import org.hl7.fhir.instance.model.api.IIdType; 057import org.hl7.fhir.instance.model.api.IPrimitiveType; 058import org.hl7.fhir.r4.model.Identifier; 059import org.hl7.fhir.utilities.npm.IPackageCacheManager; 060import org.hl7.fhir.utilities.npm.NpmPackage; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063import org.springframework.beans.factory.annotation.Autowired; 064import org.springframework.transaction.PlatformTransactionManager; 065import org.springframework.transaction.support.TransactionTemplate; 066 067import javax.annotation.Nonnull; 068import javax.annotation.PostConstruct; 069import java.io.IOException; 070import java.util.ArrayList; 071import java.util.Collection; 072import java.util.Collections; 073import java.util.HashMap; 074import java.util.List; 075import java.util.Map; 076import java.util.Optional; 077 078import static org.apache.commons.lang3.StringUtils.defaultString; 079import static org.apache.commons.lang3.StringUtils.isBlank; 080 081/** 082 * @since 5.1.0 083 */ 084public class PackageInstallerSvcImpl implements IPackageInstallerSvc { 085 086 private static final Logger ourLog = LoggerFactory.getLogger(PackageInstallerSvcImpl.class); 087 public static List<String> DEFAULT_INSTALL_TYPES = Collections.unmodifiableList(Lists.newArrayList( 088 "NamingSystem", 089 "CodeSystem", 090 "ValueSet", 091 "StructureDefinition", 092 "ConceptMap", 093 "SearchParameter", 094 "Subscription" 095 )); 096 097 boolean enabled = true; 098 @Autowired 099 private FhirContext myFhirContext; 100 @Autowired 101 private DaoRegistry myDaoRegistry; 102 @Autowired 103 private IValidationSupport validationSupport; 104 @Autowired 105 private IHapiPackageCacheManager myPackageCacheManager; 106 @Autowired 107 private PlatformTransactionManager myTxManager; 108 @Autowired 109 private INpmPackageVersionDao myPackageVersionDao; 110 @Autowired 111 private ISearchParamRegistry mySearchParamRegistry; 112 @Autowired 113 private ISearchParamRegistryController mySearchParamRegistryController; 114 @Autowired 115 private PartitionSettings myPartitionSettings; 116 /** 117 * Constructor 118 */ 119 public PackageInstallerSvcImpl() { 120 super(); 121 } 122 123 @PostConstruct 124 public void initialize() { 125 switch (myFhirContext.getVersion().getVersion()) { 126 case R5: 127 case R4: 128 case DSTU3: 129 break; 130 131 case DSTU2: 132 case DSTU2_HL7ORG: 133 case DSTU2_1: 134 default: { 135 ourLog.info("IG installation not supported for version: {}", myFhirContext.getVersion().getVersion()); 136 enabled = false; 137 } 138 } 139 } 140 141 /** 142 * Loads and installs an IG from a file on disk or the Simplifier repo using 143 * the {@link IPackageCacheManager}. 144 * <p> 145 * Installs the IG by persisting instances of the following types of resources: 146 * <p> 147 * - NamingSystem, CodeSystem, ValueSet, StructureDefinition (with snapshots), 148 * ConceptMap, SearchParameter, Subscription 149 * <p> 150 * Creates the resources if non-existent, updates them otherwise. 151 * 152 * @param theInstallationSpec The details about what should be installed 153 */ 154 @SuppressWarnings("ConstantConditions") 155 @Override 156 public PackageInstallOutcomeJson install(PackageInstallationSpec theInstallationSpec) throws ImplementationGuideInstallationException { 157 PackageInstallOutcomeJson retVal = new PackageInstallOutcomeJson(); 158 if (enabled) { 159 try { 160 161 boolean exists = new TransactionTemplate(myTxManager).execute(tx -> { 162 Optional<NpmPackageVersionEntity> existing = myPackageVersionDao.findByPackageIdAndVersion(theInstallationSpec.getName(), theInstallationSpec.getVersion()); 163 return existing.isPresent(); 164 }); 165 if (exists) { 166 ourLog.info("Package {}#{} is already installed", theInstallationSpec.getName(), theInstallationSpec.getVersion()); 167 } 168 169 NpmPackage npmPackage = myPackageCacheManager.installPackage(theInstallationSpec); 170 if (npmPackage == null) { 171 throw new IOException(Msg.code(1284) + "Package not found"); 172 } 173 174 retVal.getMessage().addAll(JpaPackageCache.getProcessingMessages(npmPackage)); 175 176 if (theInstallationSpec.isFetchDependencies()) { 177 fetchAndInstallDependencies(npmPackage, theInstallationSpec, retVal); 178 } 179 180 if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { 181 install(npmPackage, theInstallationSpec, retVal); 182 183 // If any SearchParameters were installed, let's load them right away 184 mySearchParamRegistryController.refreshCacheIfNecessary(); 185 } 186 187 } catch (IOException e) { 188 throw new ImplementationGuideInstallationException(Msg.code(1285) + "Could not load NPM package " + theInstallationSpec.getName() + "#" + theInstallationSpec.getVersion(), e); 189 } 190 } 191 192 return retVal; 193 } 194 195 /** 196 * Installs a package and its dependencies. 197 * <p> 198 * Fails fast if one of its dependencies could not be installed. 199 * 200 * @throws ImplementationGuideInstallationException if installation fails 201 */ 202 private void install(NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome) throws ImplementationGuideInstallationException { 203 String name = npmPackage.getNpm().get("name").getAsString(); 204 String version = npmPackage.getNpm().get("version").getAsString(); 205 206 String fhirVersion = npmPackage.fhirVersion(); 207 String currentFhirVersion = myFhirContext.getVersion().getVersion().getFhirVersionString(); 208 assertFhirVersionsAreCompatible(fhirVersion, currentFhirVersion); 209 210 List<String> installTypes; 211 if (!theInstallationSpec.getInstallResourceTypes().isEmpty()) { 212 installTypes = theInstallationSpec.getInstallResourceTypes(); 213 } else { 214 installTypes = DEFAULT_INSTALL_TYPES; 215 } 216 217 ourLog.info("Installing package: {}#{}", name, version); 218 int[] count = new int[installTypes.size()]; 219 220 for (int i = 0; i < installTypes.size(); i++) { 221 Collection<IBaseResource> resources = parseResourcesOfType(installTypes.get(i), npmPackage); 222 count[i] = resources.size(); 223 224 for (IBaseResource next : resources) { 225 226 try { 227 next = isStructureDefinitionWithoutSnapshot(next) ? generateSnapshot(next) : next; 228 create(next, theOutcome); 229 } catch (Exception e) { 230 ourLog.warn("Failed to upload resource of type {} with ID {} - Error: {}", myFhirContext.getResourceType(next), next.getIdElement().getValue(), e.toString()); 231 throw new ImplementationGuideInstallationException(Msg.code(1286) + String.format("Error installing IG %s#%s: %s", name, version, e), e); 232 } 233 234 } 235 236 } 237 ourLog.info(String.format("Finished installation of package %s#%s:", name, version)); 238 239 for (int i = 0; i < count.length; i++) { 240 ourLog.info(String.format("-- Created or updated %s resources of type %s", count[i], installTypes.get(i))); 241 } 242 } 243 244 private void fetchAndInstallDependencies(NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome) throws ImplementationGuideInstallationException { 245 if (npmPackage.getNpm().has("dependencies")) { 246 JsonElement dependenciesElement = npmPackage.getNpm().get("dependencies"); 247 Map<String, String> dependencies = new Gson().fromJson(dependenciesElement, HashMap.class); 248 for (Map.Entry<String, String> d : dependencies.entrySet()) { 249 String id = d.getKey(); 250 String ver = d.getValue(); 251 try { 252 theOutcome.getMessage().add("Package " + npmPackage.id() + "#" + npmPackage.version() + " depends on package " + id + "#" + ver); 253 254 boolean skip = false; 255 for (String next : theInstallationSpec.getDependencyExcludes()) { 256 if (id.matches(next)) { 257 theOutcome.getMessage().add("Not installing dependency " + id + " because it matches exclude criteria: " + next); 258 skip = true; 259 break; 260 } 261 } 262 if (skip) { 263 continue; 264 } 265 266 // resolve in local cache or on packages.fhir.org 267 NpmPackage dependency = myPackageCacheManager.loadPackage(id, ver); 268 // recursive call to install dependencies of a package before 269 // installing the package 270 fetchAndInstallDependencies(dependency, theInstallationSpec, theOutcome); 271 272 if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { 273 install(dependency, theInstallationSpec, theOutcome); 274 } 275 276 } catch (IOException e) { 277 throw new ImplementationGuideInstallationException(Msg.code(1287) + String.format( 278 "Cannot resolve dependency %s#%s", id, ver), e); 279 } 280 } 281 } 282 } 283 284 /** 285 * Asserts if package FHIR version is compatible with current FHIR version 286 * by using semantic versioning rules. 287 */ 288 private void assertFhirVersionsAreCompatible(String fhirVersion, String currentFhirVersion) 289 throws ImplementationGuideInstallationException { 290 291 FhirVersionEnum fhirVersionEnum = FhirVersionEnum.forVersionString(fhirVersion); 292 FhirVersionEnum currentFhirVersionEnum = FhirVersionEnum.forVersionString(currentFhirVersion); 293 Validate.notNull(fhirVersionEnum, "Invalid FHIR version string: %s", fhirVersion); 294 Validate.notNull(currentFhirVersionEnum, "Invalid FHIR version string: %s", currentFhirVersion); 295 boolean compatible = fhirVersionEnum.equals(currentFhirVersionEnum); 296 if (!compatible) { 297 throw new ImplementationGuideInstallationException(Msg.code(1288) + String.format( 298 "Cannot install implementation guide: FHIR versions mismatch (expected <=%s, package uses %s)", 299 currentFhirVersion, fhirVersion)); 300 } 301 } 302 303 /** 304 * ============================= Utility methods =============================== 305 */ 306 307 private List<IBaseResource> parseResourcesOfType(String type, NpmPackage pkg) { 308 if (!pkg.getFolders().containsKey("package")) { 309 return Collections.emptyList(); 310 } 311 ArrayList<IBaseResource> resources = new ArrayList<>(); 312 List<String> filesForType = pkg.getFolders().get("package").getTypes().get(type); 313 if (filesForType != null) { 314 for (String file : filesForType) { 315 try { 316 byte[] content = pkg.getFolders().get("package").fetchFile(file); 317 resources.add(myFhirContext.newJsonParser().parseResource(new String(content))); 318 } catch (IOException e) { 319 throw new InternalErrorException(Msg.code(1289) + "Cannot install resource of type " + type + ": Could not fetch file " + file, e); 320 } 321 } 322 } 323 return resources; 324 } 325 326 private void create(IBaseResource theResource, PackageInstallOutcomeJson theOutcome) { 327 IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); 328 SearchParameterMap map = createSearchParameterMapFor(theResource); 329 IBundleProvider searchResult = searchResource(dao, map); 330 if (validForUpload(theResource)) { 331 if (searchResult.isEmpty()) { 332 333 ourLog.info("Creating new resource matching {}", map.toNormalizedQueryString(myFhirContext)); 334 theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource)); 335 336 IIdType id = theResource.getIdElement(); 337 338 if (id.isEmpty()) { 339 createResource(dao, theResource); 340 ourLog.info("Created resource with new id"); 341 } else { 342 if (id.isIdPartValidLong()) { 343 String newIdPart = "npm-" + id.getIdPart(); 344 id.setParts(id.getBaseUrl(), id.getResourceType(), newIdPart, id.getVersionIdPart()); 345 } 346 updateResource(dao, theResource); 347 ourLog.info("Created resource with existing id"); 348 } 349 } else { 350 ourLog.info("Updating existing resource matching {}", map.toNormalizedQueryString(myFhirContext)); 351 theResource.setId(searchResult.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); 352 DaoMethodOutcome outcome = updateResource(dao, theResource); 353 if (!outcome.isNop()) { 354 theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource)); 355 } 356 } 357 } 358 else{ 359 ourLog.warn("Failed to upload resource of type {} with ID {} - Error: Resource failed validation", theResource.fhirType(), theResource.getIdElement().getValue()); 360 } 361 } 362 363 private IBundleProvider searchResource(IFhirResourceDao theDao, SearchParameterMap theMap) { 364 if (myPartitionSettings.isPartitioningEnabled()) { 365 SystemRequestDetails requestDetails = newSystemRequestDetails(); 366 return theDao.search(theMap, requestDetails); 367 } else { 368 return theDao.search(theMap); 369 } 370 } 371 372 @Nonnull 373 private SystemRequestDetails newSystemRequestDetails() { 374 return 375 new SystemRequestDetails() 376 .setRequestPartitionId(RequestPartitionId.defaultPartition()); 377 } 378 379 private void createResource(IFhirResourceDao theDao, IBaseResource theResource) { 380 if (myPartitionSettings.isPartitioningEnabled()) { 381 SystemRequestDetails requestDetails = newSystemRequestDetails(); 382 theDao.create(theResource, requestDetails); 383 } else { 384 theDao.create(theResource); 385 } 386 } 387 388 private DaoMethodOutcome updateResource(IFhirResourceDao theDao, IBaseResource theResource) { 389 if (myPartitionSettings.isPartitioningEnabled()) { 390 SystemRequestDetails requestDetails = newSystemRequestDetails(); 391 return theDao.update(theResource, requestDetails); 392 } else { 393 return theDao.update(theResource); 394 } 395 } 396 397 boolean validForUpload(IBaseResource theResource) { 398 String resourceType = myFhirContext.getResourceType(theResource); 399 if ("SearchParameter".equals(resourceType)) { 400 401 String code = SearchParameterUtil.getCode(myFhirContext, theResource); 402 if (defaultString(code).startsWith("_")) { 403 ourLog.warn("Failed to validate resource of type {} with url {} - Error: Resource code starts with \"_\"", theResource.fhirType(), SearchParameterUtil.getURL(myFhirContext, theResource)); 404 return false; 405 } 406 407 String expression = SearchParameterUtil.getExpression(myFhirContext, theResource); 408 if (isBlank(expression)) { 409 ourLog.warn("Failed to validate resource of type {} with url {} - Error: Resource expression is blank", theResource.fhirType(), SearchParameterUtil.getURL(myFhirContext, theResource)); 410 return false; 411 } 412 413 if (SearchParameterUtil.getBaseAsStrings(myFhirContext, theResource).isEmpty()) { 414 ourLog.warn("Failed to validate resource of type {} with url {} - Error: Resource base is empty", theResource.fhirType(), SearchParameterUtil.getURL(myFhirContext, theResource)); 415 return false; 416 } 417 418 } 419 420 if (!isValidResourceStatusForPackageUpload(theResource)) { 421 ourLog.warn("Failed to validate resource of type {} with ID {} - Error: Resource status not accepted value.", 422 theResource.fhirType(), theResource.getIdElement().getValue()); 423 return false; 424 } 425 426 return true; 427 } 428 429 /** 430 * For resources like {@link org.hl7.fhir.r4.model.Subscription}, {@link org.hl7.fhir.r4.model.DocumentReference}, 431 * and {@link org.hl7.fhir.r4.model.Communication}, the status field doesn't necessarily need to be set to 'active' 432 * for that resource to be eligible for upload via packages. For example, all {@link org.hl7.fhir.r4.model.Subscription} 433 * have a status of {@link org.hl7.fhir.r4.model.Subscription.SubscriptionStatus#REQUESTED} when they are originally 434 * inserted into the database, so we accept that value for {@link org.hl7.fhir.r4.model.Subscription} isntead. 435 * Furthermore, {@link org.hl7.fhir.r4.model.DocumentReference} and {@link org.hl7.fhir.r4.model.Communication} can 436 * exist with a wide variety of values for status that include ones such as 437 * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#ENTEREDINERROR}, 438 * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#UNKNOWN}, 439 * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#ENTEREDINERROR}, 440 * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#PRELIMINARY}, and others, which while not considered 441 * 'final' values, should still be uploaded for reference. 442 * 443 * @return {@link Boolean#TRUE} if the status value of this resource is acceptable for package upload. 444 */ 445 private boolean isValidResourceStatusForPackageUpload(IBaseResource theResource) { 446 List<IPrimitiveType> statusTypes = myFhirContext.newFhirPath().evaluate(theResource, "status", IPrimitiveType.class); 447 // Resource does not have a status field 448 if (statusTypes.isEmpty()) return true; 449 // Resource has a null status field 450 if (statusTypes.get(0).getValue() == null) return false; 451 // Resource has a status, and we need to check based on type 452 switch (theResource.fhirType()) { 453 case "Subscription": 454 return (statusTypes.get(0).getValueAsString().equals("requested")); 455 case "DocumentReference": 456 case "Communication": 457 return (!statusTypes.get(0).getValueAsString().equals("?")); 458 default: 459 return (statusTypes.get(0).getValueAsString().equals("active")); 460 } 461 } 462 463 private boolean isStructureDefinitionWithoutSnapshot(IBaseResource r) { 464 boolean retVal = false; 465 FhirTerser terser = myFhirContext.newTerser(); 466 if (r.getClass().getSimpleName().equals("StructureDefinition")) { 467 Optional<String> kind = terser.getSinglePrimitiveValue(r, "kind"); 468 if (kind.isPresent() && !(kind.get().equals("logical"))) { 469 retVal = terser.getSingleValueOrNull(r, "snapshot") == null; 470 } 471 } 472 return retVal; 473 } 474 475 private IBaseResource generateSnapshot(IBaseResource sd) { 476 try { 477 return validationSupport.generateSnapshot(new ValidationSupportContext(validationSupport), sd, null, null, null); 478 } catch (Exception e) { 479 throw new ImplementationGuideInstallationException(Msg.code(1290) + String.format( 480 "Failure when generating snapshot of StructureDefinition: %s", sd.getIdElement()), e); 481 } 482 } 483 484 private SearchParameterMap createSearchParameterMapFor(IBaseResource resource) { 485 if (resource.getClass().getSimpleName().equals("NamingSystem")) { 486 String uniqueId = extractUniqeIdFromNamingSystem(resource); 487 return SearchParameterMap.newSynchronous().add("value", new StringParam(uniqueId).setExact(true)); 488 } else if (resource.getClass().getSimpleName().equals("Subscription")) { 489 String id = extractIdFromSubscription(resource); 490 return SearchParameterMap.newSynchronous().add("_id", new TokenParam(id)); 491 } else if (resourceHasUrlElement(resource)) { 492 String url = extractUniqueUrlFromMetadataResource(resource); 493 return SearchParameterMap.newSynchronous().add("url", new UriParam(url)); 494 } else { 495 TokenParam identifierToken = extractIdentifierFromOtherResourceTypes(resource); 496 return SearchParameterMap.newSynchronous().add("identifier", identifierToken); 497 } 498 } 499 500 private String extractUniqeIdFromNamingSystem(IBaseResource resource) { 501 FhirTerser terser = myFhirContext.newTerser(); 502 IBase uniqueIdComponent = (IBase) terser.getSingleValueOrNull(resource, "uniqueId"); 503 if (uniqueIdComponent == null) { 504 throw new ImplementationGuideInstallationException(Msg.code(1291) + "NamingSystem does not have uniqueId component."); 505 } 506 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(uniqueIdComponent, "value"); 507 return (String) asPrimitiveType.getValue(); 508 } 509 510 private String extractIdFromSubscription(IBaseResource resource) { 511 FhirTerser terser = myFhirContext.newTerser(); 512 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(resource, "id"); 513 return (String) asPrimitiveType.getValue(); 514 } 515 516 private String extractUniqueUrlFromMetadataResource(IBaseResource resource) { 517 FhirTerser terser = myFhirContext.newTerser(); 518 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(resource, "url"); 519 return (String) asPrimitiveType.getValue(); 520 } 521 522 private TokenParam extractIdentifierFromOtherResourceTypes(IBaseResource resource) { 523 FhirTerser terser = myFhirContext.newTerser(); 524 Identifier identifier = (Identifier) terser.getSingleValueOrNull(resource, "identifier"); 525 if (identifier != null) { 526 return new TokenParam(identifier.getSystem(), identifier.getValue()); 527 } else { 528 throw new UnsupportedOperationException(Msg.code(1292) + "Resources in a package must have a url or identifier to be loaded by the package installer."); 529 } 530 } 531 532 private boolean resourceHasUrlElement(IBaseResource resource) { 533 BaseRuntimeElementDefinition<?> def = myFhirContext.getElementDefinition(resource.getClass()); 534 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 535 throw new IllegalArgumentException(Msg.code(1293) + "Resource is not a composite type: " + resource.getClass().getName()); 536 } 537 BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def; 538 BaseRuntimeChildDefinition nextDef = currentDef.getChildByName("url"); 539 return nextDef != null; 540 } 541 542 @VisibleForTesting 543 void setFhirContextForUnitTest(FhirContext theCtx) { 544 myFhirContext = theCtx; 545 } 546 547}