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