001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.packages;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.FhirVersionEnum;
027import ca.uhn.fhir.context.support.IValidationSupport;
028import ca.uhn.fhir.context.support.ValidationSupportContext;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.interceptor.model.RequestPartitionId;
031import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
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.dao.tx.IHapiTransactionService;
037import ca.uhn.fhir.jpa.dao.validation.SearchParameterDaoValidator;
038import ca.uhn.fhir.jpa.model.config.PartitionSettings;
039import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity;
040import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc;
041import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
042import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistryController;
043import ca.uhn.fhir.jpa.searchparam.util.SearchParameterHelper;
044import ca.uhn.fhir.rest.api.server.IBundleProvider;
045import ca.uhn.fhir.rest.api.server.RequestDetails;
046import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
047import ca.uhn.fhir.rest.param.StringParam;
048import ca.uhn.fhir.rest.param.TokenParam;
049import ca.uhn.fhir.rest.param.UriParam;
050import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
051import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
052import ca.uhn.fhir.util.FhirTerser;
053import ca.uhn.fhir.util.SearchParameterUtil;
054import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
055import com.google.common.annotations.VisibleForTesting;
056import jakarta.annotation.PostConstruct;
057import org.apache.commons.lang3.Validate;
058import org.hl7.fhir.instance.model.api.IBase;
059import org.hl7.fhir.instance.model.api.IBaseResource;
060import org.hl7.fhir.instance.model.api.IIdType;
061import org.hl7.fhir.instance.model.api.IPrimitiveType;
062import org.hl7.fhir.r4.model.Identifier;
063import org.hl7.fhir.r4.model.MetadataResource;
064import org.hl7.fhir.utilities.json.model.JsonObject;
065import org.hl7.fhir.utilities.npm.IPackageCacheManager;
066import org.hl7.fhir.utilities.npm.NpmPackage;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069import org.springframework.beans.factory.annotation.Autowired;
070
071import java.io.IOException;
072import java.util.Collection;
073import java.util.HashSet;
074import java.util.List;
075import java.util.Optional;
076
077import static ca.uhn.fhir.jpa.packages.util.PackageUtils.DEFAULT_INSTALL_TYPES;
078import static ca.uhn.fhir.util.SearchParameterUtil.getBaseAsStrings;
079
080/**
081 * @since 5.1.0
082 */
083public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
084
085        private static final Logger ourLog = LoggerFactory.getLogger(PackageInstallerSvcImpl.class);
086
087        boolean enabled = true;
088
089        @Autowired
090        private FhirContext myFhirContext;
091
092        @Autowired
093        private DaoRegistry myDaoRegistry;
094
095        @Autowired
096        private IValidationSupport validationSupport;
097
098        @Autowired
099        private IHapiPackageCacheManager myPackageCacheManager;
100
101        @Autowired
102        private IHapiTransactionService myTxService;
103
104        @Autowired
105        private INpmPackageVersionDao myPackageVersionDao;
106
107        @Autowired
108        private ISearchParamRegistryController mySearchParamRegistryController;
109
110        @Autowired
111        private PartitionSettings myPartitionSettings;
112
113        @Autowired
114        private SearchParameterHelper mySearchParameterHelper;
115
116        @Autowired
117        private PackageResourceParsingSvc myPackageResourceParsingSvc;
118
119        @Autowired
120        private JpaStorageSettings myStorageSettings;
121
122        @Autowired
123        private SearchParameterDaoValidator mySearchParameterDaoValidator;
124
125        @Autowired
126        private VersionCanonicalizer myVersionCanonicalizer;
127
128        /**
129         * Constructor
130         */
131        public PackageInstallerSvcImpl() {
132                super();
133        }
134
135        @PostConstruct
136        public void initialize() {
137                switch (myFhirContext.getVersion().getVersion()) {
138                        case R5:
139                        case R4B:
140                        case R4:
141                        case DSTU3:
142                                break;
143
144                        case DSTU2:
145                        case DSTU2_HL7ORG:
146                        case DSTU2_1:
147                        default: {
148                                ourLog.info(
149                                                "IG installation not supported for version: {}",
150                                                myFhirContext.getVersion().getVersion());
151                                enabled = false;
152                        }
153                }
154        }
155
156        @Override
157        public PackageDeleteOutcomeJson uninstall(PackageInstallationSpec theInstallationSpec) {
158                PackageDeleteOutcomeJson outcome =
159                                myPackageCacheManager.uninstallPackage(theInstallationSpec.getName(), theInstallationSpec.getVersion());
160                validationSupport.invalidateCaches();
161                return outcome;
162        }
163
164        /**
165         * Loads and installs an IG from a file on disk or the Simplifier repo using
166         * the {@link IPackageCacheManager}.
167         * <p>
168         * Installs the IG by persisting instances of the following types of resources:
169         * <p>
170         * - NamingSystem, CodeSystem, ValueSet, StructureDefinition (with snapshots),
171         * ConceptMap, SearchParameter, Subscription
172         * <p>
173         * Creates the resources if non-existent, updates them otherwise.
174         *
175         * @param theInstallationSpec The details about what should be installed
176         */
177        @SuppressWarnings("ConstantConditions")
178        @Override
179        public PackageInstallOutcomeJson install(PackageInstallationSpec theInstallationSpec)
180                        throws ImplementationGuideInstallationException {
181                PackageInstallOutcomeJson retVal = new PackageInstallOutcomeJson();
182                if (enabled) {
183                        try {
184
185                                boolean exists = myTxService
186                                                .withSystemRequest()
187                                                .withRequestPartitionId(RequestPartitionId.defaultPartition())
188                                                .execute(() -> {
189                                                        Optional<NpmPackageVersionEntity> existing = myPackageVersionDao.findByPackageIdAndVersion(
190                                                                        theInstallationSpec.getName(), theInstallationSpec.getVersion());
191                                                        return existing.isPresent();
192                                                });
193                                if (exists) {
194                                        ourLog.info(
195                                                        "Package {}#{} is already installed",
196                                                        theInstallationSpec.getName(),
197                                                        theInstallationSpec.getVersion());
198                                }
199
200                                NpmPackage npmPackage = myPackageCacheManager.installPackage(theInstallationSpec);
201                                if (npmPackage == null) {
202                                        throw new IOException(Msg.code(1284) + "Package not found");
203                                }
204
205                                retVal.getMessage().addAll(JpaPackageCache.getProcessingMessages(npmPackage));
206
207                                if (theInstallationSpec.isFetchDependencies()) {
208                                        fetchAndInstallDependencies(npmPackage, theInstallationSpec, retVal);
209                                }
210
211                                if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) {
212                                        install(npmPackage, theInstallationSpec, retVal);
213
214                                        // If any SearchParameters were installed, let's load them right away
215                                        mySearchParamRegistryController.refreshCacheIfNecessary();
216                                }
217
218                                validationSupport.invalidateCaches();
219
220                        } catch (IOException e) {
221                                throw new ImplementationGuideInstallationException(
222                                                Msg.code(1285) + "Could not load NPM package " + theInstallationSpec.getName() + "#"
223                                                                + theInstallationSpec.getVersion(),
224                                                e);
225                        }
226                }
227
228                return retVal;
229        }
230
231        /**
232         * Installs a package and its dependencies.
233         * <p>
234         * Fails fast if one of its dependencies could not be installed.
235         *
236         * @throws ImplementationGuideInstallationException if installation fails
237         */
238        private void install(
239                        NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome)
240                        throws ImplementationGuideInstallationException {
241                String name = npmPackage.getNpm().get("name").asJsonString().getValue();
242                String version = npmPackage.getNpm().get("version").asJsonString().getValue();
243
244                String fhirVersion = npmPackage.fhirVersion();
245                String currentFhirVersion = myFhirContext.getVersion().getVersion().getFhirVersionString();
246                assertFhirVersionsAreCompatible(fhirVersion, currentFhirVersion);
247
248                List<String> installTypes;
249                if (!theInstallationSpec.getInstallResourceTypes().isEmpty()) {
250                        installTypes = theInstallationSpec.getInstallResourceTypes();
251                } else {
252                        installTypes = DEFAULT_INSTALL_TYPES;
253                }
254
255                ourLog.info("Installing package: {}#{}", name, version);
256                int[] count = new int[installTypes.size()];
257
258                for (int i = 0; i < installTypes.size(); i++) {
259                        String type = installTypes.get(i);
260
261                        Collection<IBaseResource> resources = myPackageResourceParsingSvc.parseResourcesOfType(type, npmPackage);
262                        count[i] = resources.size();
263
264                        for (IBaseResource next : resources) {
265                                try {
266                                        next = isStructureDefinitionWithoutSnapshot(next) ? generateSnapshot(next) : next;
267                                        install(next, theInstallationSpec, theOutcome);
268                                } catch (Exception e) {
269                                        ourLog.warn(
270                                                        "Failed to upload resource of type {} with ID {} - Error: {}",
271                                                        myFhirContext.getResourceType(next),
272                                                        next.getIdElement().getValue(),
273                                                        e.toString());
274                                        throw new ImplementationGuideInstallationException(
275                                                        Msg.code(1286) + String.format("Error installing IG %s#%s: %s", name, version, e), e);
276                                }
277                        }
278                }
279                ourLog.info(String.format("Finished installation of package %s#%s:", name, version));
280
281                for (int i = 0; i < count.length; i++) {
282                        ourLog.info(String.format("-- Created or updated %s resources of type %s", count[i], installTypes.get(i)));
283                }
284        }
285
286        private void fetchAndInstallDependencies(
287                        NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome)
288                        throws ImplementationGuideInstallationException {
289                if (npmPackage.getNpm().has("dependencies")) {
290                        JsonObject dependenciesElement =
291                                        npmPackage.getNpm().get("dependencies").asJsonObject();
292                        for (String id : dependenciesElement.getNames()) {
293                                String ver = dependenciesElement.getJsonString(id).asString();
294                                try {
295                                        theOutcome
296                                                        .getMessage()
297                                                        .add("Package " + npmPackage.id() + "#" + npmPackage.version() + " depends on package " + id
298                                                                        + "#" + ver);
299
300                                        boolean skip = false;
301                                        for (String next : theInstallationSpec.getDependencyExcludes()) {
302                                                if (id.matches(next)) {
303                                                        theOutcome
304                                                                        .getMessage()
305                                                                        .add("Not installing dependency " + id + " because it matches exclude criteria: "
306                                                                                        + next);
307                                                        skip = true;
308                                                        break;
309                                                }
310                                        }
311                                        if (skip) {
312                                                continue;
313                                        }
314
315                                        // resolve in local cache or on packages.fhir.org
316                                        NpmPackage dependency = myPackageCacheManager.loadPackage(id, ver);
317                                        // recursive call to install dependencies of a package before
318                                        // installing the package
319                                        fetchAndInstallDependencies(dependency, theInstallationSpec, theOutcome);
320
321                                        if (theInstallationSpec.getInstallMode()
322                                                        == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) {
323                                                install(dependency, theInstallationSpec, theOutcome);
324                                        }
325
326                                } catch (IOException e) {
327                                        throw new ImplementationGuideInstallationException(
328                                                        Msg.code(1287) + String.format("Cannot resolve dependency %s#%s", id, ver), e);
329                                }
330                        }
331                }
332        }
333
334        /**
335         * Asserts if package FHIR version is compatible with current FHIR version
336         * by using semantic versioning rules.
337         */
338        protected void assertFhirVersionsAreCompatible(String fhirVersion, String currentFhirVersion)
339                        throws ImplementationGuideInstallationException {
340
341                FhirVersionEnum fhirVersionEnum = FhirVersionEnum.forVersionString(fhirVersion);
342                FhirVersionEnum currentFhirVersionEnum = FhirVersionEnum.forVersionString(currentFhirVersion);
343                Validate.notNull(fhirVersionEnum, "Invalid FHIR version string: %s", fhirVersion);
344                Validate.notNull(currentFhirVersionEnum, "Invalid FHIR version string: %s", currentFhirVersion);
345                boolean compatible = fhirVersionEnum.equals(currentFhirVersionEnum);
346                if (!compatible && fhirVersion.startsWith("R4") && currentFhirVersion.startsWith("R4")) {
347                        compatible = true;
348                }
349                if (!compatible) {
350                        throw new ImplementationGuideInstallationException(Msg.code(1288)
351                                        + String.format(
352                                                        "Cannot install implementation guide: FHIR versions mismatch (expected <=%s, package uses %s)",
353                                                        currentFhirVersion, fhirVersion));
354                }
355        }
356
357        /**
358         * ============================= Utility methods ===============================
359         */
360        @VisibleForTesting
361        void install(
362                        IBaseResource theResource,
363                        PackageInstallationSpec theInstallationSpec,
364                        PackageInstallOutcomeJson theOutcome) {
365
366                if (!validForUpload(theResource)) {
367                        ourLog.warn(
368                                        "Failed to upload resource of type {} with ID {} - Error: Resource failed validation",
369                                        theResource.fhirType(),
370                                        theResource.getIdElement().getValue());
371                        return;
372                }
373
374                IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
375                SearchParameterMap map = createSearchParameterMapFor(theResource);
376                IBundleProvider searchResult = searchResource(dao, map);
377
378                String resourceQuery = map.toNormalizedQueryString(myFhirContext);
379                if (!searchResult.isEmpty() && !theInstallationSpec.isReloadExisting()) {
380                        ourLog.info("Skipping update of existing resource matching {}", resourceQuery);
381                        return;
382                }
383                if (!searchResult.isEmpty()) {
384                        ourLog.info("Updating existing resource matching {}", resourceQuery);
385                }
386                IBaseResource existingResource =
387                                !searchResult.isEmpty() ? searchResult.getResources(0, 1).get(0) : null;
388                boolean isInstalled = createOrUpdateResource(dao, theResource, existingResource);
389                if (isInstalled) {
390                        theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource));
391                }
392        }
393
394        private Optional<IBaseResource> readResourceById(IFhirResourceDao dao, IIdType id) {
395                try {
396                        return Optional.ofNullable(dao.read(id.toUnqualifiedVersionless(), createRequestDetails()));
397
398                } catch (Exception exception) {
399                        // ignore because we're running this query to help build the log
400                        ourLog.warn("Exception when trying to read resource with ID: {}, message: {}", id, exception.getMessage());
401                }
402
403                return Optional.empty();
404        }
405
406        private IBundleProvider searchResource(IFhirResourceDao theDao, SearchParameterMap theMap) {
407                return theDao.search(theMap, createRequestDetails());
408        }
409
410        protected boolean createOrUpdateResource(
411                        IFhirResourceDao theDao, IBaseResource theResource, IBaseResource theExistingResource) {
412                final IIdType id = theResource.getIdElement();
413
414                if (theExistingResource == null && id.isEmpty()) {
415                        ourLog.debug("Install resource without id will be created");
416                        theDao.create(theResource, createRequestDetails());
417                        return true;
418                }
419
420                if (theExistingResource == null && !id.isEmpty() && id.isIdPartValidLong()) {
421                        String newIdPart = "npm-" + id.getIdPart();
422                        id.setParts(id.getBaseUrl(), id.getResourceType(), newIdPart, id.getVersionIdPart());
423                }
424
425                boolean isExistingUpdated = updateExistingResourceIfNecessary(theDao, theResource, theExistingResource);
426                boolean shouldOverrideId = theExistingResource != null && !isExistingUpdated;
427
428                if (shouldOverrideId) {
429                        ourLog.debug(
430                                        "Existing resource {} will be overridden with installed resource {}",
431                                        theExistingResource.getIdElement(),
432                                        id);
433                        theResource.setId(theExistingResource.getIdElement().toUnqualifiedVersionless());
434                } else {
435                        ourLog.debug("Install resource {} will be created", id);
436                }
437
438                DaoMethodOutcome outcome = updateResource(theDao, theResource);
439                return outcome != null && !outcome.isNop();
440        }
441
442        /*
443         * This function helps preserve the resource types in the base of an existing SP when an overriding SP's base
444         * covers only a subset of the existing base.
445         *
446         * For example, say for an existing SP,
447         *  -  the current base is: [ResourceTypeA, ResourceTypeB]
448         *   - the new base is: [ResourceTypeB]
449         *
450         * If we were to overwrite the existing SP's base to the new base ([ResourceTypeB]) then the
451         * SP would stop working on ResourceTypeA, which would be a loss of functionality.
452         *
453         * Instead, this function updates the existing SP's base by removing the resource types that
454         * are covered by the overriding SP.
455         * In our example, this function updates the existing SP's base to [ResourceTypeA], so that the existing SP
456         * still works on ResourceTypeA, and the caller then creates a new SP that covers ResourceTypeB.
457         * https://github.com/hapifhir/hapi-fhir/issues/5366
458         */
459        private boolean updateExistingResourceIfNecessary(
460                        IFhirResourceDao theDao, IBaseResource theResource, IBaseResource theExistingResource) {
461                if (!"SearchParameter".equals(theResource.getClass().getSimpleName())) {
462                        return false;
463                }
464                if (theExistingResource == null) {
465                        return false;
466                }
467                if (theExistingResource
468                                .getIdElement()
469                                .getIdPart()
470                                .equals(theResource.getIdElement().getIdPart())) {
471                        return false;
472                }
473                Collection<String> remainingBaseList = new HashSet<>(getBaseAsStrings(myFhirContext, theExistingResource));
474                remainingBaseList.removeAll(getBaseAsStrings(myFhirContext, theResource));
475                if (remainingBaseList.isEmpty()) {
476                        return false;
477                }
478                myFhirContext
479                                .getResourceDefinition(theExistingResource)
480                                .getChildByName("base")
481                                .getMutator()
482                                .setValue(theExistingResource, null);
483
484                for (String baseResourceName : remainingBaseList) {
485                        myFhirContext.newTerser().addElement(theExistingResource, "base", baseResourceName);
486                }
487                ourLog.info(
488                                "Existing SearchParameter {} will be updated with base {}",
489                                theExistingResource.getIdElement().getIdPart(),
490                                remainingBaseList);
491                updateResource(theDao, theExistingResource);
492                return true;
493        }
494
495        private DaoMethodOutcome updateResource(IFhirResourceDao theDao, IBaseResource theResource) {
496                DaoMethodOutcome outcome = null;
497
498                IIdType id = theResource.getIdElement();
499                RequestDetails requestDetails = createRequestDetails();
500
501                try {
502                        outcome = theDao.update(theResource, requestDetails);
503                } catch (ResourceVersionConflictException exception) {
504                        final Optional<IBaseResource> optResource = readResourceById(theDao, id);
505
506                        final String existingResourceUrlOrNull = optResource
507                                        .filter(MetadataResource.class::isInstance)
508                                        .map(MetadataResource.class::cast)
509                                        .map(MetadataResource::getUrl)
510                                        .orElse(null);
511                        final String newResourceUrlOrNull =
512                                        (theResource instanceof MetadataResource) ? ((MetadataResource) theResource).getUrl() : null;
513
514                        ourLog.error(
515                                        "Version conflict error: This is possibly due to a collision between ValueSets from different IGs that are coincidentally using the same resource ID: [{}] and new resource URL: [{}], with the exisitng resource having URL: [{}].  Ignoring this update and continuing:  The first IG wins.  ",
516                                        id.getIdPart(),
517                                        newResourceUrlOrNull,
518                                        existingResourceUrlOrNull,
519                                        exception);
520                }
521                return outcome;
522        }
523
524        private RequestDetails createRequestDetails() {
525                SystemRequestDetails requestDetails = new SystemRequestDetails();
526                if (myPartitionSettings.isPartitioningEnabled()) {
527                        requestDetails.setRequestPartitionId(RequestPartitionId.defaultPartition());
528                }
529                return requestDetails;
530        }
531
532        boolean validForUpload(IBaseResource theResource) {
533                String resourceType = myFhirContext.getResourceType(theResource);
534                if ("SearchParameter".equals(resourceType) && !isValidSearchParameter(theResource)) {
535                        // this is an invalid search parameter
536                        return false;
537                }
538
539                if (!isValidResourceStatusForPackageUpload(theResource)) {
540                        ourLog.warn(
541                                        "Failed to validate resource of type {} with ID {} - Error: Resource status not accepted value.",
542                                        theResource.fhirType(),
543                                        theResource.getIdElement().getValue());
544                        return false;
545                }
546
547                return true;
548        }
549
550        private boolean isValidSearchParameter(IBaseResource theResource) {
551                try {
552                        org.hl7.fhir.r5.model.SearchParameter searchParameter =
553                                        myVersionCanonicalizer.searchParameterToCanonical(theResource);
554                        mySearchParameterDaoValidator.validate(searchParameter);
555                        return true;
556                } catch (UnprocessableEntityException unprocessableEntityException) {
557                        ourLog.error(
558                                        "The SearchParameter with URL {} is invalid. Validation Error: {}",
559                                        SearchParameterUtil.getURL(myFhirContext, theResource),
560                                        unprocessableEntityException.getMessage());
561                        return false;
562                }
563        }
564
565        /**
566         * For resources like {@link org.hl7.fhir.r4.model.Subscription}, {@link org.hl7.fhir.r4.model.DocumentReference},
567         * and {@link org.hl7.fhir.r4.model.Communication}, the status field doesn't necessarily need to be set to 'active'
568         * for that resource to be eligible for upload via packages. For example, all {@link org.hl7.fhir.r4.model.Subscription}
569         * have a status of {@link org.hl7.fhir.r4.model.Subscription.SubscriptionStatus#REQUESTED} when they are originally
570         * inserted into the database, so we accept that value for {@link org.hl7.fhir.r4.model.Subscription} instead.
571         * Furthermore, {@link org.hl7.fhir.r4.model.DocumentReference} and {@link org.hl7.fhir.r4.model.Communication} can
572         * exist with a wide variety of values for status that include ones such as
573         * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#ENTEREDINERROR},
574         * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#UNKNOWN},
575         * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#ENTEREDINERROR},
576         * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#PRELIMINARY}, and others, which while not considered
577         * 'final' values, should still be uploaded for reference.
578         *
579         * @return {@link Boolean#TRUE} if the status value of this resource is acceptable for package upload.
580         */
581        private boolean isValidResourceStatusForPackageUpload(IBaseResource theResource) {
582                if (!myStorageSettings.isValidateResourceStatusForPackageUpload()) {
583                        return true;
584                }
585                List<IPrimitiveType> statusTypes =
586                                myFhirContext.newFhirPath().evaluate(theResource, "status", IPrimitiveType.class);
587                // Resource does not have a status field
588                if (statusTypes.isEmpty()) {
589                        return true;
590                }
591                // Resource has no status field, or an explicitly null one
592                if (!statusTypes.get(0).hasValue() || statusTypes.get(0).getValue() == null) {
593                        return false;
594                }
595                // Resource has a status, and we need to check based on type
596                switch (theResource.fhirType()) {
597                        case "Subscription":
598                                return (statusTypes.get(0).getValueAsString().equals("requested"));
599                        case "DocumentReference":
600                        case "Communication":
601                                return (statusTypes.get(0).isEmpty()
602                                                || !statusTypes.get(0).getValueAsString().equals("?"));
603                        default:
604                                return (statusTypes.get(0).getValueAsString().equals("active"));
605                }
606        }
607
608        private boolean isStructureDefinitionWithoutSnapshot(IBaseResource r) {
609                boolean retVal = false;
610                FhirTerser terser = myFhirContext.newTerser();
611                if (r.getClass().getSimpleName().equals("StructureDefinition")) {
612                        Optional<String> kind = terser.getSinglePrimitiveValue(r, "kind");
613                        if (kind.isPresent() && !(kind.get().equals("logical"))) {
614                                retVal = terser.getSingleValueOrNull(r, "snapshot") == null;
615                        }
616                }
617                return retVal;
618        }
619
620        private IBaseResource generateSnapshot(IBaseResource sd) {
621                try {
622                        return validationSupport.generateSnapshot(
623                                        new ValidationSupportContext(validationSupport), sd, null, null, null);
624                } catch (Exception e) {
625                        throw new ImplementationGuideInstallationException(
626                                        Msg.code(1290)
627                                                        + String.format(
628                                                                        "Failure when generating snapshot of StructureDefinition: %s", sd.getIdElement()),
629                                        e);
630                }
631        }
632
633        private SearchParameterMap createSearchParameterMapFor(IBaseResource theResource) {
634                String resourceType = theResource.getClass().getSimpleName();
635                if ("NamingSystem".equals(resourceType)) {
636                        String uniqueId = extractUniqeIdFromNamingSystem(theResource);
637                        return SearchParameterMap.newSynchronous().add("value", new StringParam(uniqueId).setExact(true));
638                } else if ("Subscription".equals(resourceType)) {
639                        String id = extractSimpleValue(theResource, "id");
640                        return SearchParameterMap.newSynchronous().add("_id", new TokenParam(id));
641                } else if ("SearchParameter".equals(resourceType)) {
642                        return buildSearchParameterMapForSearchParameter(theResource);
643                } else if (resourceHasUrlElement(theResource)) {
644                        String url = extractSimpleValue(theResource, "url");
645                        return SearchParameterMap.newSynchronous().add("url", new UriParam(url));
646                } else {
647                        TokenParam identifierToken = extractIdentifierFromOtherResourceTypes(theResource);
648                        return SearchParameterMap.newSynchronous().add("identifier", identifierToken);
649                }
650        }
651
652        /**
653         * Strategy is to build a SearchParameterMap same way the SearchParamValidatingInterceptor does, to make sure that
654         * the loader search detects existing resources and routes process to 'update' path, to avoid treating it as a new
655         * upload which validator later rejects as duplicated.
656         * To achieve this, we try canonicalizing the SearchParameter first (as the validator does) and if that is not possible
657         * we cascade to building the map from 'url' or 'identifier'.
658         */
659        private SearchParameterMap buildSearchParameterMapForSearchParameter(IBaseResource theResource) {
660                Optional<SearchParameterMap> spmFromCanonicalized =
661                                mySearchParameterHelper.buildSearchParameterMapFromCanonical(theResource);
662                if (spmFromCanonicalized.isPresent()) {
663                        return spmFromCanonicalized.get();
664                }
665
666                if (resourceHasUrlElement(theResource)) {
667                        String url = extractSimpleValue(theResource, "url");
668                        return SearchParameterMap.newSynchronous().add("url", new UriParam(url));
669                } else {
670                        TokenParam identifierToken = extractIdentifierFromOtherResourceTypes(theResource);
671                        return SearchParameterMap.newSynchronous().add("identifier", identifierToken);
672                }
673        }
674
675        private String extractUniqeIdFromNamingSystem(IBaseResource theResource) {
676                IBase uniqueIdComponent = (IBase) extractValue(theResource, "uniqueId");
677                if (uniqueIdComponent == null) {
678                        throw new ImplementationGuideInstallationException(
679                                        Msg.code(1291) + "NamingSystem does not have uniqueId component.");
680                }
681                return extractSimpleValue(uniqueIdComponent, "value");
682        }
683
684        private TokenParam extractIdentifierFromOtherResourceTypes(IBaseResource theResource) {
685                Identifier identifier = (Identifier) extractValue(theResource, "identifier");
686                if (identifier != null) {
687                        return new TokenParam(identifier.getSystem(), identifier.getValue());
688                } else {
689                        throw new UnsupportedOperationException(Msg.code(1292)
690                                        + "Resources in a package must have a url or identifier to be loaded by the package installer.");
691                }
692        }
693
694        private Object extractValue(IBase theResource, String thePath) {
695                return myFhirContext.newTerser().getSingleValueOrNull(theResource, thePath);
696        }
697
698        private String extractSimpleValue(IBase theResource, String thePath) {
699                IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) extractValue(theResource, thePath);
700                return (String) asPrimitiveType.getValue();
701        }
702
703        private boolean resourceHasUrlElement(IBaseResource resource) {
704                BaseRuntimeElementDefinition<?> def = myFhirContext.getElementDefinition(resource.getClass());
705                if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
706                        throw new IllegalArgumentException(Msg.code(1293) + "Resource is not a composite type: "
707                                        + resource.getClass().getName());
708                }
709                BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def;
710                BaseRuntimeChildDefinition nextDef = currentDef.getChildByName("url");
711                return nextDef != null;
712        }
713
714        @VisibleForTesting
715        void setFhirContextForUnitTest(FhirContext theCtx) {
716                myFhirContext = theCtx;
717        }
718}