View Javadoc
1   package ca.uhn.fhir.jpa.dao.index;
2   
3   /*-
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2019 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.FhirContext;
24  import ca.uhn.fhir.context.RuntimeResourceDefinition;
25  import ca.uhn.fhir.context.RuntimeSearchParam;
26  import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
27  import ca.uhn.fhir.jpa.dao.DaoConfig;
28  import ca.uhn.fhir.jpa.dao.IDao;
29  import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
30  import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
31  import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
32  import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
33  import ca.uhn.fhir.jpa.model.entity.ResourceLink;
34  import ca.uhn.fhir.jpa.model.entity.ResourceTable;
35  import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
36  import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
37  import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor;
38  import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
39  import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
40  import ca.uhn.fhir.model.api.IQueryParameterType;
41  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
42  import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
43  import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
44  import ca.uhn.fhir.util.FhirTerser;
45  import ca.uhn.fhir.util.UrlUtil;
46  import org.hl7.fhir.instance.model.api.IBaseReference;
47  import org.hl7.fhir.instance.model.api.IBaseResource;
48  import org.hl7.fhir.instance.model.api.IIdType;
49  import org.springframework.beans.factory.annotation.Autowired;
50  import org.springframework.context.annotation.Lazy;
51  import org.springframework.stereotype.Service;
52  
53  import javax.persistence.EntityManager;
54  import javax.persistence.PersistenceContext;
55  import javax.persistence.PersistenceContextType;
56  import java.util.*;
57  
58  import static org.apache.commons.lang3.StringUtils.isNotBlank;
59  
60  @Service
61  @Lazy
62  public class SearchParamWithInlineReferencesExtractor {
63  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class);
64  
65  	@Autowired
66  	private MatchResourceUrlService myMatchResourceUrlService;
67  	@Autowired
68  	private DaoConfig myDaoConfig;
69  	@Autowired
70  	private FhirContext myContext;
71  	@Autowired
72  	private IdHelperService myIdHelperService;
73  	@Autowired
74  	private ISearchParamRegistry mySearchParamRegistry;
75  	@Autowired
76  	SearchParamExtractorService mySearchParamExtractorService;
77  	@Autowired
78  	ResourceLinkExtractor myResourceLinkExtractor;
79  	@Autowired
80  	DaoResourceLinkResolver myDaoResourceLinkResolver;
81  	@Autowired
82  	DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
83  	@Autowired
84  	private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
85  
86  
87  	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
88  	protected EntityManager myEntityManager;
89  
90  	public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams) {
91  		mySearchParamExtractorService.extractFromResource(theParams, theEntity, theResource);
92  
93  		Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
94  		if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
95  			theParams.findMissingSearchParams(myDaoConfig.getModelConfig(), theEntity, activeSearchParams);
96  		}
97  
98  		theParams.setUpdatedTime(theUpdateTime);
99  
100 		extractInlineReferences(theResource);
101 
102 		myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver, true);
103 
104 		/*
105 		 * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
106 		 */
107 		for (Iterator<ResourceLink> existingLinkIter = theExistingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
108 			ResourceLink nextExisting = existingLinkIter.next();
109 			if (theParams.myLinks.remove(nextExisting)) {
110 				existingLinkIter.remove();
111 				theParams.myLinks.add(nextExisting);
112 			}
113 		}
114 
115 		/*
116 		 * Handle composites
117 		 */
118 		extractCompositeStringUniques(theEntity, theParams);
119 	}
120 
121 	private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
122 
123 		final String resourceType = theEntity.getResourceType();
124 		List<JpaRuntimeSearchParam> uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(resourceType);
125 
126 		for (JpaRuntimeSearchParam next : uniqueSearchParams) {
127 
128 			List<List<String>> partsChoices = new ArrayList<>();
129 
130 			for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
131 				Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
132 				Collection<ResourceLink> linksForCompositePart = null;
133 				Collection<String> linksForCompositePartWantPaths = null;
134 				switch (nextCompositeOf.getParamType()) {
135 					case NUMBER:
136 						paramsListForCompositePart = theParams.myNumberParams;
137 						break;
138 					case DATE:
139 						paramsListForCompositePart = theParams.myDateParams;
140 						break;
141 					case STRING:
142 						paramsListForCompositePart = theParams.myStringParams;
143 						break;
144 					case TOKEN:
145 						paramsListForCompositePart = theParams.myTokenParams;
146 						break;
147 					case REFERENCE:
148 						linksForCompositePart = theParams.myLinks;
149 						linksForCompositePartWantPaths = new HashSet<>(nextCompositeOf.getPathsSplit());
150 						break;
151 					case QUANTITY:
152 						paramsListForCompositePart = theParams.myQuantityParams;
153 						break;
154 					case URI:
155 						paramsListForCompositePart = theParams.myUriParams;
156 						break;
157 					case SPECIAL:
158 					case COMPOSITE:
159 					case HAS:
160 						break;
161 				}
162 
163 				ArrayList<String> nextChoicesList = new ArrayList<>();
164 				partsChoices.add(nextChoicesList);
165 
166 				String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
167 				if (paramsListForCompositePart != null) {
168 					for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
169 						if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
170 							IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
171 							String value = nextParamAsClientParam.getValueAsQueryToken(myContext);
172 							if (isNotBlank(value)) {
173 								value = UrlUtil.escapeUrlParam(value);
174 								nextChoicesList.add(key + "=" + value);
175 							}
176 						}
177 					}
178 				}
179 				if (linksForCompositePart != null) {
180 					for (ResourceLink nextLink : linksForCompositePart) {
181 						if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
182 							String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
183 							if (isNotBlank(value)) {
184 								value = UrlUtil.escapeUrlParam(value);
185 								nextChoicesList.add(key + "=" + value);
186 							}
187 						}
188 					}
189 				}
190 			}
191 
192 			Set<String> queryStringsToPopulate = ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains(resourceType, partsChoices);
193 
194 			for (String nextQueryString : queryStringsToPopulate) {
195 				if (isNotBlank(nextQueryString)) {
196 					ourLog.trace("Adding composite unique SP: {}", nextQueryString);
197 					theParams.myCompositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
198 				}
199 			}
200 		}
201 	}
202 
203 
204 
205 	/**
206 	 * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
207 	 * matching resource.
208 	 */
209 	public void extractInlineReferences(IBaseResource theResource) {
210 		if (!myDaoConfig.isAllowInlineMatchUrlReferences()) {
211 			return;
212 		}
213 		FhirTerser terser = myContext.newTerser();
214 		List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
215 		for (IBaseReference nextRef : allRefs) {
216 			IIdType nextId = nextRef.getReferenceElement();
217 			String nextIdText = nextId.getValue();
218 			if (nextIdText == null) {
219 				continue;
220 			}
221 			int qmIndex = nextIdText.indexOf('?');
222 			if (qmIndex != -1) {
223 				for (int i = qmIndex - 1; i >= 0; i--) {
224 					if (nextIdText.charAt(i) == '/') {
225 						if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
226 							// Just in case the URL is in the form Patient/?foo=bar
227 							continue;
228 						}
229 						nextIdText = nextIdText.substring(i + 1);
230 						break;
231 					}
232 				}
233 				String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
234 				RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString);
235 				if (matchResourceDef == null) {
236 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
237 					throw new InvalidRequestException(msg);
238 				}
239 				Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
240 				Set<Long> matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType);
241 				if (matches.isEmpty()) {
242 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
243 					throw new ResourceNotFoundException(msg);
244 				}
245 				if (matches.size() > 1) {
246 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
247 					throw new PreconditionFailedException(msg);
248 				}
249 				Long next = matches.iterator().next();
250 				String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, next);
251 				ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
252 				nextRef.setReference(newId);
253 			}
254 		}
255 	}
256 
257 	public void storeCompositeStringUniques(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
258 
259 		// Store composite string uniques
260 		if (myDaoConfig.isUniqueIndexesEnabled()) {
261 			for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(existingParams.myCompositeStringUniques, theParams.myCompositeStringUniques)) {
262 				ourLog.debug("Removing unique index: {}", next);
263 				myEntityManager.remove(next);
264 				theEntity.getParamsCompositeStringUnique().remove(next);
265 			}
266 			boolean haveNewParams = false;
267 			for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(theParams.myCompositeStringUniques, existingParams.myCompositeStringUniques)) {
268 				if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) {
269 					ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
270 					if (existing != null) {
271 						String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
272 						throw new PreconditionFailedException(msg);
273 					}
274 				}
275 				ourLog.debug("Persisting unique index: {}", next);
276 				myEntityManager.persist(next);
277 				haveNewParams = true;
278 			}
279 			if (theParams.myCompositeStringUniques.size() > 0 || haveNewParams) {
280 				theEntity.setParamsCompositeStringUniquePresent(true);
281 			} else {
282 				theEntity.setParamsCompositeStringUniquePresent(false);
283 			}
284 		}
285 	}
286 }