View Javadoc
1   package ca.uhn.fhir.jpa.term;
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.jpa.dao.BaseHapiFhirDao;
25  import ca.uhn.fhir.jpa.dao.DaoConfig;
26  import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
27  import ca.uhn.fhir.jpa.dao.data.*;
28  import ca.uhn.fhir.jpa.entity.*;
29  import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
30  import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
31  import ca.uhn.fhir.rest.api.server.RequestDetails;
32  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
33  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
34  import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
35  import ca.uhn.fhir.util.ObjectUtil;
36  import ca.uhn.fhir.util.StopWatch;
37  import ca.uhn.fhir.util.ValidateUtil;
38  import com.github.benmanes.caffeine.cache.Cache;
39  import com.github.benmanes.caffeine.cache.Caffeine;
40  import com.google.common.annotations.VisibleForTesting;
41  import com.google.common.base.Stopwatch;
42  import com.google.common.collect.ArrayListMultimap;
43  import org.apache.commons.lang3.StringUtils;
44  import org.apache.commons.lang3.Validate;
45  import org.apache.commons.lang3.time.DateUtils;
46  import org.apache.lucene.index.Term;
47  import org.apache.lucene.queries.TermsQuery;
48  import org.apache.lucene.search.*;
49  import org.hibernate.ScrollMode;
50  import org.hibernate.ScrollableResults;
51  import org.hibernate.search.jpa.FullTextEntityManager;
52  import org.hibernate.search.jpa.FullTextQuery;
53  import org.hibernate.search.query.dsl.BooleanJunction;
54  import org.hibernate.search.query.dsl.QueryBuilder;
55  import org.hl7.fhir.exceptions.FHIRException;
56  import org.hl7.fhir.instance.model.api.IIdType;
57  import org.hl7.fhir.r4.model.*;
58  import org.springframework.beans.BeansException;
59  import org.springframework.beans.factory.annotation.Autowired;
60  import org.springframework.context.ApplicationContext;
61  import org.springframework.context.ApplicationContextAware;
62  import org.springframework.data.domain.Page;
63  import org.springframework.data.domain.PageRequest;
64  import org.springframework.data.domain.Slice;
65  import org.springframework.data.jpa.repository.JpaRepository;
66  import org.springframework.scheduling.annotation.Scheduled;
67  import org.springframework.transaction.PlatformTransactionManager;
68  import org.springframework.transaction.TransactionStatus;
69  import org.springframework.transaction.annotation.Propagation;
70  import org.springframework.transaction.annotation.Transactional;
71  import org.springframework.transaction.support.TransactionCallbackWithoutResult;
72  import org.springframework.transaction.support.TransactionTemplate;
73  
74  import javax.annotation.PostConstruct;
75  import javax.persistence.EntityManager;
76  import javax.persistence.PersistenceContext;
77  import javax.persistence.PersistenceContextType;
78  import javax.persistence.TypedQuery;
79  import javax.persistence.criteria.*;
80  import java.util.*;
81  import java.util.concurrent.TimeUnit;
82  import java.util.concurrent.atomic.AtomicInteger;
83  import java.util.function.Supplier;
84  import java.util.stream.Collectors;
85  
86  import static org.apache.commons.lang3.StringUtils.isBlank;
87  import static org.apache.commons.lang3.StringUtils.isNotBlank;
88  
89  public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, ApplicationContextAware {
90  	public static final int DEFAULT_FETCH_SIZE = 250;
91  
92  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiTerminologySvcImpl.class);
93  	private static final Object PLACEHOLDER_OBJECT = new Object();
94  	private static boolean ourForceSaveDeferredAlwaysForUnitTest;
95  	private static boolean ourLastResultsFromTranslationCache; // For testing.
96  	private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing.
97  	@Autowired
98  	protected ITermCodeSystemDao myCodeSystemDao;
99  	@Autowired
100 	protected ITermConceptDao myConceptDao;
101 	@Autowired
102 	protected ITermConceptMapDao myConceptMapDao;
103 	@Autowired
104 	protected ITermConceptMapGroupDao myConceptMapGroupDao;
105 	@Autowired
106 	protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao;
107 	@Autowired
108 	protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
109 	@Autowired
110 	protected ITermConceptPropertyDao myConceptPropertyDao;
111 	@Autowired
112 	protected ITermConceptDesignationDao myConceptDesignationDao;
113 	@Autowired
114 	protected FhirContext myContext;
115 	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
116 	protected EntityManager myEntityManager;
117 	private ArrayListMultimap<Long, Long> myChildToParentPidCache;
118 	@Autowired
119 	private ITermCodeSystemVersionDao myCodeSystemVersionDao;
120 	private List<TermConceptParentChildLink> myConceptLinksToSaveLater = new ArrayList<>();
121 	@Autowired
122 	private ITermConceptParentChildLinkDao myConceptParentChildLinkDao;
123 	private List<TermConcept> myDeferredConcepts = Collections.synchronizedList(new ArrayList<>());
124 	private List<ValueSet> myDeferredValueSets = Collections.synchronizedList(new ArrayList<>());
125 	private List<ConceptMap> myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>());
126 	@Autowired
127 	private DaoConfig myDaoConfig;
128 	private long myNextReindexPass;
129 	private boolean myProcessDeferred = true;
130 	@Autowired
131 	private PlatformTransactionManager myTransactionMgr;
132 	private IFhirResourceDaoCodeSystem<?, ?, ?> myCodeSystemResourceDao;
133 	private Cache<TranslationQuery, List<TermConceptMapGroupElementTarget>> myTranslationCache;
134 	private Cache<TranslationQuery, List<TermConceptMapGroupElement>> myTranslationWithReverseCache;
135 	private int myFetchSize = DEFAULT_FETCH_SIZE;
136 	private ApplicationContext myApplicationContext;
137 
138 	/**
139 	 * @param theAdd         If true, add the code. If false, remove the code.
140 	 * @param theCodeCounter
141 	 */
142 	private void addCodeIfNotAlreadyAdded(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) {
143 		String code = theConcept.getCode();
144 		if (theAdd && theAddedCodes.add(code)) {
145 			String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
146 			ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains();
147 			contains.setCode(code);
148 			contains.setSystem(codeSystem);
149 			contains.setDisplay(theConcept.getDisplay());
150 			for (TermConceptDesignation nextDesignation : theConcept.getDesignations()) {
151 				contains
152 					.addDesignation()
153 					.setValue(nextDesignation.getValue())
154 					.getUse()
155 					.setSystem(nextDesignation.getUseSystem())
156 					.setCode(nextDesignation.getUseCode())
157 					.setDisplay(nextDesignation.getUseDisplay());
158 			}
159 
160 			theCodeCounter.incrementAndGet();
161 		}
162 
163 		if (!theAdd && theAddedCodes.remove(code)) {
164 			String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
165 			removeCodeFromExpansion(codeSystem, code, theExpansionComponent);
166 			theCodeCounter.decrementAndGet();
167 		}
168 	}
169 
170 	private void addConceptsToList(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set<String> theAddedCodes, String theSystem, List<CodeSystem.ConceptDefinitionComponent> theConcept, boolean theAdd) {
171 		for (CodeSystem.ConceptDefinitionComponent next : theConcept) {
172 			if (theAdd && theAddedCodes.add(next.getCode())) {
173 				ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains();
174 				contains.setCode(next.getCode());
175 				contains.setSystem(theSystem);
176 				contains.setDisplay(next.getDisplay());
177 			}
178 			if (!theAdd && theAddedCodes.remove(next.getCode())) {
179 				removeCodeFromExpansion(theSystem, next.getCode(), theExpansionComponent);
180 			}
181 			addConceptsToList(theExpansionComponent, theAddedCodes, theSystem, next.getConcept(), theAdd);
182 		}
183 	}
184 
185 	private void addDisplayFilterExact(QueryBuilder qb, BooleanJunction<?> bool, ValueSet.ConceptSetFilterComponent nextFilter) {
186 		bool.must(qb.phrase().onField("myDisplay").sentence(nextFilter.getValue()).createQuery());
187 	}
188 
189 	private void addDisplayFilterInexact(QueryBuilder qb, BooleanJunction<?> bool, ValueSet.ConceptSetFilterComponent nextFilter) {
190 		Query textQuery = qb
191 			.phrase()
192 			.withSlop(2)
193 			.onField("myDisplay").boostedTo(4.0f)
194 			.andField("myDisplayEdgeNGram").boostedTo(2.0f)
195 			// .andField("myDisplayNGram").boostedTo(1.0f)
196 			// .andField("myDisplayPhonetic").boostedTo(0.5f)
197 			.sentence(nextFilter.getValue().toLowerCase()).createQuery();
198 		bool.must(textQuery);
199 	}
200 
201 	private boolean addToSet(Set<TermConcept> theSetToPopulate, TermConcept theConcept) {
202 		boolean retVal = theSetToPopulate.add(theConcept);
203 		if (retVal) {
204 			if (theSetToPopulate.size() >= myDaoConfig.getMaximumExpansionSize()) {
205 				String msg = myContext.getLocalizer().getMessage(BaseHapiTerminologySvcImpl.class, "expansionTooLarge", myDaoConfig.getMaximumExpansionSize());
206 				throw new InvalidRequestException(msg);
207 			}
208 		}
209 		return retVal;
210 	}
211 
212 	@PostConstruct
213 	public void buildTranslationCaches() {
214 		Long timeout = myDaoConfig.getTranslationCachesExpireAfterWriteInMinutes();
215 
216 		myTranslationCache =
217 			Caffeine.newBuilder()
218 				.maximumSize(10000)
219 				.expireAfterWrite(timeout, TimeUnit.MINUTES)
220 				.build();
221 
222 		myTranslationWithReverseCache =
223 			Caffeine.newBuilder()
224 				.maximumSize(10000)
225 				.expireAfterWrite(timeout, TimeUnit.MINUTES)
226 				.build();
227 	}
228 
229 	/**
230 	 * This method is present only for unit tests, do not call from client code
231 	 */
232 	@VisibleForTesting
233 	public void clearDeferred() {
234 		myDeferredValueSets.clear();
235 		myDeferredConceptMaps.clear();
236 		myDeferredConcepts.clear();
237 	}
238 
239 	/**
240 	 * This method is present only for unit tests, do not call from client code
241 	 */
242 	@VisibleForTesting
243 	public void clearTranslationCache() {
244 		myTranslationCache.invalidateAll();
245 	}
246 
247 	/**
248 	 * This method is present only for unit tests, do not call from client code
249 	 */
250 	@VisibleForTesting()
251 	public void clearTranslationWithReverseCache() {
252 		myTranslationWithReverseCache.invalidateAll();
253 	}
254 
255 	protected abstract IIdType createOrUpdateCodeSystem(CodeSystem theCodeSystemResource);
256 
257 	protected abstract void createOrUpdateConceptMap(ConceptMap theNextConceptMap);
258 
259 	abstract void createOrUpdateValueSet(ValueSet theValueSet);
260 
261 	@Override
262 	public void deleteCodeSystem(TermCodeSystem theCodeSystem) {
263 		ourLog.info(" * Deleting code system {}", theCodeSystem.getPid());
264 
265 		myEntityManager.flush();
266 		TermCodeSystem cs = myCodeSystemDao.findById(theCodeSystem.getPid()).orElseThrow(IllegalStateException::new);
267 		cs.setCurrentVersion(null);
268 		myCodeSystemDao.save(cs);
269 		myCodeSystemDao.flush();
270 
271 		int i = 0;
272 		for (TermCodeSystemVersion next : myCodeSystemVersionDao.findByCodeSystemResource(theCodeSystem.getPid())) {
273 			deleteCodeSystemVersion(next.getPid());
274 		}
275 		myCodeSystemVersionDao.deleteForCodeSystem(theCodeSystem);
276 		myCodeSystemDao.delete(theCodeSystem);
277 
278 		myEntityManager.flush();
279 	}
280 
281 	public void deleteCodeSystemVersion(final Long theCodeSystemVersionPid) {
282 		ourLog.info(" * Deleting code system version {}", theCodeSystemVersionPid);
283 
284 		PageRequest page1000 = PageRequest.of(0, 1000);
285 
286 		// Parent/Child links
287 		{
288 			String descriptor = "parent/child links";
289 			Supplier<Slice<TermConceptParentChildLink>> loader = () -> myConceptParentChildLinkDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid);
290 			Supplier<Integer> counter = () -> myConceptParentChildLinkDao.countByCodeSystemVersion(theCodeSystemVersionPid);
291 			doDelete(descriptor, loader, counter, myConceptParentChildLinkDao);
292 		}
293 
294 		// Properties
295 		{
296 			String descriptor = "concept properties";
297 			Supplier<Slice<TermConceptProperty>> loader = () -> myConceptPropertyDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid);
298 			Supplier<Integer> counter = () -> myConceptPropertyDao.countByCodeSystemVersion(theCodeSystemVersionPid);
299 			doDelete(descriptor, loader, counter, myConceptPropertyDao);
300 		}
301 
302 		// Designations
303 		{
304 			String descriptor = "concept designations";
305 			Supplier<Slice<TermConceptDesignation>> loader = () -> myConceptDesignationDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid);
306 			Supplier<Integer> counter = () -> myConceptDesignationDao.countByCodeSystemVersion(theCodeSystemVersionPid);
307 			doDelete(descriptor, loader, counter, myConceptDesignationDao);
308 		}
309 
310 		// Concepts
311 		{
312 			String descriptor = "concepts";
313 			// For some reason, concepts are much slower to delete, so use a smaller batch size
314 			PageRequest page100 = PageRequest.of(0, 100);
315 			Supplier<Slice<TermConcept>> loader = () -> myConceptDao.findByCodeSystemVersion(page100, theCodeSystemVersionPid);
316 			Supplier<Integer> counter = () -> myConceptDao.countByCodeSystemVersion(theCodeSystemVersionPid);
317 			doDelete(descriptor, loader, counter, myConceptDao);
318 		}
319 
320 		Optional<TermCodeSystem> codeSystemOpt = myCodeSystemDao.findWithCodeSystemVersionAsCurrentVersion(theCodeSystemVersionPid);
321 		if (codeSystemOpt.isPresent()) {
322 			TermCodeSystem codeSystem = codeSystemOpt.get();
323 			ourLog.info(" * Removing code system version {} as current version of code system {}", theCodeSystemVersionPid, codeSystem.getPid());
324 			codeSystem.setCurrentVersion(null);
325 			myCodeSystemDao.save(codeSystem);
326 		}
327 
328 		ourLog.info(" * Deleting code system version");
329 		myCodeSystemVersionDao.deleteById(theCodeSystemVersionPid);
330 
331 	}
332 
333 	public void deleteConceptMap(ResourceTable theResourceTable) {
334 		// Get existing entity so it can be deleted.
335 		Optional<TermConceptMap> optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
336 
337 		if (optionalExistingTermConceptMapById.isPresent()) {
338 			TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
339 
340 			ourLog.info("Deleting existing TermConceptMap {} and its children...", existingTermConceptMap.getId());
341 			for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
342 
343 				for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
344 
345 					for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
346 
347 						myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
348 					}
349 
350 					myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
351 				}
352 
353 				myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
354 			}
355 
356 			myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
357 			ourLog.info("Done deleting existing TermConceptMap {} and its children.", existingTermConceptMap.getId());
358 
359 			ourLog.info("Flushing...");
360 			myConceptMapGroupElementTargetDao.flush();
361 			myConceptMapGroupElementDao.flush();
362 			myConceptMapGroupDao.flush();
363 			myConceptMapDao.flush();
364 			ourLog.info("Done flushing.");
365 		}
366 	}
367 
368 	@Override
369 	@Transactional
370 	public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
371 		deleteConceptMap(theResourceTable);
372 	}
373 
374 	private <T> void doDelete(String theDescriptor, Supplier<Slice<T>> theLoader, Supplier<Integer> theCounter, JpaRepository<T, ?> theDao) {
375 		int count;
376 		ourLog.info(" * Deleting {}", theDescriptor);
377 		int totalCount = theCounter.get();
378 		StopWatch sw = new StopWatch();
379 		count = 0;
380 		while (true) {
381 			Slice<T> link = theLoader.get();
382 			if (link.hasContent() == false) {
383 				break;
384 			}
385 
386 			theDao.deleteInBatch(link);
387 
388 			count += link.getNumberOfElements();
389 			ourLog.info(" * {} {} deleted - {}/sec - ETA: {}", count, theDescriptor, sw.formatThroughput(count, TimeUnit.SECONDS), sw.getEstimatedTimeRemaining(count, totalCount));
390 		}
391 		theDao.flush();
392 	}
393 
394 	private int ensureParentsSaved(Collection<TermConceptParentChildLink> theParents) {
395 		ourLog.trace("Checking {} parents", theParents.size());
396 		int retVal = 0;
397 
398 		for (TermConceptParentChildLink nextLink : theParents) {
399 			if (nextLink.getRelationshipType() == RelationshipTypeEnum.ISA) {
400 				TermConcept nextParent = nextLink.getParent();
401 				retVal += ensureParentsSaved(nextParent.getParents());
402 				if (nextParent.getId() == null) {
403 					nextParent.setUpdated(new Date());
404 					myConceptDao.saveAndFlush(nextParent);
405 					retVal++;
406 					ourLog.debug("Saved parent code {} and got id {}", nextParent.getCode(), nextParent.getId());
407 				}
408 			}
409 		}
410 
411 		return retVal;
412 	}
413 
414 	@Override
415 	@Transactional(propagation = Propagation.REQUIRED)
416 	public ValueSet expandValueSet(ValueSet theValueSetToExpand) {
417 
418 		ValueSet.ValueSetExpansionComponent expansionComponent = new ValueSet.ValueSetExpansionComponent();
419 		expansionComponent.setIdentifier(UUID.randomUUID().toString());
420 		expansionComponent.setTimestamp(new Date());
421 
422 		Set<String> addedCodes = new HashSet<>();
423 		AtomicInteger codeCounter = new AtomicInteger(0);
424 
425 		// Handle includes
426 		ourLog.debug("Handling includes");
427 		for (ValueSet.ConceptSetComponent include : theValueSetToExpand.getCompose().getInclude()) {
428 			boolean add = true;
429 			expandValueSetHandleIncludeOrExclude(expansionComponent, addedCodes, include, add, codeCounter);
430 		}
431 
432 		// Handle excludes
433 		ourLog.debug("Handling excludes");
434 		for (ValueSet.ConceptSetComponent include : theValueSetToExpand.getCompose().getExclude()) {
435 			boolean add = false;
436 			expandValueSetHandleIncludeOrExclude(expansionComponent, addedCodes, include, add, codeCounter);
437 		}
438 
439 		expansionComponent.setTotal(codeCounter.get());
440 
441 		ValueSet valueSet = new ValueSet();
442 		valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);
443 		valueSet.setCompose(theValueSetToExpand.getCompose());
444 		valueSet.setExpansion(expansionComponent);
445 		return valueSet;
446 	}
447 
448 	protected List<VersionIndependentConcept> expandValueSetAndReturnVersionIndependentConcepts(org.hl7.fhir.r4.model.ValueSet theValueSetToExpandR4) {
449 		org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent expandedR4 = expandValueSet(theValueSetToExpandR4).getExpansion();
450 
451 		ArrayList<VersionIndependentConcept> retVal = new ArrayList<>();
452 		for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) {
453 			retVal.add(
454 				new VersionIndependentConcept()
455 					.setSystem(nextContains.getSystem())
456 					.setCode(nextContains.getCode()));
457 		}
458 		return retVal;
459 	}
460 
461 	public void expandValueSetHandleIncludeOrExclude(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theInclude, boolean theAdd, AtomicInteger theCodeCounter) {
462 		String system = theInclude.getSystem();
463 		boolean hasSystem = isNotBlank(system);
464 		boolean hasValueSet = theInclude.getValueSet().size() > 0;
465 
466 		if (hasSystem) {
467 			ourLog.info("Starting {} expansion around code system: {}", (theAdd ? "inclusion" : "exclusion"), system);
468 
469 			TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system);
470 			if (cs != null) {
471 				TermCodeSystemVersion csv = cs.getCurrentVersion();
472 				FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
473 				QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(TermConcept.class).get();
474 				BooleanJunction<?> bool = qb.bool();
475 
476 				bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery());
477 
478 				/*
479 				 * Filters
480 				 */
481 
482 				if (theInclude.getFilter().size() > 0) {
483 
484 					for (ValueSet.ConceptSetFilterComponent nextFilter : theInclude.getFilter()) {
485 						if (isBlank(nextFilter.getValue()) && nextFilter.getOp() == null && isBlank(nextFilter.getProperty())) {
486 							continue;
487 						}
488 
489 						if (isBlank(nextFilter.getValue()) || nextFilter.getOp() == null || isBlank(nextFilter.getProperty())) {
490 							throw new InvalidRequestException("Invalid filter, must have fields populated: property op value");
491 						}
492 
493 
494 						if (nextFilter.getProperty().equals("display:exact") && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
495 							addDisplayFilterExact(qb, bool, nextFilter);
496 						} else if ("display".equals(nextFilter.getProperty()) && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
497 							if (nextFilter.getValue().trim().contains(" ")) {
498 								addDisplayFilterExact(qb, bool, nextFilter);
499 							} else {
500 								addDisplayFilterInexact(qb, bool, nextFilter);
501 							}
502 						} else if (nextFilter.getProperty().equals("concept") || nextFilter.getProperty().equals("code")) {
503 
504 							TermConcept code = findCode(system, nextFilter.getValue());
505 							if (code == null) {
506 								throw new InvalidRequestException("Invalid filter criteria - code does not exist: {" + system + "}" + nextFilter.getValue());
507 							}
508 
509 							if (nextFilter.getOp() == ValueSet.FilterOperator.ISA) {
510 								ourLog.info(" * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay());
511 								bool.must(qb.keyword().onField("myParentPids").matching("" + code.getId()).createQuery());
512 							} else {
513 								throw new InvalidRequestException("Don't know how to handle op=" + nextFilter.getOp() + " on property " + nextFilter.getProperty());
514 							}
515 
516 						} else {
517 
518 							if (nextFilter.getOp() == ValueSet.FilterOperator.REGEX) {
519 
520 								/*
521 								 * We treat the regex filter as a match on the regex
522 								 * anywhere in the property string. The spec does not
523 								 * say whether or not this is the right behaviour, but
524 								 * there are examples that seem to suggest that it is.
525 								 */
526 								String value = nextFilter.getValue();
527 								if (value.endsWith("$")) {
528 									value = value.substring(0, value.length() - 1);
529 								} else if (value.endsWith(".*") == false) {
530 									value = value + ".*";
531 								}
532 								if (value.startsWith("^") == false && value.startsWith(".*") == false) {
533 									value = ".*" + value;
534 								} else if (value.startsWith("^")) {
535 									value = value.substring(1);
536 								}
537 
538 								Term term = new Term(TermConceptPropertyFieldBridge.CONCEPT_FIELD_PROPERTY_PREFIX + nextFilter.getProperty(), value);
539 								RegexpQuery query = new RegexpQuery(term);
540 								bool.must(query);
541 
542 							} else {
543 
544 								String value = nextFilter.getValue();
545 								Term term = new Term(TermConceptPropertyFieldBridge.CONCEPT_FIELD_PROPERTY_PREFIX + nextFilter.getProperty(), value);
546 								bool.must(new TermsQuery(term));
547 
548 							}
549 
550 						}
551 					}
552 
553 				}
554 
555 				Query luceneQuery = bool.createQuery();
556 
557 				/*
558 				 * Include Concepts
559 				 */
560 
561 				List<Term> codes = theInclude
562 					.getConcept()
563 					.stream()
564 					.filter(Objects::nonNull)
565 					.map(ValueSet.ConceptReferenceComponent::getCode)
566 					.filter(StringUtils::isNotBlank)
567 					.map(t -> new Term("myCode", t))
568 					.collect(Collectors.toList());
569 				if (codes.size() > 0) {
570 					MultiPhraseQuery query = new MultiPhraseQuery();
571 					query.add(codes.toArray(new Term[0]));
572 					luceneQuery = new BooleanQuery.Builder()
573 						.add(luceneQuery, BooleanClause.Occur.MUST)
574 						.add(query, BooleanClause.Occur.MUST)
575 						.build();
576 				}
577 
578 				/*
579 				 * Execute the query
580 				 */
581 
582 				FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, TermConcept.class);
583 				int maxResult = 50000;
584 				jpaQuery.setMaxResults(maxResult);
585 
586 				StopWatch sw = new StopWatch();
587 				AtomicInteger count = new AtomicInteger(0);
588 
589 				for (Object next : jpaQuery.getResultList()) {
590 					count.incrementAndGet();
591 					TermConcept concept = (TermConcept) next;
592 					addCodeIfNotAlreadyAdded(theExpansionComponent, theAddedCodes, concept, theAdd, theCodeCounter);
593 				}
594 
595 
596 				if (maxResult == count.get()) {
597 					throw new InternalErrorException("Expansion fragment produced too many (>= " + maxResult + ") results");
598 				}
599 
600 				ourLog.info("Expansion for {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), count, sw.getMillis());
601 
602 			} else {
603 				// No codesystem matching the URL found in the database
604 
605 				CodeSystem codeSystemFromContext = getCodeSystemFromContext(system);
606 				if (codeSystemFromContext == null) {
607 					throw new InvalidRequestException("Unknown code system: " + system);
608 				}
609 
610 				if (theInclude.getConcept().isEmpty() == false) {
611 					for (ValueSet.ConceptReferenceComponent next : theInclude.getConcept()) {
612 						String nextCode = next.getCode();
613 						if (isNotBlank(nextCode) && !theAddedCodes.contains(nextCode)) {
614 							CodeSystem.ConceptDefinitionComponent code = findCode(codeSystemFromContext.getConcept(), nextCode);
615 							if (code != null) {
616 								if (theAdd && theAddedCodes.add(nextCode)) {
617 									ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains();
618 									contains.setCode(nextCode);
619 									contains.setSystem(system);
620 									contains.setDisplay(code.getDisplay());
621 								}
622 								if (!theAdd && theAddedCodes.remove(nextCode)) {
623 									removeCodeFromExpansion(system, nextCode, theExpansionComponent);
624 								}
625 							}
626 						}
627 					}
628 				} else {
629 					List<CodeSystem.ConceptDefinitionComponent> concept = codeSystemFromContext.getConcept();
630 					addConceptsToList(theExpansionComponent, theAddedCodes, system, concept, theAdd);
631 				}
632 
633 			}
634 		} else if (hasValueSet) {
635 			for (CanonicalType nextValueSet : theInclude.getValueSet()) {
636 				ourLog.info("Starting {} expansion around ValueSet URI: {}", (theAdd ? "inclusion" : "exclusion"), nextValueSet.getValueAsString());
637 
638 				List<VersionIndependentConcept> expanded = expandValueSet(nextValueSet.getValueAsString());
639 				for (VersionIndependentConcept nextConcept : expanded) {
640 					if (theAdd) {
641 						TermCodeSystem codeSystem = myCodeSystemDao.findByCodeSystemUri(nextConcept.getSystem());
642 						TermConcept concept = myConceptDao.findByCodeSystemAndCode(codeSystem.getCurrentVersion(), nextConcept.getCode());
643 						addCodeIfNotAlreadyAdded(theExpansionComponent, theAddedCodes, concept, theAdd, theCodeCounter);
644 					}
645 					if (!theAdd && theAddedCodes.remove(nextConcept.getCode())) {
646 						removeCodeFromExpansion(nextConcept.getSystem(), nextConcept.getCode(), theExpansionComponent);
647 					}
648 				}
649 
650 			}
651 		} else {
652 			throw new InvalidRequestException("ValueSet contains " + (theAdd ? "include" : "exclude") + " criteria with no system defined");
653 		}
654 	}
655 
656 	private void fetchChildren(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
657 		for (TermConceptParentChildLink nextChildLink : theConcept.getChildren()) {
658 			TermConcept nextChild = nextChildLink.getChild();
659 			if (addToSet(theSetToPopulate, nextChild)) {
660 				fetchChildren(nextChild, theSetToPopulate);
661 			}
662 		}
663 	}
664 
665 	private TermConcept fetchLoadedCode(Long theCodeSystemResourcePid, String theCode) {
666 		TermCodeSystemVersion codeSystem = myCodeSystemVersionDao.findCurrentVersionForCodeSystemResourcePid(theCodeSystemResourcePid);
667 		return myConceptDao.findByCodeSystemAndCode(codeSystem, theCode);
668 	}
669 
670 	private void fetchParents(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
671 		for (TermConceptParentChildLink nextChildLink : theConcept.getParents()) {
672 			TermConcept nextChild = nextChildLink.getParent();
673 			if (addToSet(theSetToPopulate, nextChild)) {
674 				fetchParents(nextChild, theSetToPopulate);
675 			}
676 		}
677 	}
678 
679 	private CodeSystem.ConceptDefinitionComponent findCode(List<CodeSystem.ConceptDefinitionComponent> theConcepts, String theCode) {
680 		for (CodeSystem.ConceptDefinitionComponent next : theConcepts) {
681 			if (theCode.equals(next.getCode())) {
682 				return next;
683 			}
684 			CodeSystem.ConceptDefinitionComponent val = findCode(next.getConcept(), theCode);
685 			if (val != null) {
686 				return val;
687 			}
688 		}
689 		return null;
690 	}
691 
692 	@Override
693 	public TermConcept findCode(String theCodeSystem, String theCode) {
694 		TermCodeSystemVersion csv = findCurrentCodeSystemVersionForSystem(theCodeSystem);
695 
696 		return myConceptDao.findByCodeSystemAndCode(csv, theCode);
697 	}
698 
699 	@Override
700 	public List<TermConcept> findCodes(String theSystem) {
701 		return myConceptDao.findByCodeSystemVersion(findCurrentCodeSystemVersionForSystem(theSystem));
702 	}
703 
704 	@Transactional(propagation = Propagation.REQUIRED)
705 	@Override
706 	public Set<TermConcept> findCodesAbove(Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
707 		StopWatch stopwatch = new StopWatch();
708 
709 		TermConcept concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
710 		if (concept == null) {
711 			return Collections.emptySet();
712 		}
713 
714 		Set<TermConcept> retVal = new HashSet<>();
715 		retVal.add(concept);
716 
717 		fetchParents(concept, retVal);
718 
719 		ourLog.info("Fetched {} codes above code {} in {}ms", retVal.size(), theCode, stopwatch.getMillis());
720 		return retVal;
721 	}
722 
723 	@Override
724 	public List<VersionIndependentConcept> findCodesAbove(String theSystem, String theCode) {
725 		TermCodeSystem cs = getCodeSystem(theSystem);
726 		if (cs == null) {
727 			return findCodesAboveUsingBuiltInSystems(theSystem, theCode);
728 		}
729 		TermCodeSystemVersion csv = cs.getCurrentVersion();
730 
731 		Set<TermConcept> codes = findCodesAbove(cs.getResource().getId(), csv.getPid(), theCode);
732 		return toVersionIndependentConcepts(theSystem, codes);
733 	}
734 
735 	@Transactional(propagation = Propagation.REQUIRED)
736 	@Override
737 	public Set<TermConcept> findCodesBelow(Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
738 		Stopwatch stopwatch = Stopwatch.createStarted();
739 
740 		TermConcept concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
741 		if (concept == null) {
742 			return Collections.emptySet();
743 		}
744 
745 		Set<TermConcept> retVal = new HashSet<>();
746 		retVal.add(concept);
747 
748 		fetchChildren(concept, retVal);
749 
750 		ourLog.info("Fetched {} codes below code {} in {}ms", retVal.size(), theCode, stopwatch.elapsed(TimeUnit.MILLISECONDS));
751 		return retVal;
752 	}
753 
754 	@Override
755 	public List<VersionIndependentConcept> findCodesBelow(String theSystem, String theCode) {
756 		TermCodeSystem cs = getCodeSystem(theSystem);
757 		if (cs == null) {
758 			return findCodesBelowUsingBuiltInSystems(theSystem, theCode);
759 		}
760 		TermCodeSystemVersion csv = cs.getCurrentVersion();
761 
762 		Set<TermConcept> codes = findCodesBelow(cs.getResource().getId(), csv.getPid(), theCode);
763 		return toVersionIndependentConcepts(theSystem, codes);
764 	}
765 
766 	private TermCodeSystemVersion findCurrentCodeSystemVersionForSystem(String theCodeSystem) {
767 		TermCodeSystem cs = getCodeSystem(theCodeSystem);
768 		if (cs == null || cs.getCurrentVersion() == null) {
769 			return null;
770 		}
771 		return cs.getCurrentVersion();
772 	}
773 
774 	private TermCodeSystem getCodeSystem(String theSystem) {
775 		return myCodeSystemDao.findByCodeSystemUri(theSystem);
776 	}
777 
778 	protected abstract CodeSystem getCodeSystemFromContext(String theSystem);
779 
780 	private void persistChildren(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap<TermConcept, Object> theConceptsStack, int theTotalConcepts) {
781 		if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) {
782 			return;
783 		}
784 
785 		if (theConceptsStack.size() == 1 || theConceptsStack.size() % 10000 == 0) {
786 			float pct = (float) theConceptsStack.size() / (float) theTotalConcepts;
787 			ourLog.info("Have processed {}/{} concepts ({}%)", theConceptsStack.size(), theTotalConcepts, (int) (pct * 100.0f));
788 		}
789 
790 		theConcept.setCodeSystemVersion(theCodeSystem);
791 		theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED);
792 
793 		if (theConceptsStack.size() <= myDaoConfig.getDeferIndexingForCodesystemsOfSize()) {
794 			saveConcept(theConcept);
795 		} else {
796 			myDeferredConcepts.add(theConcept);
797 		}
798 
799 		for (TermConceptParentChildLink next : theConcept.getChildren()) {
800 			persistChildren(next.getChild(), theCodeSystem, theConceptsStack, theTotalConcepts);
801 		}
802 
803 		for (TermConceptParentChildLink next : theConcept.getChildren()) {
804 			if (theConceptsStack.size() <= myDaoConfig.getDeferIndexingForCodesystemsOfSize()) {
805 				saveConceptLink(next);
806 			} else {
807 				myConceptLinksToSaveLater.add(next);
808 			}
809 		}
810 
811 	}
812 
813 	private void populateVersion(TermConcept theNext, TermCodeSystemVersion theCodeSystemVersion) {
814 		if (theNext.getCodeSystemVersion() != null) {
815 			return;
816 		}
817 		theNext.setCodeSystemVersion(theCodeSystemVersion);
818 		for (TermConceptParentChildLink next : theNext.getChildren()) {
819 			populateVersion(next.getChild(), theCodeSystemVersion);
820 		}
821 	}
822 
823 	private void processDeferredConceptMaps() {
824 		int count = Math.min(myDeferredConceptMaps.size(), 20);
825 		for (ConceptMap nextConceptMap : new ArrayList<>(myDeferredConceptMaps.subList(0, count))) {
826 			ourLog.info("Creating ConceptMap: {}", nextConceptMap.getId());
827 			createOrUpdateConceptMap(nextConceptMap);
828 			myDeferredConceptMaps.remove(nextConceptMap);
829 		}
830 		ourLog.info("Saved {} deferred ConceptMap resources, have {} remaining", count, myDeferredConceptMaps.size());
831 	}
832 
833 	private void processDeferredConcepts() {
834 		int codeCount = 0, relCount = 0;
835 		StopWatch stopwatch = new StopWatch();
836 
837 		int count = Math.min(myDaoConfig.getDeferIndexingForCodesystemsOfSize(), myDeferredConcepts.size());
838 		ourLog.info("Saving {} deferred concepts...", count);
839 		while (codeCount < count && myDeferredConcepts.size() > 0) {
840 			TermConcept next = myDeferredConcepts.remove(0);
841 			codeCount += saveConcept(next);
842 		}
843 
844 		if (codeCount > 0) {
845 			ourLog.info("Saved {} deferred concepts ({} codes remain and {} relationships remain) in {}ms ({}ms / code)",
846 				codeCount, myDeferredConcepts.size(), myConceptLinksToSaveLater.size(), stopwatch.getMillis(), stopwatch.getMillisPerOperation(codeCount));
847 		}
848 
849 		if (codeCount == 0) {
850 			count = Math.min(myDaoConfig.getDeferIndexingForCodesystemsOfSize(), myConceptLinksToSaveLater.size());
851 			ourLog.info("Saving {} deferred concept relationships...", count);
852 			while (relCount < count && myConceptLinksToSaveLater.size() > 0) {
853 				TermConceptParentChildLink next = myConceptLinksToSaveLater.remove(0);
854 
855 				if (!myConceptDao.findById(next.getChild().getId()).isPresent() || !myConceptDao.findById(next.getParent().getId()).isPresent()) {
856 					ourLog.warn("Not inserting link from child {} to parent {} because it appears to have been deleted", next.getParent().getCode(), next.getChild().getCode());
857 					continue;
858 				}
859 
860 				saveConceptLink(next);
861 				relCount++;
862 			}
863 		}
864 
865 		if (relCount > 0) {
866 			ourLog.info("Saved {} deferred relationships ({} remain) in {}ms ({}ms / code)",
867 				relCount, myConceptLinksToSaveLater.size(), stopwatch.getMillis(), stopwatch.getMillisPerOperation(codeCount));
868 		}
869 
870 		if ((myDeferredConcepts.size() + myConceptLinksToSaveLater.size()) == 0) {
871 			ourLog.info("All deferred concepts and relationships have now been synchronized to the database");
872 		}
873 	}
874 
875 	private void processDeferredValueSets() {
876 		int count = Math.min(myDeferredValueSets.size(), 20);
877 		for (ValueSet nextValueSet : new ArrayList<>(myDeferredValueSets.subList(0, count))) {
878 			ourLog.info("Creating ValueSet: {}", nextValueSet.getId());
879 			createOrUpdateValueSet(nextValueSet);
880 			myDeferredValueSets.remove(nextValueSet);
881 		}
882 		ourLog.info("Saved {} deferred ValueSet resources, have {} remaining", count, myDeferredValueSets.size());
883 	}
884 
885 	private void processReindexing() {
886 		if (System.currentTimeMillis() < myNextReindexPass && !ourForceSaveDeferredAlwaysForUnitTest) {
887 			return;
888 		}
889 
890 		TransactionTemplate tt = new TransactionTemplate(myTransactionMgr);
891 		tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
892 		tt.execute(new TransactionCallbackWithoutResult() {
893 			private void createParentsString(StringBuilder theParentsBuilder, Long theConceptPid) {
894 				Validate.notNull(theConceptPid, "theConceptPid must not be null");
895 				List<Long> parents = myChildToParentPidCache.get(theConceptPid);
896 				if (parents.contains(-1L)) {
897 					return;
898 				} else if (parents.isEmpty()) {
899 					Collection<Long> parentLinks = myConceptParentChildLinkDao.findAllWithChild(theConceptPid);
900 					if (parentLinks.isEmpty()) {
901 						myChildToParentPidCache.put(theConceptPid, -1L);
902 						ourLog.info("Found {} parent concepts of concept {} (cache has {})", 0, theConceptPid, myChildToParentPidCache.size());
903 						return;
904 					} else {
905 						for (Long next : parentLinks) {
906 							myChildToParentPidCache.put(theConceptPid, next);
907 						}
908 						int parentCount = myChildToParentPidCache.get(theConceptPid).size();
909 						ourLog.info("Found {} parent concepts of concept {} (cache has {})", parentCount, theConceptPid, myChildToParentPidCache.size());
910 					}
911 				}
912 
913 				for (Long nextParent : parents) {
914 					if (theParentsBuilder.length() > 0) {
915 						theParentsBuilder.append(' ');
916 					}
917 					theParentsBuilder.append(nextParent);
918 					createParentsString(theParentsBuilder, nextParent);
919 				}
920 
921 			}
922 
923 			@Override
924 			protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
925 				int maxResult = 1000;
926 				Page<TermConcept> concepts = myConceptDao.findResourcesRequiringReindexing(new PageRequest(0, maxResult));
927 				if (concepts.hasContent() == false) {
928 					if (myChildToParentPidCache != null) {
929 						ourLog.info("Clearing parent concept cache");
930 						myNextReindexPass = System.currentTimeMillis() + DateUtils.MILLIS_PER_MINUTE;
931 						myChildToParentPidCache = null;
932 					}
933 					return;
934 				}
935 
936 				if (myChildToParentPidCache == null) {
937 					myChildToParentPidCache = ArrayListMultimap.create();
938 				}
939 
940 				ourLog.info("Indexing {} / {} concepts", concepts.getContent().size(), concepts.getTotalElements());
941 
942 				int count = 0;
943 				StopWatch stopwatch = new StopWatch();
944 
945 				for (TermConcept nextConcept : concepts) {
946 
947 					if (isBlank(nextConcept.getParentPidsAsString())) {
948 						StringBuilder parentsBuilder = new StringBuilder();
949 						createParentsString(parentsBuilder, nextConcept.getId());
950 						nextConcept.setParentPids(parentsBuilder.toString());
951 					}
952 
953 					saveConcept(nextConcept);
954 					count++;
955 				}
956 
957 				ourLog.info("Indexed {} / {} concepts in {}ms - Avg {}ms / resource", count, concepts.getContent().size(), stopwatch.getMillis(), stopwatch.getMillisPerOperation(count));
958 			}
959 		});
960 
961 	}
962 
963 	private void removeCodeFromExpansion(String theCodeSystem, String theCode, ValueSet.ValueSetExpansionComponent theExpansionComponent) {
964 		theExpansionComponent
965 			.getContains()
966 			.removeIf(t ->
967 				theCodeSystem.equals(t.getSystem()) &&
968 					theCode.equals(t.getCode()));
969 	}
970 
971 	private int saveConcept(TermConcept theConcept) {
972 		int retVal = 0;
973 
974 		/*
975 		 * If the concept has an ID, we're reindexing, so there's no need to
976 		 * save parent concepts first (it's way too slow to do that)
977 		 */
978 		if (theConcept.getId() == null) {
979 			retVal += ensureParentsSaved(theConcept.getParents());
980 		}
981 
982 		if (theConcept.getId() == null || theConcept.getIndexStatus() == null) {
983 			retVal++;
984 			theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED);
985 			theConcept.setUpdated(new Date());
986 			myConceptDao.save(theConcept);
987 
988 			for (TermConceptProperty next : theConcept.getProperties()) {
989 				myConceptPropertyDao.save(next);
990 			}
991 
992 			for (TermConceptDesignation next : theConcept.getDesignations()) {
993 				myConceptDesignationDao.save(next);
994 			}
995 		}
996 
997 		ourLog.trace("Saved {} and got PID {}", theConcept.getCode(), theConcept.getId());
998 		return retVal;
999 	}
1000 
1001 	private void saveConceptLink(TermConceptParentChildLink next) {
1002 		if (next.getId() == null) {
1003 			myConceptParentChildLinkDao.save(next);
1004 		}
1005 	}
1006 
1007 	@Scheduled(fixedRate = 5000)
1008 	@Transactional(propagation = Propagation.NEVER)
1009 	@Override
1010 	public synchronized void saveDeferred() {
1011 		if (!myProcessDeferred) {
1012 			return;
1013 		} else if (myDeferredConcepts.isEmpty() && myConceptLinksToSaveLater.isEmpty()) {
1014 			processReindexing();
1015 		}
1016 
1017 		TransactionTemplate tt = new TransactionTemplate(myTransactionMgr);
1018 		tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
1019 		if (!myDeferredConcepts.isEmpty() || !myConceptLinksToSaveLater.isEmpty()) {
1020 			tt.execute(t -> {
1021 				processDeferredConcepts();
1022 				return null;
1023 			});
1024 		}
1025 
1026 		if (myDeferredValueSets.size() > 0) {
1027 			tt.execute(t -> {
1028 				processDeferredValueSets();
1029 				return null;
1030 			});
1031 		}
1032 		if (myDeferredConceptMaps.size() > 0) {
1033 			tt.execute(t -> {
1034 				processDeferredConceptMaps();
1035 				return null;
1036 			});
1037 		}
1038 
1039 	}
1040 
1041 	@Override
1042 	public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
1043 		myApplicationContext = theApplicationContext;
1044 	}
1045 
1046 	@Override
1047 	public void setProcessDeferred(boolean theProcessDeferred) {
1048 		myProcessDeferred = theProcessDeferred;
1049 	}
1050 
1051 	@PostConstruct
1052 	public void start() {
1053 		myCodeSystemResourceDao = myApplicationContext.getBean(IFhirResourceDaoCodeSystem.class);
1054 	}
1055 
1056 	@Override
1057 	@Transactional(propagation = Propagation.REQUIRED)
1058 	public void storeNewCodeSystemVersion(Long theCodeSystemResourcePid, String theSystemUri, String theSystemName, TermCodeSystemVersion theCodeSystemVersion) {
1059 		ourLog.info("Storing code system");
1060 
1061 		ValidateUtil.isTrueOrThrowInvalidRequest(theCodeSystemVersion.getResource() != null, "No resource supplied");
1062 		ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystemUri, "No system URI supplied");
1063 
1064 		// Grab the existing versions so we can delete them later
1065 		List<TermCodeSystemVersion> existing = myCodeSystemVersionDao.findByCodeSystemResource(theCodeSystemResourcePid);
1066 
1067 //		verifyNoDuplicates(theCodeSystemVersion.getConcepts(), new HashSet<String>());
1068 
1069 		/*
1070 		 * For now we always delete old versions.. At some point it would be nice to allow configuration to keep old versions
1071 		 */
1072 
1073 		ourLog.info("Deleting old code system versions");
1074 		for (TermCodeSystemVersion next : existing) {
1075 			Long codeSystemVersionPid = next.getPid();
1076 			deleteCodeSystemVersion(codeSystemVersionPid);
1077 		}
1078 
1079 		ourLog.info("Flushing...");
1080 		myConceptDao.flush();
1081 		ourLog.info("Done flushing");
1082 
1083 		/*
1084 		 * Do the upload
1085 		 */
1086 
1087 		TermCodeSystem codeSystem = getCodeSystem(theSystemUri);
1088 		if (codeSystem == null) {
1089 			codeSystem = myCodeSystemDao.findByResourcePid(theCodeSystemResourcePid);
1090 			if (codeSystem == null) {
1091 				codeSystem = new TermCodeSystem();
1092 			}
1093 			codeSystem.setResource(theCodeSystemVersion.getResource());
1094 			codeSystem.setCodeSystemUri(theSystemUri);
1095 			codeSystem.setName(theSystemName);
1096 			myCodeSystemDao.save(codeSystem);
1097 		} else {
1098 			if (!ObjectUtil.equals(codeSystem.getResource().getId(), theCodeSystemVersion.getResource().getId())) {
1099 				String msg = myContext.getLocalizer().getMessage(BaseHapiTerminologySvcImpl.class, "cannotCreateDuplicateCodeSystemUri", theSystemUri,
1100 					codeSystem.getResource().getIdDt().toUnqualifiedVersionless().getValue());
1101 				throw new UnprocessableEntityException(msg);
1102 			}
1103 		}
1104 		theCodeSystemVersion.setCodeSystem(codeSystem);
1105 
1106 		ourLog.info("Validating all codes in CodeSystem for storage (this can take some time for large sets)");
1107 
1108 		// Validate the code system
1109 		ArrayList<String> conceptsStack = new ArrayList<>();
1110 		IdentityHashMap<TermConcept, Object> allConcepts = new IdentityHashMap<>();
1111 		int totalCodeCount = 0;
1112 		for (TermConcept next : theCodeSystemVersion.getConcepts()) {
1113 			totalCodeCount += validateConceptForStorage(next, theCodeSystemVersion, conceptsStack, allConcepts);
1114 		}
1115 
1116 		ourLog.info("Saving version containing {} concepts", totalCodeCount);
1117 
1118 		TermCodeSystemVersion codeSystemVersion = myCodeSystemVersionDao.saveAndFlush(theCodeSystemVersion);
1119 
1120 		ourLog.info("Saving code system");
1121 
1122 		codeSystem.setCurrentVersion(theCodeSystemVersion);
1123 		codeSystem = myCodeSystemDao.saveAndFlush(codeSystem);
1124 
1125 		ourLog.info("Setting CodeSystemVersion[{}] on {} concepts...", codeSystem.getPid(), totalCodeCount);
1126 
1127 		for (TermConcept next : theCodeSystemVersion.getConcepts()) {
1128 			populateVersion(next, codeSystemVersion);
1129 		}
1130 
1131 		ourLog.info("Saving {} concepts...", totalCodeCount);
1132 
1133 		IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<TermConcept, Object>();
1134 		for (TermConcept next : theCodeSystemVersion.getConcepts()) {
1135 			persistChildren(next, codeSystemVersion, conceptsStack2, totalCodeCount);
1136 		}
1137 
1138 		ourLog.info("Done saving concepts, flushing to database");
1139 
1140 		myConceptDao.flush();
1141 		myConceptParentChildLinkDao.flush();
1142 
1143 		ourLog.info("Done deleting old code system versions");
1144 
1145 		if (myDeferredConcepts.size() > 0 || myConceptLinksToSaveLater.size() > 0) {
1146 			ourLog.info("Note that some concept saving was deferred - still have {} concepts and {} relationships", myDeferredConcepts.size(), myConceptLinksToSaveLater.size());
1147 		}
1148 	}
1149 
1150 	@Override
1151 	@Transactional(propagation = Propagation.REQUIRED)
1152 	public IIdType storeNewCodeSystemVersion(CodeSystem theCodeSystemResource, TermCodeSystemVersion theCodeSystemVersion, RequestDetails theRequestDetails, List<ValueSet> theValueSets, List<ConceptMap> theConceptMaps) {
1153 		Validate.notBlank(theCodeSystemResource.getUrl(), "theCodeSystemResource must have a URL");
1154 
1155 		IIdType csId = createOrUpdateCodeSystem(theCodeSystemResource);
1156 
1157 		ResourceTable resource = (ResourceTable) myCodeSystemResourceDao.readEntity(csId);
1158 		Long codeSystemResourcePid = resource.getId();
1159 
1160 		ourLog.info("CodeSystem resource has ID: {}", csId.getValue());
1161 
1162 		theCodeSystemVersion.setResource(resource);
1163 		storeNewCodeSystemVersion(codeSystemResourcePid, theCodeSystemResource.getUrl(), theCodeSystemResource.getName(), theCodeSystemVersion);
1164 
1165 		myDeferredConceptMaps.addAll(theConceptMaps);
1166 		myDeferredValueSets.addAll(theValueSets);
1167 
1168 		return csId;
1169 	}
1170 
1171 	@Override
1172 	@Transactional
1173 	public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) {
1174 		ourLog.info("Storing TermConceptMap {}", theConceptMap.getIdElement().getValue());
1175 
1176 		ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
1177 		ValidateUtil.isNotBlankOrThrowUnprocessableEntity(theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url");
1178 
1179 		TermConceptMap termConceptMap = new TermConceptMap();
1180 		termConceptMap.setResource(theResourceTable);
1181 		termConceptMap.setUrl(theConceptMap.getUrl());
1182 
1183 		/*
1184 		 * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions.
1185 		 */
1186 		deleteConceptMap(theResourceTable);
1187 
1188 		/*
1189 		 * Do the upload.
1190 		 */
1191 		String conceptMapUrl = termConceptMap.getUrl();
1192 		Optional<TermConceptMap> optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrl(conceptMapUrl);
1193 		if (!optionalExistingTermConceptMapByUrl.isPresent()) {
1194 			try {
1195 				String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null;
1196 				if (isNotBlank(source)) {
1197 					termConceptMap.setSource(source);
1198 				}
1199 				String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null;
1200 				if (isNotBlank(target)) {
1201 					termConceptMap.setTarget(target);
1202 				}
1203 			} catch (FHIRException fe) {
1204 				throw new InternalErrorException(fe);
1205 			}
1206 			myConceptMapDao.save(termConceptMap);
1207 			int codesSaved = 0;
1208 
1209 			if (theConceptMap.hasGroup()) {
1210 				TermConceptMapGroup termConceptMapGroup;
1211 				for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) {
1212 					if (isBlank(group.getSource())) {
1213 						throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.source");
1214 					}
1215 					if (isBlank(group.getTarget())) {
1216 						throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.target");
1217 					}
1218 					termConceptMapGroup = new TermConceptMapGroup();
1219 					termConceptMapGroup.setConceptMap(termConceptMap);
1220 					termConceptMapGroup.setSource(group.getSource());
1221 					termConceptMapGroup.setSourceVersion(group.getSourceVersion());
1222 					termConceptMapGroup.setTarget(group.getTarget());
1223 					termConceptMapGroup.setTargetVersion(group.getTargetVersion());
1224 					myConceptMapGroupDao.save(termConceptMapGroup);
1225 
1226 					if (group.hasElement()) {
1227 						TermConceptMapGroupElement termConceptMapGroupElement;
1228 						for (ConceptMap.SourceElementComponent element : group.getElement()) {
1229 							termConceptMapGroupElement = new TermConceptMapGroupElement();
1230 							termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup);
1231 							termConceptMapGroupElement.setCode(element.getCode());
1232 							termConceptMapGroupElement.setDisplay(element.getDisplay());
1233 							myConceptMapGroupElementDao.save(termConceptMapGroupElement);
1234 
1235 							if (element.hasTarget()) {
1236 								TermConceptMapGroupElementTarget termConceptMapGroupElementTarget;
1237 								for (ConceptMap.TargetElementComponent target : element.getTarget()) {
1238 									termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
1239 									termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
1240 									termConceptMapGroupElementTarget.setCode(target.getCode());
1241 									termConceptMapGroupElementTarget.setDisplay(target.getDisplay());
1242 									termConceptMapGroupElementTarget.setEquivalence(target.getEquivalence());
1243 									myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
1244 
1245 									if (codesSaved++ % 250 == 0) {
1246 										ourLog.info("Have saved {} codes in conceptmap", codesSaved);
1247 										myConceptMapGroupElementTargetDao.flush();
1248 									}
1249 								}
1250 							}
1251 						}
1252 					}
1253 				}
1254 			}
1255 		} else {
1256 			TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
1257 
1258 			String msg = myContext.getLocalizer().getMessage(
1259 				BaseHapiTerminologySvcImpl.class,
1260 				"cannotCreateDuplicateConceptMapUrl",
1261 				conceptMapUrl,
1262 				existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
1263 
1264 			throw new UnprocessableEntityException(msg);
1265 		}
1266 
1267 		ourLog.info("Done storing TermConceptMap.");
1268 	}
1269 
1270 	@Override
1271 	public boolean supportsSystem(String theSystem) {
1272 		TermCodeSystem cs = getCodeSystem(theSystem);
1273 		return cs != null;
1274 	}
1275 
1276 	private ArrayList<VersionIndependentConcept> toVersionIndependentConcepts(String theSystem, Set<TermConcept> codes) {
1277 		ArrayList<VersionIndependentConcept> retVal = new ArrayList<>(codes.size());
1278 		for (TermConcept next : codes) {
1279 			retVal.add(new VersionIndependentConcept(theSystem, next.getCode()));
1280 		}
1281 		return retVal;
1282 	}
1283 
1284 	@Override
1285 	@Transactional(propagation = Propagation.REQUIRED)
1286 	public List<TermConceptMapGroupElementTarget> translate(TranslationRequest theTranslationRequest) {
1287 		List<TermConceptMapGroupElementTarget> retVal = new ArrayList<>();
1288 
1289 		CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
1290 		CriteriaQuery<TermConceptMapGroupElementTarget> query = criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
1291 		Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
1292 
1293 		Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin = root.join("myConceptMapGroupElement");
1294 		Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
1295 		Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
1296 
1297 		List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
1298 		List<TermConceptMapGroupElementTarget> cachedTargets;
1299 		ArrayList<Predicate> predicates;
1300 		Coding coding;
1301 		for (TranslationQuery translationQuery : translationQueries) {
1302 			cachedTargets = myTranslationCache.getIfPresent(translationQuery);
1303 			if (cachedTargets == null) {
1304 				final List<TermConceptMapGroupElementTarget> targets = new ArrayList<>();
1305 
1306 				predicates = new ArrayList<>();
1307 
1308 				coding = translationQuery.getCoding();
1309 				if (coding.hasCode()) {
1310 					predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode()));
1311 				} else {
1312 					throw new InvalidRequestException("A code must be provided for translation to occur.");
1313 				}
1314 
1315 				if (coding.hasSystem()) {
1316 					predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem()));
1317 				}
1318 
1319 				if (coding.hasVersion()) {
1320 					predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion()));
1321 				}
1322 
1323 				if (translationQuery.hasTargetSystem()) {
1324 					predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem().getValueAsString()));
1325 				}
1326 
1327 				if (translationQuery.hasSource()) {
1328 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource().getValueAsString()));
1329 				}
1330 
1331 				if (translationQuery.hasTarget()) {
1332 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget().getValueAsString()));
1333 				}
1334 
1335 				if (translationQuery.hasResourceId()) {
1336 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
1337 				}
1338 
1339 				Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
1340 				query.where(outerPredicate);
1341 
1342 				// Use scrollable results.
1343 				final TypedQuery<TermConceptMapGroupElementTarget> typedQuery = myEntityManager.createQuery(query.select(root));
1344 				org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
1345 				hibernateQuery.setFetchSize(myFetchSize);
1346 				ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
1347 				Iterator<TermConceptMapGroupElementTarget> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults);
1348 
1349 				while (scrollableResultsIterator.hasNext()) {
1350 					targets.add(scrollableResultsIterator.next());
1351 				}
1352 
1353 				ourLastResultsFromTranslationCache = false; // For testing.
1354 				myTranslationCache.get(translationQuery, k -> targets);
1355 				retVal.addAll(targets);
1356 			} else {
1357 				ourLastResultsFromTranslationCache = true; // For testing.
1358 				retVal.addAll(cachedTargets);
1359 			}
1360 		}
1361 
1362 		return retVal;
1363 	}
1364 
1365 	@Override
1366 	@Transactional(propagation = Propagation.REQUIRED)
1367 	public List<TermConceptMapGroupElement> translateWithReverse(TranslationRequest theTranslationRequest) {
1368 		List<TermConceptMapGroupElement> retVal = new ArrayList<>();
1369 
1370 		CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
1371 		CriteriaQuery<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
1372 		Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
1373 
1374 		Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin = root.join("myConceptMapGroupElementTargets");
1375 		Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
1376 		Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
1377 
1378 		List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
1379 		List<TermConceptMapGroupElement> cachedElements;
1380 		ArrayList<Predicate> predicates;
1381 		Coding coding;
1382 		for (TranslationQuery translationQuery : translationQueries) {
1383 			cachedElements = myTranslationWithReverseCache.getIfPresent(translationQuery);
1384 			if (cachedElements == null) {
1385 				final List<TermConceptMapGroupElement> elements = new ArrayList<>();
1386 
1387 				predicates = new ArrayList<>();
1388 
1389 				coding = translationQuery.getCoding();
1390 				String targetCode = null;
1391 				String targetCodeSystem = null;
1392 				if (coding.hasCode()) {
1393 					predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
1394 					targetCode = coding.getCode();
1395 				} else {
1396 					throw new InvalidRequestException("A code must be provided for translation to occur.");
1397 				}
1398 
1399 				if (coding.hasSystem()) {
1400 					predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
1401 					targetCodeSystem = coding.getSystem();
1402 				}
1403 
1404 				if (coding.hasVersion()) {
1405 					predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion()));
1406 				}
1407 
1408 				if (translationQuery.hasTargetSystem()) {
1409 					predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem().getValueAsString()));
1410 				}
1411 
1412 				if (translationQuery.hasSource()) {
1413 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource().getValueAsString()));
1414 				}
1415 
1416 				if (translationQuery.hasTarget()) {
1417 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget().getValueAsString()));
1418 				}
1419 
1420 				if (translationQuery.hasResourceId()) {
1421 					predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
1422 				}
1423 
1424 				Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
1425 				query.where(outerPredicate);
1426 
1427 				// Use scrollable results.
1428 				final TypedQuery<TermConceptMapGroupElement> typedQuery = myEntityManager.createQuery(query.select(root));
1429 				org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
1430 				hibernateQuery.setFetchSize(myFetchSize);
1431 				ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
1432 				Iterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults);
1433 
1434 				while (scrollableResultsIterator.hasNext()) {
1435 					TermConceptMapGroupElement nextElement = scrollableResultsIterator.next();
1436 					nextElement.getConceptMapGroupElementTargets().size();
1437 					myEntityManager.detach(nextElement);
1438 
1439 					if (isNotBlank(targetCode) && isNotBlank(targetCodeSystem)) {
1440 						for (Iterator<TermConceptMapGroupElementTarget> iter = nextElement.getConceptMapGroupElementTargets().iterator(); iter.hasNext(); ) {
1441 							TermConceptMapGroupElementTarget next = iter.next();
1442 							if (targetCodeSystem.equals(next.getSystem())) {
1443 								if (targetCode.equals(next.getCode())) {
1444 									continue;
1445 								}
1446 							}
1447 
1448 							iter.remove();
1449 						}
1450 					}
1451 
1452 					elements.add(nextElement);
1453 				}
1454 
1455 				ourLastResultsFromTranslationWithReverseCache = false; // For testing.
1456 				myTranslationWithReverseCache.get(translationQuery, k -> elements);
1457 				retVal.addAll(elements);
1458 			} else {
1459 				ourLastResultsFromTranslationWithReverseCache = true; // For testing.
1460 				retVal.addAll(cachedElements);
1461 			}
1462 		}
1463 
1464 		return retVal;
1465 	}
1466 
1467 	private int validateConceptForStorage(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, ArrayList<String> theConceptsStack,
1468 													  IdentityHashMap<TermConcept, Object> theAllConcepts) {
1469 		ValidateUtil.isTrueOrThrowInvalidRequest(theConcept.getCodeSystemVersion() != null, "CodesystemValue is null");
1470 		ValidateUtil.isTrueOrThrowInvalidRequest(theConcept.getCodeSystemVersion() == theCodeSystem, "CodeSystems are not equal");
1471 		ValidateUtil.isNotBlankOrThrowInvalidRequest(theConcept.getCode(), "Codesystem contains a code with no code value");
1472 
1473 		if (theConceptsStack.contains(theConcept.getCode())) {
1474 			throw new InvalidRequestException("CodeSystem contains circular reference around code " + theConcept.getCode());
1475 		}
1476 		theConceptsStack.add(theConcept.getCode());
1477 
1478 		int retVal = 0;
1479 		if (theAllConcepts.put(theConcept, theAllConcepts) == null) {
1480 			if (theAllConcepts.size() % 1000 == 0) {
1481 				ourLog.info("Have validated {} concepts", theAllConcepts.size());
1482 			}
1483 			retVal = 1;
1484 		}
1485 
1486 		for (TermConceptParentChildLink next : theConcept.getChildren()) {
1487 			next.setCodeSystem(theCodeSystem);
1488 			retVal += validateConceptForStorage(next.getChild(), theCodeSystem, theConceptsStack, theAllConcepts);
1489 		}
1490 
1491 		theConceptsStack.remove(theConceptsStack.size() - 1);
1492 
1493 		return retVal;
1494 	}
1495 
1496 	private void verifyNoDuplicates(Collection<TermConcept> theConcepts, Set<String> theCodes) {
1497 		for (TermConcept next : theConcepts) {
1498 			if (!theCodes.add(next.getCode())) {
1499 				throw new InvalidRequestException("Duplicate code " + next.getCode() + " found in codesystem after checking " + theCodes.size() + " codes");
1500 			}
1501 			verifyNoDuplicates(next.getChildren().stream().map(TermConceptParentChildLink::getChild).collect(Collectors.toList()), theCodes);
1502 		}
1503 	}
1504 
1505 	/**
1506 	 * This method is present only for unit tests, do not call from client code
1507 	 */
1508 	@VisibleForTesting
1509 	public static void clearOurLastResultsFromTranslationCache() {
1510 		ourLastResultsFromTranslationCache = false;
1511 	}
1512 
1513 	/**
1514 	 * This method is present only for unit tests, do not call from client code
1515 	 */
1516 	@VisibleForTesting
1517 	public static void clearOurLastResultsFromTranslationWithReverseCache() {
1518 		ourLastResultsFromTranslationWithReverseCache = false;
1519 	}
1520 
1521 	/**
1522 	 * This method is present only for unit tests, do not call from client code
1523 	 */
1524 	@VisibleForTesting
1525 	static boolean isOurLastResultsFromTranslationCache() {
1526 		return ourLastResultsFromTranslationCache;
1527 	}
1528 
1529 	/**
1530 	 * This method is present only for unit tests, do not call from client code
1531 	 */
1532 	@VisibleForTesting
1533 	static boolean isOurLastResultsFromTranslationWithReverseCache() {
1534 		return ourLastResultsFromTranslationWithReverseCache;
1535 	}
1536 
1537 	/**
1538 	 * This method is present only for unit tests, do not call from client code
1539 	 */
1540 	@VisibleForTesting
1541 	public static void setForceSaveDeferredAlwaysForUnitTest(boolean theForceSaveDeferredAlwaysForUnitTest) {
1542 		ourForceSaveDeferredAlwaysForUnitTest = theForceSaveDeferredAlwaysForUnitTest;
1543 	}
1544 }