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