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