001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2025 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.dao.index;
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.RuntimeResourceDefinition;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.interceptor.api.HookParams;
029import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
030import ca.uhn.fhir.interceptor.api.Pointcut;
031import ca.uhn.fhir.interceptor.model.RequestPartitionId;
032import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
033import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
034import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
035import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
036import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
037import ca.uhn.fhir.jpa.dao.BaseStorageDao;
038import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
039import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
040import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
041import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
042import ca.uhn.fhir.jpa.model.entity.StorageSettings;
043import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
044import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
045import ca.uhn.fhir.rest.api.Constants;
046import ca.uhn.fhir.rest.api.QualifiedParamList;
047import ca.uhn.fhir.rest.api.server.RequestDetails;
048import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
049import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
050import ca.uhn.fhir.rest.param.TokenParam;
051import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
052import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
053import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
054import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
055import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
056import ca.uhn.fhir.storage.interceptor.AutoCreatePlaceholderReferenceTargetRequest;
057import ca.uhn.fhir.storage.interceptor.AutoCreatePlaceholderReferenceTargetResponse;
058import ca.uhn.fhir.util.CanonicalIdentifier;
059import ca.uhn.fhir.util.HapiExtensions;
060import ca.uhn.fhir.util.TerserUtil;
061import ca.uhn.fhir.util.UrlUtil;
062import jakarta.annotation.Nonnull;
063import jakarta.annotation.Nullable;
064import org.apache.commons.lang3.Validate;
065import org.hl7.fhir.instance.model.api.IBase;
066import org.hl7.fhir.instance.model.api.IBaseExtension;
067import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
068import org.hl7.fhir.instance.model.api.IBaseReference;
069import org.hl7.fhir.instance.model.api.IBaseResource;
070import org.hl7.fhir.instance.model.api.IIdType;
071import org.hl7.fhir.instance.model.api.IPrimitiveType;
072import org.springframework.beans.factory.annotation.Autowired;
073
074import java.util.ArrayList;
075import java.util.Date;
076import java.util.List;
077import java.util.Map;
078import java.util.Optional;
079
080import static org.apache.commons.lang3.StringUtils.isBlank;
081import static org.apache.commons.lang3.StringUtils.isNotBlank;
082
083public class DaoResourceLinkResolver<T extends IResourcePersistentId<?>> implements IResourceLinkResolver {
084        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class);
085
086        @Autowired
087        private JpaStorageSettings myStorageSettings;
088
089        @Autowired
090        private FhirContext myContext;
091
092        @Autowired
093        private IIdHelperService<T> myIdHelperService;
094
095        @Autowired
096        private DaoRegistry myDaoRegistry;
097
098        @Autowired
099        private IHapiTransactionService myTransactionService;
100
101        @Autowired
102        private IInterceptorBroadcaster myInterceptorBroadcaster;
103
104        @Override
105        public IResourceLookup findTargetResource(
106                        @Nonnull RequestPartitionId theRequestPartitionId,
107                        String theSourceResourceName,
108                        PathAndRef thePathAndRef,
109                        RequestDetails theRequest,
110                        TransactionDetails theTransactionDetails) {
111
112                IBaseReference targetReference = thePathAndRef.getRef();
113                String sourcePath = thePathAndRef.getPath();
114
115                IIdType targetResourceId = targetReference.getReferenceElement();
116                if (targetResourceId.isEmpty() && targetReference.getResource() != null) {
117                        targetResourceId = targetReference.getResource().getIdElement();
118                }
119
120                String resourceType = targetResourceId.getResourceType();
121                RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(resourceType);
122                Class<? extends IBaseResource> type = resourceDef.getImplementingClass();
123
124                T persistentId = null;
125                if (theTransactionDetails != null) {
126                        T resolvedResourceId = (T) theTransactionDetails.getResolvedResourceId(targetResourceId);
127                        if (resolvedResourceId != null
128                                        && resolvedResourceId.getId() != null
129                                        && resolvedResourceId.getAssociatedResourceId() != null) {
130                                persistentId = resolvedResourceId;
131                        }
132                }
133
134                IResourceLookup<?> resolvedResource;
135                String idPart = targetResourceId.getIdPart();
136                try {
137                        if (persistentId == null) {
138
139                                // If we previously looked up the ID, and it was not found, don't bother
140                                // looking it up again
141                                if (theTransactionDetails != null
142                                                && theTransactionDetails.hasNullResolvedResourceId(targetResourceId)) {
143                                        throw new ResourceNotFoundException(Msg.code(2602));
144                                }
145
146                                resolvedResource = myIdHelperService.resolveResourceIdentity(
147                                                theRequestPartitionId,
148                                                resourceType,
149                                                idPart,
150                                                ResolveIdentityMode.excludeDeleted().noCacheUnlessDeletesDisabled());
151                                ourLog.trace("Translated {}/{} to resource PID {}", type, idPart, resolvedResource);
152                        } else {
153                                resolvedResource = new ResourceLookupPersistentIdWrapper<>(persistentId);
154                        }
155                } catch (ResourceNotFoundException e) {
156
157                        Optional<IBasePersistedResource> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(
158                                        type, targetReference, idPart, theRequest, theTransactionDetails);
159                        if (!createdTableOpt.isPresent()) {
160
161                                if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
162                                        return null;
163                                }
164
165                                RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type);
166                                String resName = missingResourceDef.getName();
167
168                                // Check if this was a deleted resource
169                                try {
170                                        resolvedResource = myIdHelperService.resolveResourceIdentity(
171                                                        theRequestPartitionId,
172                                                        resourceType,
173                                                        idPart,
174                                                        ResolveIdentityMode.includeDeleted().noCacheUnlessDeletesDisabled());
175                                        handleDeletedTarget(resourceType, idPart, sourcePath);
176                                } catch (ResourceNotFoundException e2) {
177                                        resolvedResource = null;
178                                }
179
180                                if (resolvedResource == null) {
181                                        throw new InvalidRequestException(Msg.code(1094) + "Resource " + resName + "/" + idPart
182                                                        + " not found, specified in path: " + sourcePath);
183                                }
184                        }
185
186                        resolvedResource = createdTableOpt.get();
187                }
188
189                ourLog.trace(
190                                "Resolved resource of type {} as PID: {}",
191                                resolvedResource.getResourceType(),
192                                resolvedResource.getPersistentId());
193                if (!validateResolvedResourceOrThrow(resourceType, resolvedResource, targetResourceId, idPart, sourcePath)) {
194                        return null;
195                }
196
197                if (persistentId == null) {
198                        Object id = resolvedResource.getPersistentId().getId();
199                        Integer partitionId = null;
200                        if (resolvedResource.getPartitionId() != null) {
201                                partitionId = resolvedResource.getPartitionId().getPartitionId();
202                        }
203                        persistentId = myIdHelperService.newPid(id, partitionId);
204                        persistentId.setAssociatedResourceId(targetResourceId);
205                        if (theTransactionDetails != null) {
206                                theTransactionDetails.addResolvedResourceId(targetResourceId, persistentId);
207                        }
208                }
209
210                return resolvedResource;
211        }
212
213        /**
214         * Validates the resolved resource.
215         * If 'Enforce Referential Integrity on Write' is enabled:
216         * Throws <code>UnprocessableEntityException</code> when resource types do not match
217         * Throws <code>InvalidRequestException</code> when the resolved resource was deleted
218         * <p>
219         * Otherwise, return false when resource types do not match or resource was deleted
220         * and return true if the resolved resource is valid.
221         */
222        private boolean validateResolvedResourceOrThrow(
223                        String resourceType,
224                        IResourceLookup resolvedResource,
225                        IIdType targetResourceId,
226                        String idPart,
227                        String sourcePath) {
228                if (!resourceType.equals(resolvedResource.getResourceType())) {
229                        ourLog.error(
230                                        "Resource with PID {} was of type {} and wanted {}",
231                                        resolvedResource.getPersistentId(),
232                                        resourceType,
233                                        resolvedResource.getResourceType());
234                        if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
235                                return false;
236                        }
237                        throw new UnprocessableEntityException(Msg.code(1095)
238                                        + "Resource contains reference to unknown resource ID " + targetResourceId.getValue());
239                }
240
241                if (resolvedResource.getDeleted() != null) {
242                        return handleDeletedTarget(resolvedResource.getResourceType(), idPart, sourcePath);
243                }
244                return true;
245        }
246
247        private boolean handleDeletedTarget(String resType, String idPart, String sourcePath) {
248                if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
249                        return false;
250                }
251                String resName = resType;
252                throw new InvalidRequestException(Msg.code(1096) + "Resource " + resName + "/" + idPart
253                                + " is deleted, specified in path: " + sourcePath);
254        }
255
256        @Nullable
257        @Override
258        public IBaseResource loadTargetResource(
259                        @Nonnull RequestPartitionId theRequestPartitionId,
260                        String theSourceResourceName,
261                        PathAndRef thePathAndRef,
262                        RequestDetails theRequest,
263                        TransactionDetails theTransactionDetails) {
264                return myTransactionService
265                                .withRequest(theRequest)
266                                .withTransactionDetails(theTransactionDetails)
267                                .withRequestPartitionId(theRequestPartitionId)
268                                .execute(() -> {
269                                        IIdType targetId = thePathAndRef.getRef().getReferenceElement();
270                                        IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(targetId.getResourceType());
271                                        return dao.read(targetId, theRequest);
272                                });
273        }
274
275        /**
276         * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
277         */
278        public <T extends IBaseResource> Optional<IBasePersistedResource> createPlaceholderTargetIfConfiguredToDoSo(
279                        Class<T> theType,
280                        IBaseReference theReference,
281                        @Nullable String theIdToAssignToPlaceholder,
282                        RequestDetails theRequest,
283                        TransactionDetails theTransactionDetails) {
284                IBasePersistedResource placeholderEntity = null;
285
286                if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) {
287                        RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
288                        String resName = missingResourceDef.getName();
289
290                        @SuppressWarnings("unchecked")
291                        T newResource = (T) missingResourceDef.newInstance();
292
293                        tryToAddPlaceholderExtensionToResource(newResource);
294
295                        IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType);
296                        ourLog.debug(
297                                        "Automatically creating empty placeholder resource: {}",
298                                        newResource.getIdElement().getValue());
299
300                        boolean urlIdentifiersCopied = false;
301                        if (myStorageSettings.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()) {
302                                urlIdentifiersCopied =
303                                                tryToCopyIdentifierFromReferenceToTargetResource(theReference, missingResourceDef, newResource);
304                        }
305
306                        if (isBlank(theIdToAssignToPlaceholder) && !urlIdentifiersCopied) {
307                                String msg = myContext
308                                                .getLocalizer()
309                                                .getMessage(
310                                                                BaseStorageDao.class,
311                                                                "invalidMatchUrlCantUseForAutoCreatePlaceholder",
312                                                                theReference.getReferenceElement().getValue());
313                                throw new ResourceNotFoundException(Msg.code(2746) + msg);
314                        }
315
316                        if (theIdToAssignToPlaceholder != null) {
317                                if (theTransactionDetails != null) {
318                                        String existingId = newResource.getIdElement().getValue();
319                                        theTransactionDetails.addRollbackUndoAction(() -> newResource.setId(existingId));
320                                }
321                                newResource.setId(resName + "/" + theIdToAssignToPlaceholder);
322                        }
323
324                        // Interceptor: STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE
325                        IInterceptorBroadcaster interceptorBroadcaster =
326                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
327                        if (interceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE)) {
328                                AutoCreatePlaceholderReferenceTargetRequest request =
329                                                new AutoCreatePlaceholderReferenceTargetRequest(newResource);
330                                HookParams params = new HookParams()
331                                                .add(AutoCreatePlaceholderReferenceTargetRequest.class, request)
332                                                .add(RequestDetails.class, theRequest)
333                                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
334                                AutoCreatePlaceholderReferenceTargetResponse response =
335                                                (AutoCreatePlaceholderReferenceTargetResponse) interceptorBroadcaster.callHooksAndReturnObject(
336                                                                Pointcut.STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE, params);
337                                if (response != null) {
338                                        if (response.isDoNotCreateTarget()) {
339                                                return Optional.empty();
340                                        }
341                                }
342
343                                // Sanity check: Make sure that interceptors haven't changed the ID
344                                if (theIdToAssignToPlaceholder != null) {
345                                        Validate.isTrue(
346                                                        theIdToAssignToPlaceholder.equals(
347                                                                        newResource.getIdElement().getIdPart()),
348                                                        "Interceptors must not modify the ID of auto-created placeholder reference targets");
349                                } else {
350                                        Validate.isTrue(
351                                                        isBlank(newResource.getIdElement().getIdPart()),
352                                                        "Interceptors must not modify the ID of auto-created placeholder reference targets");
353                                }
354                        }
355
356                        if (theIdToAssignToPlaceholder != null) {
357                                placeholderEntity = placeholderResourceDao
358                                                .update(newResource, null, true, false, theRequest, theTransactionDetails)
359                                                .getEntity();
360                        } else {
361                                placeholderEntity =
362                                                placeholderResourceDao.create(newResource, theRequest).getEntity();
363                        }
364
365                        verifyPlaceholderCanBeCreated(theType, theIdToAssignToPlaceholder, theReference, placeholderEntity);
366
367                        IResourcePersistentId persistentId = placeholderEntity.getPersistentId();
368                        persistentId = myIdHelperService.newPid(persistentId.getId());
369                        persistentId.setAssociatedResourceId(placeholderEntity.getIdDt());
370                        theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
371                        theTransactionDetails.addAutoCreatedPlaceholderResource(newResource.getIdElement());
372                }
373
374                return Optional.ofNullable(placeholderEntity);
375        }
376
377        /**
378         * Subclasses may override
379         */
380        protected void verifyPlaceholderCanBeCreated(
381                        Class<? extends IBaseResource> theType,
382                        String theIdToAssignToPlaceholder,
383                        IBaseReference theReference,
384                        IBasePersistedResource theStoredEntity) {}
385
386        private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) {
387                if (newResource instanceof IBaseHasExtensions) {
388                        IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
389                        extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
390                        extension.setValue(myContext.newPrimitiveBoolean(true));
391                }
392        }
393
394        /**
395         * This method returns false if the reference contained a conditional reference, but
396         * the reference couldn't be resolved into one or more identifiers (and only one or
397         * more identifiers) according to the rules in {@link #extractIdentifierFromUrl(String)}.
398         */
399        private <T extends IBaseResource> boolean tryToCopyIdentifierFromReferenceToTargetResource(
400                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
401                String urlValue = theSourceReference.getReferenceElement().getValue();
402                List<CanonicalIdentifier> referenceMatchUrlIdentifiers;
403                if (urlValue.contains("?")) {
404                        referenceMatchUrlIdentifiers = extractIdentifierFromUrl(urlValue);
405                        for (CanonicalIdentifier identifier : referenceMatchUrlIdentifiers) {
406                                addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, identifier);
407                        }
408
409                        if (referenceMatchUrlIdentifiers.isEmpty()) {
410                                return false;
411                        }
412
413                } else {
414                        referenceMatchUrlIdentifiers = List.of();
415                }
416
417                CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference);
418                if (referenceIdentifier != null && !referenceMatchUrlIdentifiers.contains(referenceIdentifier)) {
419                        addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
420                }
421
422                return true;
423        }
424
425        private <T extends IBaseResource> void addSubjectIdentifierToTargetResource(
426                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
427                BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
428                if (targetIdentifier != null) {
429                        BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
430                        String identifierElementName = identifierElement.getName();
431                        boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
432                        if (targetHasIdentifierElement) {
433
434                                BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>)
435                                                myContext.getElementDefinition(theSourceReference.getClass());
436                                BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
437                                Optional<IBase> identifierOpt =
438                                                referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
439                                identifierOpt.ifPresent(
440                                                theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
441                        }
442                }
443        }
444
445        private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource(
446                        RuntimeResourceDefinition theTargetResourceDef,
447                        T theTargetResource,
448                        CanonicalIdentifier referenceMatchUrlIdentifier) {
449                BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier");
450                IBase identifierIBase = identifierDefinition
451                                .getChildByName("identifier")
452                                .newInstance(identifierDefinition.getInstanceConstructorArguments());
453                IBase systemIBase = TerserUtil.newElement(
454                                myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString());
455                IBase valueIBase = TerserUtil.newElement(
456                                myContext,
457                                "string",
458                                referenceMatchUrlIdentifier.getValueElement().getValueAsString());
459                // Set system in the IBase Identifier
460
461                BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass());
462
463                BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system");
464                systemDefinition.getMutator().setValue(identifierIBase, systemIBase);
465
466                BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value");
467                valueDefinition.getMutator().setValue(identifierIBase, valueIBase);
468
469                // Set Value in the IBase identifier
470                identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase);
471        }
472
473        private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) {
474                Optional<IBase> identifier =
475                                myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class);
476                if (!identifier.isPresent()) {
477                        return null;
478                } else {
479                        CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier();
480                        Optional<IPrimitiveType> system =
481                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class);
482                        Optional<IPrimitiveType> value =
483                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class);
484
485                        system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString()));
486                        value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString()));
487                        return canonicalIdentifier;
488                }
489        }
490
491        /**
492         * Extracts the identifier(s) from a query URL. This method is quite strict, as it is intended only for
493         * use when creating {@link StorageSettings#isAutoCreatePlaceholderReferenceTargets() auto-created placeholder reference targets}.
494         * As such, it will:
495         * <ul>
496         *     <li>Ignore any <code>_tag:not</code> parameters</li>
497         *     <li>Add an entry to the returned list for each <code>identifier</code> parameter containing a single valid value</li>
498         *     <li>Return an empty list if any <code>identifier</code> parameters have modifiers or multiple OR values in a single parameter instance</li>
499         *     <li>Return an empty list if any other parameters are found</li>
500         * </ul>
501         *
502         * @param theValue Part of the URL to extract identifiers from
503         */
504        protected List<CanonicalIdentifier> extractIdentifierFromUrl(String theValue) {
505                Map<String, String[]> parsedQuery = UrlUtil.parseQueryString(theValue);
506
507                ArrayList<CanonicalIdentifier> retVal = new ArrayList<>(2);
508
509                for (String paramName : parsedQuery.keySet()) {
510                        switch (paramName) {
511                                case "identifier" -> {
512                                        String[] values = parsedQuery.get(paramName);
513                                        for (String value : values) {
514                                                QualifiedParamList paramList =
515                                                                QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, value);
516                                                if (paramList.size() > 1) {
517                                                        return List.of();
518                                                } else if (!paramList.isEmpty()) {
519                                                        TokenParam tokenParam = new TokenParam();
520                                                        tokenParam.setValueAsQueryToken(myContext, "identifier", null, paramList.get(0));
521                                                        if (isNotBlank(tokenParam.getSystem()) || isNotBlank(tokenParam.getValue())) {
522                                                                CanonicalIdentifier identifier = new CanonicalIdentifier();
523                                                                identifier.setSystem(tokenParam.getSystem());
524                                                                identifier.setValue(tokenParam.getValue());
525                                                                retVal.add(identifier);
526                                                        }
527                                                }
528                                        }
529                                }
530                                case Constants.PARAM_TAG + Constants.PARAMQUALIFIER_TOKEN_NOT -> {
531                                        // We ignore _tag:not expressions since any auto-created placeholder
532                                        // won't have tags so they will match this parameter
533                                }
534                                default -> {
535                                        return List.of();
536                                }
537                        }
538                }
539
540                return retVal;
541        }
542
543        @Override
544        public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
545                myDaoRegistry.getDaoOrThrow(theType);
546        }
547
548        private static class ResourceLookupPersistentIdWrapper<P extends IResourcePersistentId> implements IResourceLookup {
549                private final P myPersistentId;
550
551                public ResourceLookupPersistentIdWrapper(P thePersistentId) {
552                        myPersistentId = thePersistentId;
553                }
554
555                @Override
556                public String getResourceType() {
557                        return myPersistentId.getAssociatedResourceId().getResourceType();
558                }
559
560                @Override
561                public String getFhirId() {
562                        return myPersistentId.getAssociatedResourceId().getIdPart();
563                }
564
565                @Override
566                public Date getDeleted() {
567                        return null;
568                }
569
570                @Override
571                public P getPersistentId() {
572                        return myPersistentId;
573                }
574
575                @Override
576                public PartitionablePartitionId getPartitionId() {
577                        return new PartitionablePartitionId(myPersistentId.getPartitionId(), null);
578                }
579        }
580}