View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 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.search.JpaRuntimeSearchParam;
27  import ca.uhn.fhir.rest.api.server.IBundleProvider;
28  import ca.uhn.fhir.util.SearchParameterUtil;
29  import ca.uhn.fhir.util.StopWatch;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.time.DateUtils;
32  import org.hl7.fhir.instance.model.api.IBaseResource;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import org.springframework.beans.BeansException;
36  import org.springframework.beans.factory.annotation.Autowired;
37  import org.springframework.context.ApplicationContext;
38  import org.springframework.context.ApplicationContextAware;
39  import org.springframework.scheduling.annotation.Scheduled;
40  import org.springframework.transaction.PlatformTransactionManager;
41  import org.springframework.transaction.support.TransactionTemplate;
42  
43  import javax.annotation.PostConstruct;
44  import java.util.*;
45  
46  import static org.apache.commons.lang3.StringUtils.isBlank;
47  
48  public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implements ISearchParamRegistry, ApplicationContextAware {
49  
50  	private static final int MAX_MANAGED_PARAM_COUNT = 10000;
51  	private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class);
52  	private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams;
53  	private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
54  	private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
55  	@Autowired
56  	private FhirContext myCtx;
57  	private Collection<IFhirResourceDao<?>> myResourceDaos;
58  	private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
59  	@Autowired
60  	private DaoConfig myDaoConfig;
61  	private volatile long myLastRefresh;
62  	private ApplicationContext myApplicationContext;
63  	@Autowired
64  	private PlatformTransactionManager myTxManager;
65  	public BaseSearchParamRegistry() {
66  		super();
67  	}
68  
69  	@Override
70  	public void requestRefresh() {
71  		synchronized (this) {
72  			myLastRefresh = 0;
73  		}
74  	}
75  
76  	@Override
77  	public void forceRefresh() {
78  		requestRefresh();
79  		refreshCacheIfNecessary();
80  	}
81  
82  	@Override
83  	public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
84  		RuntimeSearchParam retVal = null;
85  		Map<String, RuntimeSearchParam> params = getActiveSearchParams().get(theResourceName);
86  		if (params != null) {
87  			retVal = params.get(theParamName);
88  		}
89  		return retVal;
90  	}
91  
92  
93  	@Override
94  	public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
95  		return myActiveSearchParams;
96  	}
97  
98  	@Override
99  	public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
100 		return myActiveSearchParams.get(theResourceName);
101 	}
102 
103 	@Override
104 	public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) {
105 		List<JpaRuntimeSearchParam> retVal = myActiveUniqueSearchParams.get(theResourceName);
106 		if (retVal == null) {
107 			retVal = Collections.emptyList();
108 		}
109 		return retVal;
110 	}
111 
112 	@Override
113 	public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName, Set<String> theParamNames) {
114 
115 		Map<Set<String>, List<JpaRuntimeSearchParam>> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName);
116 		if (paramNamesToParams == null) {
117 			return Collections.emptyList();
118 		}
119 
120 		List<JpaRuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames);
121 		if (retVal == null) {
122 			retVal = Collections.emptyList();
123 		}
124 		return Collections.unmodifiableList(retVal);
125 	}
126 
127 	public Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
128 		return myBuiltInSearchParams;
129 	}
130 
131 	private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
132 		Map<String, RuntimeSearchParam> retVal = searchParams.get(theResourceName);
133 		if (retVal == null) {
134 			retVal = new HashMap<>();
135 			searchParams.put(theResourceName, retVal);
136 		}
137 		return retVal;
138 	}
139 
140 	public abstract IFhirResourceDao<SP> getSearchParameterDao();
141 
142 	private void populateActiveSearchParams(Map<String, Map<String, RuntimeSearchParam>> theActiveSearchParams) {
143 		Map<String, List<JpaRuntimeSearchParam>> activeUniqueSearchParams = new HashMap<>();
144 		Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> activeParamNamesToUniqueSearchParams = new HashMap<>();
145 
146 		Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>();
147 		List<JpaRuntimeSearchParam> jpaSearchParams = new ArrayList<>();
148 
149 		/*
150 		 * Loop through parameters and find JPA params
151 		 */
152 		for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextResourceNameToEntries : theActiveSearchParams.entrySet()) {
153 			List<JpaRuntimeSearchParam> uniqueSearchParams = activeUniqueSearchParams.get(nextResourceNameToEntries.getKey());
154 			if (uniqueSearchParams == null) {
155 				uniqueSearchParams = new ArrayList<>();
156 				activeUniqueSearchParams.put(nextResourceNameToEntries.getKey(), uniqueSearchParams);
157 			}
158 			Collection<RuntimeSearchParam> nextSearchParamsForResourceName = nextResourceNameToEntries.getValue().values();
159 			for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) {
160 
161 				if (nextCandidate.getId() != null) {
162 					idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate);
163 				}
164 
165 				if (nextCandidate instanceof JpaRuntimeSearchParam) {
166 					JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate;
167 					jpaSearchParams.add(nextCandidateCasted);
168 					if (nextCandidateCasted.isUnique()) {
169 						uniqueSearchParams.add(nextCandidateCasted);
170 					}
171 				}
172 			}
173 
174 		}
175 
176 		Set<String> haveSeen = new HashSet<>();
177 		for (JpaRuntimeSearchParam next : jpaSearchParams) {
178 			if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) {
179 				continue;
180 			}
181 
182 			Set<String> paramNames = new HashSet<>();
183 			for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) {
184 				String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue();
185 				RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef);
186 				if (componentTarget != null) {
187 					next.getCompositeOf().add(componentTarget);
188 					paramNames.add(componentTarget.getName());
189 				} else {
190 					ourLog.warn("Search parameter {} refers to unknown component {}", next.getId().toUnqualifiedVersionless().getValue(), nextRef);
191 				}
192 			}
193 
194 			if (next.getCompositeOf() != null) {
195 				Collections.sort(next.getCompositeOf(), new Comparator<RuntimeSearchParam>() {
196 					@Override
197 					public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) {
198 						return StringUtils.compare(theO1.getName(), theO2.getName());
199 					}
200 				});
201 				for (String nextBase : next.getBase()) {
202 					if (!activeParamNamesToUniqueSearchParams.containsKey(nextBase)) {
203 						activeParamNamesToUniqueSearchParams.put(nextBase, new HashMap<>());
204 					}
205 					if (!activeParamNamesToUniqueSearchParams.get(nextBase).containsKey(paramNames)) {
206 						activeParamNamesToUniqueSearchParams.get(nextBase).put(paramNames, new ArrayList<JpaRuntimeSearchParam>());
207 					}
208 					activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next);
209 				}
210 			}
211 		}
212 
213 		myActiveUniqueSearchParams = activeUniqueSearchParams;
214 		myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams;
215 	}
216 
217 	@PostConstruct
218 	public void postConstruct() {
219 		Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
220 
221 		myResourceDaos = new ArrayList<>();
222 		Map<String, IFhirResourceDao> daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false);
223 		for (IFhirResourceDao next : daos.values()) {
224 			myResourceDaos.add(next);
225 		}
226 
227 		for (IFhirResourceDao<?> nextDao : myResourceDaos) {
228 			RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(nextDao.getResourceType());
229 			String nextResourceName = nextResDef.getName();
230 			HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
231 			resourceNameToSearchParams.put(nextResourceName, nameToParam);
232 
233 			for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
234 				nameToParam.put(nextSp.getName(), nextSp);
235 			}
236 		}
237 
238 		myBuiltInSearchParams = Collections.unmodifiableMap(resourceNameToSearchParams);
239 
240 		refreshCacheIfNecessary();
241 	}
242 
243 	@Override
244 	public void refreshCacheIfNecessary() {
245 		long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE;
246 		if (System.currentTimeMillis() - refreshInterval > myLastRefresh) {
247 			synchronized (this) {
248 				TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
249 				txTemplate.execute(t->{
250 					doRefresh(refreshInterval);
251 					return null;
252 				});
253 			}
254 		}
255 	}
256 
257 	private void doRefresh(long theRefreshInterval) {
258 		if (System.currentTimeMillis() - theRefreshInterval > myLastRefresh) {
259 			StopWatch sw = new StopWatch();
260 
261 			Map<String, Map<String, RuntimeSearchParam>> searchParams = new HashMap<>();
262 			for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : getBuiltInSearchParams().entrySet()) {
263 				for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
264 					String nextResourceName = nextBuiltInEntry.getKey();
265 					getSearchParamMap(searchParams, nextResourceName).put(nextParam.getName(), nextParam);
266 				}
267 			}
268 
269 			SearchParameterMap params = new SearchParameterMap();
270 			params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
271 
272 			IBundleProvider allSearchParamsBp = getSearchParameterDao().search(params);
273 			int size = allSearchParamsBp.size();
274 
275 			// Just in case..
276 			if (size >= MAX_MANAGED_PARAM_COUNT) {
277 				ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
278 				size = MAX_MANAGED_PARAM_COUNT;
279 			}
280 
281 			List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
282 			for (IBaseResource nextResource : allSearchParams) {
283 				SP nextSp = (SP) nextResource;
284 				if (nextSp == null) {
285 					continue;
286 				}
287 
288 				RuntimeSearchParam runtimeSp = toRuntimeSp(nextSp);
289 				if (runtimeSp == null) {
290 					continue;
291 				}
292 
293 				for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myCtx, nextSp)) {
294 					if (isBlank(nextBaseName)) {
295 						continue;
296 					}
297 
298 					Map<String, RuntimeSearchParam> searchParamMap = getSearchParamMap(searchParams, nextBaseName);
299 					String name = runtimeSp.getName();
300 					if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) {
301 						searchParamMap.put(name, runtimeSp);
302 					}
303 
304 				}
305 			}
306 
307 			Map<String, Map<String, RuntimeSearchParam>> activeSearchParams = new HashMap<>();
308 			for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextEntry : searchParams.entrySet()) {
309 				for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) {
310 					String nextName = nextSp.getName();
311 					if (nextSp.getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE) {
312 						nextSp = null;
313 					}
314 
315 					if (!activeSearchParams.containsKey(nextEntry.getKey())) {
316 						activeSearchParams.put(nextEntry.getKey(), new HashMap<>());
317 					}
318 					if (activeSearchParams.containsKey(nextEntry.getKey())) {
319 						ourLog.debug("Replacing existing/built in search param {}:{} with new one", nextEntry.getKey(), nextName);
320 					}
321 
322 					if (nextSp != null) {
323 						activeSearchParams.get(nextEntry.getKey()).put(nextName, nextSp);
324 					} else {
325 						activeSearchParams.get(nextEntry.getKey()).remove(nextName);
326 					}
327 				}
328 			}
329 
330 			myActiveSearchParams = activeSearchParams;
331 
332 			populateActiveSearchParams(activeSearchParams);
333 
334 			myLastRefresh = System.currentTimeMillis();
335 			ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis());
336 		}
337 	}
338 
339 	@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
340 	public void refreshCacheOnSchedule() {
341 		refreshCacheIfNecessary();
342 	}
343 
344 	@Override
345 	public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
346 		myApplicationContext = theApplicationContext;
347 	}
348 
349 	protected abstract RuntimeSearchParam toRuntimeSp(SP theNextSp);
350 
351 
352 }