001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao.index;
021
022import ca.uhn.fhir.context.RuntimeSearchParam;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
026import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao;
027import ca.uhn.fhir.jpa.model.config.PartitionSettings;
028import ca.uhn.fhir.jpa.model.dao.JpaPid;
029import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
030import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
031import ca.uhn.fhir.jpa.model.entity.ResourceTable;
032import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamWithInlineReferencesExtractor;
033import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
034import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamWithInlineReferencesExtractor;
035import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
036import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
037import ca.uhn.fhir.rest.api.server.RequestDetails;
038import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
039import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
040import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
041import com.google.common.annotations.VisibleForTesting;
042import jakarta.annotation.Nullable;
043import jakarta.persistence.EntityManager;
044import jakarta.persistence.PersistenceContext;
045import jakarta.persistence.PersistenceContextType;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047import org.springframework.beans.factory.annotation.Autowired;
048import org.springframework.context.annotation.Lazy;
049import org.springframework.stereotype.Service;
050
051import java.util.Collection;
052import java.util.stream.Collectors;
053
054@Service
055@Lazy
056public class SearchParamWithInlineReferencesExtractor extends BaseSearchParamWithInlineReferencesExtractor<JpaPid>
057                implements ISearchParamWithInlineReferencesExtractor {
058        private static final org.slf4j.Logger ourLog =
059                        org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class);
060
061        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
062        protected EntityManager myEntityManager;
063
064        @Autowired
065        private ISearchParamRegistry mySearchParamRegistry;
066
067        @Autowired
068        private SearchParamExtractorService mySearchParamExtractorService;
069
070        @Autowired
071        private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
072
073        @Autowired
074        private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
075
076        @Autowired
077        private PartitionSettings myPartitionSettings;
078
079        @VisibleForTesting
080        public void setPartitionSettings(PartitionSettings thePartitionSettings) {
081                myPartitionSettings = thePartitionSettings;
082        }
083
084        @VisibleForTesting
085        public void setSearchParamExtractorService(SearchParamExtractorService theSearchParamExtractorService) {
086                mySearchParamExtractorService = theSearchParamExtractorService;
087        }
088
089        @VisibleForTesting
090        public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
091                mySearchParamRegistry = theSearchParamRegistry;
092        }
093
094        public void populateFromResource(
095                        RequestPartitionId theRequestPartitionId,
096                        ResourceIndexedSearchParams theParams,
097                        TransactionDetails theTransactionDetails,
098                        ResourceTable theEntity,
099                        IBaseResource theResource,
100                        ResourceIndexedSearchParams theExistingParams,
101                        RequestDetails theRequest,
102                        boolean thePerformIndexing) {
103                if (thePerformIndexing) {
104                        // Perform inline match URL substitution
105                        extractInlineReferences(theRequest, theResource, theTransactionDetails);
106                }
107
108                mySearchParamExtractorService.extractFromResource(
109                                theRequestPartitionId,
110                                theRequest,
111                                theParams,
112                                theExistingParams,
113                                theEntity,
114                                theResource,
115                                theTransactionDetails,
116                                thePerformIndexing,
117                                ISearchParamExtractor.ALL_PARAMS);
118        }
119
120        @Nullable
121        private Collection<? extends BaseResourceIndexedSearchParam> findParameterIndexes(
122                        ResourceIndexedSearchParams theParams, RuntimeSearchParam nextCompositeOf) {
123                Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
124                switch (nextCompositeOf.getParamType()) {
125                        case NUMBER:
126                                paramsListForCompositePart = theParams.myNumberParams;
127                                break;
128                        case DATE:
129                                paramsListForCompositePart = theParams.myDateParams;
130                                break;
131                        case STRING:
132                                paramsListForCompositePart = theParams.myStringParams;
133                                break;
134                        case TOKEN:
135                                paramsListForCompositePart = theParams.myTokenParams;
136                                break;
137                        case QUANTITY:
138                                paramsListForCompositePart = theParams.myQuantityParams;
139                                break;
140                        case URI:
141                                paramsListForCompositePart = theParams.myUriParams;
142                                break;
143                        case REFERENCE:
144                        case SPECIAL:
145                        case COMPOSITE:
146                        case HAS:
147                                break;
148                }
149                if (paramsListForCompositePart != null) {
150                        paramsListForCompositePart = paramsListForCompositePart.stream()
151                                        .filter(t -> t.getParamName().equals(nextCompositeOf.getName()))
152                                        .collect(Collectors.toList());
153                }
154                return paramsListForCompositePart;
155        }
156
157        @VisibleForTesting
158        public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) {
159                myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer;
160        }
161
162        public void storeUniqueComboParameters(
163                        ResourceIndexedSearchParams theParams,
164                        ResourceTable theEntity,
165                        ResourceIndexedSearchParams theExistingParams) {
166
167                /*
168                 * String Uniques
169                 */
170                if (myStorageSettings.isUniqueIndexesEnabled()) {
171                        for (ResourceIndexedComboStringUnique next : DaoSearchParamSynchronizer.subtract(
172                                        theExistingParams.myComboStringUniques, theParams.myComboStringUniques)) {
173                                ourLog.debug("Removing unique index: {}", next);
174                                myEntityManager.remove(next);
175                                theEntity.getParamsComboStringUnique().remove(next);
176                        }
177                        boolean haveNewStringUniqueParams = false;
178                        for (ResourceIndexedComboStringUnique next : DaoSearchParamSynchronizer.subtract(
179                                        theParams.myComboStringUniques, theExistingParams.myComboStringUniques)) {
180                                if (myStorageSettings.isUniqueIndexesCheckedBeforeSave()) {
181                                        ResourceIndexedComboStringUnique existing =
182                                                        myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
183                                        if (existing != null) {
184
185                                                String searchParameterId = "(unknown)";
186                                                if (next.getSearchParameterId() != null) {
187                                                        searchParameterId = next.getSearchParameterId()
188                                                                        .toUnqualifiedVersionless()
189                                                                        .getValue();
190                                                }
191
192                                                String msg = myFhirContext
193                                                                .getLocalizer()
194                                                                .getMessage(
195                                                                                BaseHapiFhirDao.class,
196                                                                                "uniqueIndexConflictFailure",
197                                                                                theEntity.getResourceType(),
198                                                                                next.getIndexString(),
199                                                                                existing.getResource()
200                                                                                                .getIdDt()
201                                                                                                .toUnqualifiedVersionless()
202                                                                                                .getValue(),
203                                                                                searchParameterId);
204
205                                                // Use ResourceVersionConflictException here because the HapiTransactionService
206                                                // catches this and can retry it if needed
207                                                throw new ResourceVersionConflictException(Msg.code(1093) + msg);
208                                        }
209                                }
210                                ourLog.debug("Persisting unique index: {}", next);
211                                myEntityManager.persist(next);
212                                haveNewStringUniqueParams = true;
213                        }
214                        theEntity.setParamsComboStringUniquePresent(
215                                        theParams.myComboStringUniques.size() > 0 || haveNewStringUniqueParams);
216                }
217        }
218}