001package ca.uhn.fhir.jpa.dao.index;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
025import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
026import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
027import ca.uhn.fhir.context.FhirContext;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.context.RuntimeSearchParam;
030import ca.uhn.fhir.interceptor.model.RequestPartitionId;
031import ca.uhn.fhir.jpa.api.config.DaoConfig;
032import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
033import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
034import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
035import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
036import ca.uhn.fhir.jpa.model.entity.ResourceTable;
037import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
038import ca.uhn.fhir.mdm.util.CanonicalIdentifier;
039import ca.uhn.fhir.rest.api.server.RequestDetails;
040import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
041import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
043import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
044import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
045import ca.uhn.fhir.util.HapiExtensions;
046import ca.uhn.fhir.util.TerserUtil;
047import org.apache.http.NameValuePair;
048import org.apache.http.client.utils.URLEncodedUtils;
049import org.hl7.fhir.instance.model.api.IBase;
050import org.hl7.fhir.instance.model.api.IBaseExtension;
051import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
052import org.hl7.fhir.instance.model.api.IBaseReference;
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.springframework.beans.factory.annotation.Autowired;
057
058import javax.annotation.Nonnull;
059import javax.annotation.Nullable;
060import javax.persistence.EntityManager;
061import javax.persistence.PersistenceContext;
062import javax.persistence.PersistenceContextType;
063import java.nio.charset.StandardCharsets;
064import java.util.Date;
065import java.util.List;
066import java.util.Optional;
067
068public class DaoResourceLinkResolver implements IResourceLinkResolver {
069        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class);
070        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
071        protected EntityManager myEntityManager;
072        @Autowired
073        private DaoConfig myDaoConfig;
074        @Autowired
075        private FhirContext myContext;
076        @Autowired
077        private IIdHelperService myIdHelperService;
078        @Autowired
079        private DaoRegistry myDaoRegistry;
080
081        @Override
082        public IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
083                ResourcePersistentId persistentId = null;
084                if (theTransactionDetails != null) {
085                        ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theSourceResourceId);
086                        if (resolvedResourceId != null && resolvedResourceId.getIdAsLong() != null && resolvedResourceId.getAssociatedResourceId() != null) {
087                                persistentId = resolvedResourceId;
088                        }
089                }
090
091                IResourceLookup resolvedResource;
092                String idPart = theSourceResourceId.getIdPart();
093                try {
094                        if (persistentId == null) {
095                                resolvedResource = myIdHelperService.resolveResourceIdentity(theRequestPartitionId, theResourceType, idPart);
096                                ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
097                        } else {
098                                resolvedResource = new ResourceLookupPersistentIdWrapper(persistentId);
099                        }
100                } catch (ResourceNotFoundException e) {
101
102                        Optional<ResourceTable> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(theType, theReference, idPart, theRequest, theTransactionDetails);
103                        if (!createdTableOpt.isPresent()) {
104
105                                if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
106                                        return null;
107                                }
108
109                                RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
110                                String resName = missingResourceDef.getName();
111                                throw new InvalidRequestException(Msg.code(1094) + "Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
112
113                        }
114                        resolvedResource = createdTableOpt.get();
115                }
116
117                ourLog.trace("Resolved resource of type {} as PID: {}", resolvedResource.getResourceType(), resolvedResource.getResourceId());
118                if (!theResourceType.equals(resolvedResource.getResourceType())) {
119                        ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theResourceType, resolvedResource.getResourceType());
120                        throw new UnprocessableEntityException(Msg.code(1095) + "Resource contains reference to unknown resource ID " + theSourceResourceId.getValue());
121                }
122
123                if (resolvedResource.getDeleted() != null) {
124                        String resName = resolvedResource.getResourceType();
125                        throw new InvalidRequestException(Msg.code(1096) + "Resource " + resName + "/" + idPart + " is deleted, specified in path: " + theSourcePath);
126                }
127
128                if (persistentId == null) {
129                        persistentId = new ResourcePersistentId(resolvedResource.getResourceId());
130                        persistentId.setAssociatedResourceId(theSourceResourceId);
131                        theTransactionDetails.addResolvedResourceId(theSourceResourceId, persistentId);
132                }
133
134                if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theResourceType)) {
135                        return null;
136                }
137
138                return resolvedResource;
139        }
140
141        /**
142         * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
143         */
144        public <T extends IBaseResource> Optional<ResourceTable> createPlaceholderTargetIfConfiguredToDoSo(Class<T> theType, IBaseReference theReference, @Nullable String theIdToAssignToPlaceholder, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
145                ResourceTable valueOf = null;
146
147                if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
148                        RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
149                        String resName = missingResourceDef.getName();
150
151                        @SuppressWarnings("unchecked")
152                        T newResource = (T) missingResourceDef.newInstance();
153
154                        tryToAddPlaceholderExtensionToResource(newResource);
155
156                        IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType);
157                        ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
158
159                        if (myDaoConfig.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()) {
160                                tryToCopyIdentifierFromReferenceToTargetResource(theReference, missingResourceDef, newResource);
161                        }
162
163                        if (theIdToAssignToPlaceholder != null) {
164                                newResource.setId(resName + "/" + theIdToAssignToPlaceholder);
165                                valueOf = ((ResourceTable) placeholderResourceDao.update(newResource, theRequest).getEntity());
166                        } else {
167                                valueOf = ((ResourceTable) placeholderResourceDao.create(newResource, theRequest).getEntity());
168                        }
169
170                        ResourcePersistentId persistentId = new ResourcePersistentId(valueOf.getResourceId(), 1L);
171                        persistentId.setAssociatedResourceId(valueOf.getIdType(myContext));
172                        theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
173                }
174
175                return Optional.ofNullable(valueOf);
176        }
177
178        private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) {
179                if (newResource instanceof IBaseHasExtensions) {
180                        IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
181                        extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
182                        extension.setValue(myContext.getPrimitiveBoolean(true));
183                }
184        }
185
186        private <T extends IBaseResource> void tryToCopyIdentifierFromReferenceToTargetResource(IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
187//              boolean referenceHasIdentifier = theSourceReference.hasIdentifier();
188                CanonicalIdentifier referenceMatchUrlIdentifier = extractIdentifierFromUrl(theSourceReference.getReferenceElement().getValue());
189                CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference);
190
191                if (referenceIdentifier == null && referenceMatchUrlIdentifier != null) {
192                        addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
193                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier == null) {
194                        addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
195                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier != null) {
196                        if (referenceIdentifier.equals(referenceMatchUrlIdentifier)) {
197                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
198                        } else {
199                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
200                                addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
201                        }
202                }
203        }
204
205        private <T extends IBaseResource> void addSubjectIdentifierToTargetResource(IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
206                BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
207                if (targetIdentifier != null) {
208                        BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
209                        String identifierElementName = identifierElement.getName();
210                        boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
211                        if (targetHasIdentifierElement) {
212
213                                BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theSourceReference.getClass());
214                                BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
215                                Optional<IBase> identifierOpt = referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
216                                identifierOpt.ifPresent(theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
217                        }
218                }
219        }
220
221        private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource(RuntimeResourceDefinition theTargetResourceDef, T theTargetResource, CanonicalIdentifier referenceMatchUrlIdentifier) {
222                BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier");
223                IBase identifierIBase = identifierDefinition.getChildByName("identifier").newInstance(identifierDefinition.getInstanceConstructorArguments());
224                IBase systemIBase = TerserUtil.newElement(myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString());
225                IBase valueIBase = TerserUtil.newElement(myContext, "string", referenceMatchUrlIdentifier.getValueElement().getValueAsString());
226                //Set system in the IBase Identifier
227
228                BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass());
229
230                BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system");
231                systemDefinition.getMutator().setValue(identifierIBase, systemIBase);
232
233                BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value");
234                valueDefinition.getMutator().setValue(identifierIBase, valueIBase);
235
236                //Set Value in the IBase identifier
237                identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase);
238        }
239
240        private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) {
241                Optional<IBase> identifier = myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class);
242                if (!identifier.isPresent()) {
243                        return null;
244                } else {
245                        CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier();
246                        Optional<IPrimitiveType> system = myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class);
247                        Optional<IPrimitiveType> value = myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class);
248
249                        system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString()));
250                        value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString()));
251                        return canonicalIdentifier;
252                }
253        }
254
255        /**
256         * Extracts the first available identifier from the URL part
257         *
258         * @param theValue Part of the URL to extract identifiers from
259         * @return Returns the first available identifier in the canonical form or null if URL contains no identifier param
260         * @throws IllegalArgumentException IllegalArgumentException is thrown in case identifier parameter can not be split using <code>system|value</code> pattern.
261         */
262        protected CanonicalIdentifier extractIdentifierFromUrl(String theValue) {
263                int identifierIndex = theValue.indexOf("identifier=");
264                if (identifierIndex == -1) {
265                        return null;
266                }
267
268                List<NameValuePair> params = URLEncodedUtils.parse(theValue.substring(identifierIndex), StandardCharsets.UTF_8, '&', ';');
269                Optional<NameValuePair> idOptional = params.stream().filter(p -> p.getName().equals("identifier")).findFirst();
270                if (!idOptional.isPresent()) {
271                        return null;
272                }
273
274                NameValuePair id = idOptional.get();
275                String identifierString = id.getValue();
276                String[] split = identifierString.split("\\|");
277                if (split.length != 2) {
278                        throw new IllegalArgumentException(Msg.code(1097) + "Can't create a placeholder reference with identifier " + theValue + ". It is not a valid identifier");
279                }
280
281                CanonicalIdentifier identifier = new CanonicalIdentifier();
282                identifier.setSystem(split[0]);
283                identifier.setValue(split[1]);
284                return identifier;
285        }
286
287        @Override
288        public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
289                myDaoRegistry.getDaoOrThrowException(theType);
290        }
291
292        private static class ResourceLookupPersistentIdWrapper implements IResourceLookup {
293                private final ResourcePersistentId myPersistentId;
294
295                public ResourceLookupPersistentIdWrapper(ResourcePersistentId thePersistentId) {
296                        myPersistentId = thePersistentId;
297                }
298
299                @Override
300                public String getResourceType() {
301                        return myPersistentId.getAssociatedResourceId().getResourceType();
302                }
303
304                @Override
305                public Long getResourceId() {
306                        return myPersistentId.getIdAsLong();
307                }
308
309                @Override
310                public Date getDeleted() {
311                        return null;
312                }
313        }
314}