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}