Sync with upstream/master
This commit is contained in:
commit
98f929997b
|
@ -53,8 +53,8 @@ public class FhirTerser {
|
||||||
if (theChildDefinition == null)
|
if (theChildDefinition == null)
|
||||||
return null;
|
return null;
|
||||||
if (theCurrentList == null || theCurrentList.isEmpty())
|
if (theCurrentList == null || theCurrentList.isEmpty())
|
||||||
return new ArrayList<String>(Arrays.asList(theChildDefinition.getElementName()));
|
return new ArrayList<>(Arrays.asList(theChildDefinition.getElementName()));
|
||||||
List<String> newList = new ArrayList<String>(theCurrentList);
|
List<String> newList = new ArrayList<>(theCurrentList);
|
||||||
newList.add(theChildDefinition.getElementName());
|
newList.add(theChildDefinition.getElementName());
|
||||||
return newList;
|
return newList;
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,20 +174,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISearchDao mySearchDao;
|
private ISearchDao mySearchDao;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISearchParamExtractor mySearchParamExtractor;
|
|
||||||
@Autowired
|
|
||||||
private ISearchParamPresenceSvc mySearchParamPresenceSvc;
|
private ISearchParamPresenceSvc mySearchParamPresenceSvc;
|
||||||
//@Autowired
|
//@Autowired
|
||||||
//private ISearchResultDao mySearchResultDao;
|
//private ISearchResultDao mySearchResultDao;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
|
|
||||||
@Autowired
|
|
||||||
private BeanFactory beanFactory;
|
private BeanFactory beanFactory;
|
||||||
@Autowired
|
@Autowired
|
||||||
private DaoRegistry myDaoRegistry;
|
private DaoRegistry myDaoRegistry;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SearchParamExtractorService mySearchParamExtractorService;
|
|
||||||
@Autowired
|
|
||||||
private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
|
private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
|
||||||
@Autowired
|
@Autowired
|
||||||
private DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer;
|
private DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer;
|
||||||
|
|
|
@ -58,4 +58,6 @@ public interface IResourceReindexingSvc {
|
||||||
* to be used by unit tests.
|
* to be used by unit tests.
|
||||||
*/
|
*/
|
||||||
void cancelAndPurgeAllJobs();
|
void cancelAndPurgeAllJobs();
|
||||||
|
|
||||||
|
int countReindexJobs();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search.reindex;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -30,11 +30,11 @@ import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
|
||||||
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||||
import ca.uhn.fhir.util.StopWatch;
|
import ca.uhn.fhir.util.StopWatch;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
@ -98,6 +98,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
private FhirContext myContext;
|
private FhirContext myContext;
|
||||||
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
|
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
|
||||||
private EntityManager myEntityManager;
|
private EntityManager myEntityManager;
|
||||||
|
@Autowired
|
||||||
|
private ISearchParamRegistry mySearchParamRegistry;
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) {
|
void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) {
|
||||||
|
@ -186,7 +188,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
runReindexingPass();
|
runReindexingPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(Transactional.TxType.NEVER)
|
@Transactional(Transactional.TxType.NEVER)
|
||||||
public Integer runReindexingPass() {
|
public Integer runReindexingPass() {
|
||||||
|
@ -203,7 +204,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer doReindexingPassInsideLock() {
|
private int doReindexingPassInsideLock() {
|
||||||
expungeJobsMarkedAsDeleted();
|
expungeJobsMarkedAsDeleted();
|
||||||
return runReindexJobs();
|
return runReindexJobs();
|
||||||
}
|
}
|
||||||
|
@ -233,13 +234,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int runReindexJobs() {
|
private int runReindexJobs() {
|
||||||
Collection<ResourceReindexJobEntity> jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false));
|
Collection<ResourceReindexJobEntity> jobs = getResourceReindexJobEntities();
|
||||||
assert jobs != null;
|
|
||||||
|
|
||||||
if (jobs.size() > 0) {
|
if (jobs.size() > 0) {
|
||||||
ourLog.info("Running {} reindex jobs: {}", jobs.size(), jobs);
|
ourLog.info("Running {} reindex jobs: {}", jobs.size(), jobs);
|
||||||
} else {
|
} else {
|
||||||
ourLog.debug("Running {} reindex jobs: {}", jobs.size(), jobs);
|
ourLog.debug("Running {} reindex jobs: {}", jobs.size(), jobs);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
@ -255,6 +256,17 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countReindexJobs() {
|
||||||
|
return getResourceReindexJobEntities().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<ResourceReindexJobEntity> getResourceReindexJobEntities() {
|
||||||
|
Collection<ResourceReindexJobEntity> jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false));
|
||||||
|
assert jobs != null;
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
private void markJobAsDeleted(ResourceReindexJobEntity theJob) {
|
private void markJobAsDeleted(ResourceReindexJobEntity theJob) {
|
||||||
ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId());
|
ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId());
|
||||||
myTxTemplate.execute(t -> {
|
myTxTemplate.execute(t -> {
|
||||||
|
@ -263,6 +275,11 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) {
|
||||||
|
mySearchParamRegistry = theSearchParamRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
private int runReindexJob(ResourceReindexJobEntity theJob) {
|
private int runReindexJob(ResourceReindexJobEntity theJob) {
|
||||||
if (theJob.getSuspendedUntil() != null) {
|
if (theJob.getSuspendedUntil() != null) {
|
||||||
if (theJob.getSuspendedUntil().getTime() > System.currentTimeMillis()) {
|
if (theJob.getSuspendedUntil().getTime() > System.currentTimeMillis()) {
|
||||||
|
@ -274,6 +291,15 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
StopWatch sw = new StopWatch();
|
StopWatch sw = new StopWatch();
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* On the first time we run a particular reindex job, let's make sure we
|
||||||
|
* have the latest search parameters loaded. This is good since a common reason to
|
||||||
|
* be reindexing is that the search parameters have changed in some way.
|
||||||
|
*/
|
||||||
|
if (theJob.getThresholdLow() == null) {
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate range
|
// Calculate range
|
||||||
Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME;
|
Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME;
|
||||||
Date high = theJob.getThresholdHigh();
|
Date high = theJob.getThresholdHigh();
|
||||||
|
@ -461,7 +487,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
|
||||||
|
|
||||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceTable.getResourceType());
|
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceTable.getResourceType());
|
||||||
long expectedVersion = resourceTable.getVersion();
|
long expectedVersion = resourceTable.getVersion();
|
||||||
IBaseResource resource = dao.read(resourceTable.getIdDt().toVersionless(), null,true);
|
IBaseResource resource = dao.read(resourceTable.getIdDt().toVersionless(), null, true);
|
||||||
if (resource == null) {
|
if (resource == null) {
|
||||||
throw new InternalErrorException("Could not find resource version " + resourceTable.getIdDt().toUnqualified().getValue() + " in database");
|
throw new InternalErrorException("Could not find resource version " + resourceTable.getIdDt().toUnqualified().getValue() + " in database");
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
|
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
@ -17,15 +19,13 @@ import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.entity.ContentType;
|
import org.apache.http.entity.ContentType;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.*;
|
||||||
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
|
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
|
||||||
import org.hl7.fhir.r4.model.Bundle.BundleType;
|
import org.hl7.fhir.r4.model.Bundle.BundleType;
|
||||||
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
|
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
|
||||||
import org.hl7.fhir.r4.model.Organization;
|
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
|
||||||
import org.hl7.fhir.r4.model.Reference;
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -38,6 +38,7 @@ import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.in;
|
||||||
import static org.hamcrest.Matchers.startsWith;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
@ -236,6 +237,32 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
public void testCreateReflexResourceTheHardWay() throws IOException, ServletException {
|
||||||
|
ServerOperationInterceptorAdapter interceptor = new ReflexInterceptor();
|
||||||
|
|
||||||
|
ourRestServer.registerInterceptor(interceptor);
|
||||||
|
try {
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setActive(true);
|
||||||
|
IIdType pid = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
Bundle observations = ourClient
|
||||||
|
.search()
|
||||||
|
.forResource("Observation")
|
||||||
|
.where(Observation.SUBJECT.hasId(pid))
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertEquals(1, observations.getEntry().size());
|
||||||
|
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(observations));
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
ourRestServer.unregisterInterceptor(interceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testCreateResourceWithVersionedReference() throws IOException, ServletException {
|
public void testCreateResourceWithVersionedReference() throws IOException, ServletException {
|
||||||
String methodName = "testCreateResourceWithVersionedReference";
|
String methodName = "testCreateResourceWithVersionedReference";
|
||||||
|
|
||||||
|
@ -353,6 +380,26 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ReflexInterceptor extends ServerOperationInterceptorAdapter {
|
||||||
|
@Override
|
||||||
|
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
if (theResource instanceof Patient) {
|
||||||
|
((ServletRequestDetails) theRequest).getServletRequest().setAttribute("CREATED_PATIENT", theResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processingCompletedNormally(ServletRequestDetails theRequestDetails) {
|
||||||
|
Patient createdPatient = (Patient) theRequestDetails.getServletRequest().getAttribute("CREATED_PATIENT");
|
||||||
|
if (createdPatient != null) {
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.setSubject(new Reference(createdPatient.getId()));
|
||||||
|
|
||||||
|
ourClient.create().resource(observation).execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void afterClassClearContext() {
|
public static void afterClassClearContext() {
|
||||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||||
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
@ -63,6 +64,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest {
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<Date> myHighCaptor;
|
private ArgumentCaptor<Date> myHighCaptor;
|
||||||
private ResourceReindexJobEntity mySingleJob;
|
private ResourceReindexJobEntity mySingleJob;
|
||||||
|
@Mock
|
||||||
|
private ISearchParamRegistry mySearchParamRegistry;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FhirContext getContext() {
|
protected FhirContext getContext() {
|
||||||
|
@ -87,6 +90,7 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest {
|
||||||
mySvc.setReindexJobDaoForUnitTest(myReindexJobDao);
|
mySvc.setReindexJobDaoForUnitTest(myReindexJobDao);
|
||||||
mySvc.setResourceTableDaoForUnitTest(myResourceTableDao);
|
mySvc.setResourceTableDaoForUnitTest(myResourceTableDao);
|
||||||
mySvc.setTxManagerForUnitTest(myTxManager);
|
mySvc.setTxManagerForUnitTest(myTxManager);
|
||||||
|
mySvc.setSearchParamRegistryForUnitTest(mySearchParamRegistry);
|
||||||
mySvc.start();
|
mySvc.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,6 +179,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest {
|
||||||
verify(myReindexJobDao, times(1)).getReindexCount(any());
|
verify(myReindexJobDao, times(1)).getReindexCount(any());
|
||||||
verify(myReindexJobDao, times(1)).setReindexCount(any(), anyInt());
|
verify(myReindexJobDao, times(1)).setReindexCount(any(), anyInt());
|
||||||
verifyNoMoreInteractions(myReindexJobDao);
|
verifyNoMoreInteractions(myReindexJobDao);
|
||||||
|
|
||||||
|
verify(mySearchParamRegistry, times(1)).forceRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.dstu3.model.CodeSystem;
|
import org.hl7.fhir.dstu3.model.CodeSystem;
|
||||||
import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode;
|
import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode;
|
||||||
|
@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
@ -583,6 +585,50 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a custom ValueSet against a custom CodeSystem expands correctly
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testCustomValueSetExpansion() {
|
||||||
|
|
||||||
|
CodeSystem cs= new CodeSystem();
|
||||||
|
cs.setUrl("http://codesystems-r-us");
|
||||||
|
cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
|
||||||
|
IIdType csId = myCodeSystemDao.create(cs).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
TermCodeSystemVersion version = new TermCodeSystemVersion();
|
||||||
|
version.getConcepts().add(new TermConcept(version, "A"));
|
||||||
|
version.getConcepts().add(new TermConcept(version, "B"));
|
||||||
|
version.getConcepts().add(new TermConcept(version, "C"));
|
||||||
|
version.getConcepts().add(new TermConcept(version, "D"));
|
||||||
|
runInTransaction(()->{
|
||||||
|
ResourceTable resTable = myEntityManager.find(ResourceTable.class, csId.getIdPartAsLong());
|
||||||
|
version.setResource(resTable);
|
||||||
|
myTermSvc.storeNewCodeSystemVersion(csId.getIdPartAsLong(), cs.getUrl(), "My System", version);
|
||||||
|
});
|
||||||
|
|
||||||
|
org.hl7.fhir.dstu3.model.ValueSet vs = new org.hl7.fhir.dstu3.model.ValueSet();
|
||||||
|
vs.setUrl("http://valuesets-r-us");
|
||||||
|
vs.getCompose()
|
||||||
|
.addInclude()
|
||||||
|
.setSystem(cs.getUrl())
|
||||||
|
.addConcept(new org.hl7.fhir.dstu3.model.ValueSet.ConceptReferenceComponent().setCode("A"))
|
||||||
|
.addConcept(new org.hl7.fhir.dstu3.model.ValueSet.ConceptReferenceComponent().setCode("C"));
|
||||||
|
myValueSetDao.create(vs);
|
||||||
|
|
||||||
|
org.hl7.fhir.dstu3.model.ValueSet expansion = myValueSetDao.expandByIdentifier(vs.getUrl(), null);
|
||||||
|
List<String> expansionCodes = expansion
|
||||||
|
.getExpansion()
|
||||||
|
.getContains()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getCode())
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(Lists.newArrayList("A","C"), expansionCodes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static List<String> toCodesContains(List<ValueSet.ValueSetExpansionContainsComponent> theContains) {
|
public static List<String> toCodesContains(List<ValueSet.ValueSetExpansionContainsComponent> theContains) {
|
||||||
List<String> retVal = new ArrayList<>();
|
List<String> retVal = new ArrayList<>();
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.migrate;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -53,7 +53,7 @@ public class JdbcUtils {
|
||||||
DatabaseMetaData metadata;
|
DatabaseMetaData metadata;
|
||||||
try {
|
try {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, true);
|
ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), false, true);
|
||||||
|
|
||||||
Set<String> indexNames = new HashSet<>();
|
Set<String> indexNames = new HashSet<>();
|
||||||
while (indexes.next()) {
|
while (indexes.next()) {
|
||||||
|
@ -81,7 +81,7 @@ public class JdbcUtils {
|
||||||
DatabaseMetaData metadata;
|
DatabaseMetaData metadata;
|
||||||
try {
|
try {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, false);
|
ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), false, false);
|
||||||
|
|
||||||
while (indexes.next()) {
|
while (indexes.next()) {
|
||||||
String indexName = indexes.getString("INDEX_NAME");
|
String indexName = indexes.getString("INDEX_NAME");
|
||||||
|
@ -112,7 +112,7 @@ public class JdbcUtils {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
String catalog = connection.getCatalog();
|
String catalog = connection.getCatalog();
|
||||||
String schema = connection.getSchema();
|
String schema = connection.getSchema();
|
||||||
ResultSet indexes = metadata.getColumns(catalog, schema, theTableName, null);
|
ResultSet indexes = metadata.getColumns(catalog, schema, massageIdentifier(metadata, theTableName), null);
|
||||||
|
|
||||||
while (indexes.next()) {
|
while (indexes.next()) {
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ public class JdbcUtils {
|
||||||
DatabaseMetaData metadata;
|
DatabaseMetaData metadata;
|
||||||
try {
|
try {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), theTableName, connection.getCatalog(), connection.getSchema(), theForeignTable);
|
ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theForeignTable));
|
||||||
|
|
||||||
Set<String> columnNames = new HashSet<>();
|
Set<String> columnNames = new HashSet<>();
|
||||||
while (indexes.next()) {
|
while (indexes.next()) {
|
||||||
|
@ -201,7 +201,7 @@ public class JdbcUtils {
|
||||||
DatabaseMetaData metadata;
|
DatabaseMetaData metadata;
|
||||||
try {
|
try {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, null);
|
ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null);
|
||||||
|
|
||||||
Set<String> columnNames = new HashSet<>();
|
Set<String> columnNames = new HashSet<>();
|
||||||
while (indexes.next()) {
|
while (indexes.next()) {
|
||||||
|
@ -253,7 +253,7 @@ public class JdbcUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sequenceNames;
|
return sequenceNames;
|
||||||
} catch (SQLException e ) {
|
} catch (SQLException e) {
|
||||||
throw new InternalErrorException(e);
|
throw new InternalErrorException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -298,7 +298,7 @@ public class JdbcUtils {
|
||||||
DatabaseMetaData metadata;
|
DatabaseMetaData metadata;
|
||||||
try {
|
try {
|
||||||
metadata = connection.getMetaData();
|
metadata = connection.getMetaData();
|
||||||
ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, theColumnName);
|
ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null);
|
||||||
|
|
||||||
while (tables.next()) {
|
while (tables.next()) {
|
||||||
String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US);
|
String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US);
|
||||||
|
@ -325,4 +325,14 @@ public class JdbcUtils {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String massageIdentifier(DatabaseMetaData theMetadata, String theCatalog) throws SQLException {
|
||||||
|
String retVal = theCatalog;
|
||||||
|
if (theMetadata.storesLowerCaseIdentifiers()) {
|
||||||
|
retVal = retVal.toLowerCase();
|
||||||
|
} else {
|
||||||
|
retVal = retVal.toUpperCase();
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,10 +57,9 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
|
||||||
@Autowired
|
@Autowired
|
||||||
private ModelConfig myModelConfig;
|
private ModelConfig myModelConfig;
|
||||||
private volatile long myLastRefresh;
|
private volatile long myLastRefresh;
|
||||||
private ApplicationContext myApplicationContext;
|
|
||||||
private ISearchParamProvider mySearchParamProvider;
|
private ISearchParamProvider mySearchParamProvider;
|
||||||
|
|
||||||
public BaseSearchParamRegistry(ISearchParamProvider theSearchParamProvider) {
|
BaseSearchParamRegistry(ISearchParamProvider theSearchParamProvider) {
|
||||||
super();
|
super();
|
||||||
mySearchParamProvider = theSearchParamProvider;
|
mySearchParamProvider = theSearchParamProvider;
|
||||||
}
|
}
|
||||||
|
@ -128,7 +127,7 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
|
||||||
return Collections.unmodifiableList(retVal);
|
return Collections.unmodifiableList(retVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
|
private Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
|
||||||
return myBuiltInSearchParams;
|
return myBuiltInSearchParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
private Function<IWorkerContext, IEnableWhenEvaluator> enableWhenEvaluatorSupplier = ctx -> new DefaultEnableWhenEvaluator();
|
private Function<IWorkerContext, IEnableWhenEvaluator> enableWhenEvaluatorSupplier = ctx -> new DefaultEnableWhenEvaluator();
|
||||||
|
|
||||||
private boolean errorForUnknownProfiles;
|
private boolean errorForUnknownProfiles;
|
||||||
|
private List<String> extensionDomains = Collections.emptyList();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -87,18 +88,52 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
myValidationSupport = theValidationSupport;
|
myValidationSupport = theValidationSupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String determineResourceName(Document theDocument) {
|
/**
|
||||||
Element root = null;
|
* Every element in a resource or data type includes an optional <it>extension</it> child element
|
||||||
|
* which is identified by it's {@code url attribute}. There exists a number of predefined
|
||||||
|
* extension urls or extension domains:<ul>
|
||||||
|
* <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
|
||||||
|
* <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
|
||||||
|
* </ul>
|
||||||
|
* It is possible to extend this list of known extension by defining custom extensions:
|
||||||
|
* Any url which starts which one of the elements in the list of custom extension domains is
|
||||||
|
* considered as known.
|
||||||
|
* <p>
|
||||||
|
* Any unknown extension domain will result in an information message when validating a resource.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public FhirInstanceValidator setCustomExtensionDomains(List<String> extensionDomains) {
|
||||||
|
this.extensionDomains = extensionDomains;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every element in a resource or data type includes an optional <it>extension</it> child element
|
||||||
|
* which is identified by it's {@code url attribute}. There exists a number of predefined
|
||||||
|
* extension urls or extension domains:<ul>
|
||||||
|
* <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
|
||||||
|
* <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
|
||||||
|
* </ul>
|
||||||
|
* It is possible to extend this list of known extension by defining custom extensions:
|
||||||
|
* Any url which starts which one of the elements in the list of custom extension domains is
|
||||||
|
* considered as known.
|
||||||
|
* <p>
|
||||||
|
* Any unknown extension domain will result in an information message when validating a resource.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public FhirInstanceValidator setCustomExtensionDomains(String... extensionDomains) {
|
||||||
|
this.extensionDomains = Arrays.asList(extensionDomains);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String determineResourceName(Document theDocument) {
|
||||||
NodeList list = theDocument.getChildNodes();
|
NodeList list = theDocument.getChildNodes();
|
||||||
for (int i = 0; i < list.getLength(); i++) {
|
for (int i = 0; i < list.getLength(); i++) {
|
||||||
if (list.item(i) instanceof Element) {
|
if (list.item(i) instanceof Element) {
|
||||||
root = (Element) list.item(i);
|
return list.item(i).getLocalName();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root = theDocument.getDocumentElement();
|
return theDocument.getDocumentElement().getLocalName();
|
||||||
return root.getLocalName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ArrayList<String> determineIfProfilesSpecified(Document theDocument) {
|
private ArrayList<String> determineIfProfilesSpecified(Document theDocument) {
|
||||||
|
@ -144,7 +179,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
* guielines will be ignored.
|
* guielines will be ignored.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)}
|
* @see #setBestPracticeWarningLevel(BestPracticeWarningLevel)
|
||||||
*/
|
*/
|
||||||
public BestPracticeWarningLevel getBestPracticeWarningLevel() {
|
public BestPracticeWarningLevel getBestPracticeWarningLevel() {
|
||||||
return myBestPracticeWarningLevel;
|
return myBestPracticeWarningLevel;
|
||||||
|
@ -260,6 +295,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
v.setNoTerminologyChecks(isNoTerminologyChecks());
|
v.setNoTerminologyChecks(isNoTerminologyChecks());
|
||||||
v.setMyEnableWhenEvaluator(enableWhenEvaluatorSupplier.apply(wrappedWorkerContext));
|
v.setMyEnableWhenEvaluator(enableWhenEvaluatorSupplier.apply(wrappedWorkerContext));
|
||||||
v.setErrorForUnknownProfiles(isErrorForUnknownProfiles());
|
v.setErrorForUnknownProfiles(isErrorForUnknownProfiles());
|
||||||
|
v.addExtensionDomains(extensionDomains);
|
||||||
|
|
||||||
List<ValidationMessage> messages = new ArrayList<>();
|
List<ValidationMessage> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -368,7 +404,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
private LoadingCache<ResourceKey, org.hl7.fhir.r4.model.Resource> myFetchResourceCache;
|
private LoadingCache<ResourceKey, org.hl7.fhir.r4.model.Resource> myFetchResourceCache;
|
||||||
private org.hl7.fhir.r4.model.Parameters myExpansionProfile;
|
private org.hl7.fhir.r4.model.Parameters myExpansionProfile;
|
||||||
|
|
||||||
public WorkerContextWrapper(HapiWorkerContext theWorkerContext) {
|
WorkerContextWrapper(HapiWorkerContext theWorkerContext) {
|
||||||
myWrap = theWorkerContext;
|
myWrap = theWorkerContext;
|
||||||
myConverter = new VersionConvertor_30_40();
|
myConverter = new VersionConvertor_30_40();
|
||||||
|
|
||||||
|
@ -449,7 +485,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cacheResource(org.hl7.fhir.r4.model.Resource res) throws FHIRException {
|
public void cacheResource(org.hl7.fhir.r4.model.Resource res) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,7 +507,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ValueSet source, boolean cacheOk, boolean heiarchical) {
|
public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ValueSet source, boolean cacheOk, boolean heiarchical) {
|
||||||
ValueSet convertedSource = null;
|
ValueSet convertedSource;
|
||||||
try {
|
try {
|
||||||
convertedSource = VersionConvertor_30_40.convertValueSet(source);
|
convertedSource = VersionConvertor_30_40.convertValueSet(source);
|
||||||
} catch (FHIRException e) {
|
} catch (FHIRException e) {
|
||||||
|
@ -495,7 +531,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heiarchical) throws FHIRException {
|
public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heiarchical) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,7 +698,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IResourceValidator newValidator() throws FHIRException {
|
public IResourceValidator newValidator() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,7 +723,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsSystem(String system) throws TerminologyServiceException {
|
public boolean supportsSystem(String system) {
|
||||||
return myWrap.supportsSystem(system);
|
return myWrap.supportsSystem(system);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -704,6 +740,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult validateCode(String system, String code, String display) {
|
public ValidationResult validateCode(String system, String code, String display) {
|
||||||
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(system, code, display);
|
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(system, code, display);
|
||||||
|
// TODO: converted code might be null -> NPE
|
||||||
return convertValidationResult(result);
|
return convertValidationResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -754,6 +791,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
throw new InternalErrorException(e);
|
throw new InternalErrorException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: converted code might be null -> NPE
|
||||||
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs);
|
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs);
|
||||||
return convertValidationResult(result);
|
return convertValidationResult(result);
|
||||||
}
|
}
|
||||||
|
@ -774,6 +812,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
throw new InternalErrorException(e);
|
throw new InternalErrorException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: converted code might be null -> NPE
|
||||||
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs);
|
org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs);
|
||||||
return convertValidationResult(result);
|
return convertValidationResult(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -45,6 +46,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
private DocumentBuilderFactory myDocBuilderFactory;
|
private DocumentBuilderFactory myDocBuilderFactory;
|
||||||
private boolean myNoTerminologyChecks;
|
private boolean myNoTerminologyChecks;
|
||||||
private StructureDefinition myStructureDefintion;
|
private StructureDefinition myStructureDefintion;
|
||||||
|
private List<String> extensionDomains = Collections.emptyList();
|
||||||
|
|
||||||
private IValidationSupport myValidationSupport;
|
private IValidationSupport myValidationSupport;
|
||||||
|
|
||||||
|
@ -68,18 +70,52 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
myValidationSupport = theValidationSupport;
|
myValidationSupport = theValidationSupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String determineResourceName(Document theDocument) {
|
/**
|
||||||
Element root = null;
|
* Every element in a resource or data type includes an optional <it>extension</it> child element
|
||||||
|
* which is identified by it's {@code url attribute}. There exists a number of predefined
|
||||||
|
* extension urls or extension domains:<ul>
|
||||||
|
* <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
|
||||||
|
* <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
|
||||||
|
* </ul>
|
||||||
|
* It is possible to extend this list of known extension by defining custom extensions:
|
||||||
|
* Any url which starts which one of the elements in the list of custom extension domains is
|
||||||
|
* considered as known.
|
||||||
|
* <p>
|
||||||
|
* Any unknown extension domain will result in an information message when validating a resource.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public FhirInstanceValidator setCustomExtensionDomains(List<String> extensionDomains) {
|
||||||
|
this.extensionDomains = extensionDomains;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every element in a resource or data type includes an optional <it>extension</it> child element
|
||||||
|
* which is identified by it's {@code url attribute}. There exists a number of predefined
|
||||||
|
* extension urls or extension domains:<ul>
|
||||||
|
* <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
|
||||||
|
* <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
|
||||||
|
* </ul>
|
||||||
|
* It is possible to extend this list of known extension by defining custom extensions:
|
||||||
|
* Any url which starts which one of the elements in the list of custom extension domains is
|
||||||
|
* considered as known.
|
||||||
|
* <p>
|
||||||
|
* Any unknown extension domain will result in an information message when validating a resource.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public FhirInstanceValidator setCustomExtensionDomains(String... extensionDomains) {
|
||||||
|
this.extensionDomains = Arrays.asList(extensionDomains);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String determineResourceName(Document theDocument) {
|
||||||
NodeList list = theDocument.getChildNodes();
|
NodeList list = theDocument.getChildNodes();
|
||||||
for (int i = 0; i < list.getLength(); i++) {
|
for (int i = 0; i < list.getLength(); i++) {
|
||||||
if (list.item(i) instanceof Element) {
|
if (list.item(i) instanceof Element) {
|
||||||
root = (Element) list.item(i);
|
return list.item(i).getLocalName();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root = theDocument.getDocumentElement();
|
return theDocument.getDocumentElement().getLocalName();
|
||||||
return root.getLocalName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ArrayList<String> determineIfProfilesSpecified(Document theDocument) {
|
private ArrayList<String> determineIfProfilesSpecified(Document theDocument) {
|
||||||
|
@ -120,8 +156,8 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
* reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
|
* reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
|
||||||
* guielines will be ignored.
|
* guielines will be ignored.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)}
|
* @see #setBestPracticeWarningLevel(BestPracticeWarningLevel)
|
||||||
*/
|
*/
|
||||||
public BestPracticeWarningLevel getBestPracticeWarningLevel() {
|
public BestPracticeWarningLevel getBestPracticeWarningLevel() {
|
||||||
return myBestPracticeWarningLevel;
|
return myBestPracticeWarningLevel;
|
||||||
|
@ -211,8 +247,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
v.setAnyExtensionsAllowed(isAnyExtensionsAllowed());
|
v.setAnyExtensionsAllowed(isAnyExtensionsAllowed());
|
||||||
v.setResourceIdRule(IdStatus.OPTIONAL);
|
v.setResourceIdRule(IdStatus.OPTIONAL);
|
||||||
v.setNoTerminologyChecks(isNoTerminologyChecks());
|
v.setNoTerminologyChecks(isNoTerminologyChecks());
|
||||||
|
v.addExtensionDomains(extensionDomains);
|
||||||
|
|
||||||
List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
|
List<ValidationMessage> messages = new ArrayList<>();
|
||||||
|
|
||||||
if (theEncoding == EncodingEnum.XML) {
|
if (theEncoding == EncodingEnum.XML) {
|
||||||
Document document;
|
Document document;
|
||||||
|
@ -312,7 +349,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
public static class NullEvaluationContext implements IEvaluationContext {
|
public static class NullEvaluationContext implements IEvaluationContext {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) throws PathEngineException {
|
public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,12 +364,12 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException {
|
public Base resolveConstant(Object theAppContext, String theName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TypeDetails resolveConstantType(Object theAppContext, String theName) throws PathEngineException {
|
public TypeDetails resolveConstantType(Object theAppContext, String theName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -420,8 +420,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean allowUnknownExtension(String url) {
|
private boolean allowUnknownExtension(String url) {
|
||||||
if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression"))
|
if (isPredefinedExtension(url))
|
||||||
// Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with
|
|
||||||
return true;
|
return true;
|
||||||
for (String s : extensionDomains)
|
for (String s : extensionDomains)
|
||||||
if (url.startsWith(s))
|
if (url.startsWith(s))
|
||||||
|
@ -430,8 +429,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isKnownExtension(String url) {
|
private boolean isKnownExtension(String url) {
|
||||||
// Added structuredefinition-expression and following extensions explicitly because they weren't defined in the version of the spec they need to be used with
|
if (isPredefinedExtension(url))
|
||||||
if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression") || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION))
|
|
||||||
return true;
|
return true;
|
||||||
for (String s : extensionDomains)
|
for (String s : extensionDomains)
|
||||||
if (url.startsWith(s))
|
if (url.startsWith(s))
|
||||||
|
@ -439,6 +437,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isPredefinedExtension(String url) {
|
||||||
|
return url.contains("example.org")
|
||||||
|
|| url.contains("acme.com")
|
||||||
|
|| url.contains("nema.org")
|
||||||
|
|| url.startsWith("http://hl7.org/fhir/StructureDefinition/")
|
||||||
|
|| url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
private void bpCheck(List<ValidationMessage> errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) {
|
private void bpCheck(List<ValidationMessage> errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) {
|
||||||
if (bpWarnings != null) {
|
if (bpWarnings != null) {
|
||||||
switch (bpWarnings) {
|
switch (bpWarnings) {
|
||||||
|
@ -1954,6 +1960,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
return extensionDomains;
|
return extensionDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InstanceValidator addExtensionDomains(List<String> extensionDomains) {
|
||||||
|
this.extensionDomains.addAll(extensionDomains);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private Element getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path) {
|
private Element getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path) {
|
||||||
String targetUrl = null;
|
String targetUrl = null;
|
||||||
String version = "";
|
String version = "";
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package org.hl7.fhir.dstu3.hapi.validation;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
import ca.uhn.fhir.validation.FhirValidator;
|
||||||
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
|
import ca.uhn.fhir.validation.ValidationResult;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||||
|
import org.hl7.fhir.dstu3.model.CodeableConcept;
|
||||||
|
import org.hl7.fhir.dstu3.model.Coding;
|
||||||
|
import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
|
||||||
|
import org.hl7.fhir.dstu3.model.Questionnaire;
|
||||||
|
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class QuestionnaireValidatorDstu3Test {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(QuestionnaireValidatorDstu3Test.class);
|
||||||
|
private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport();
|
||||||
|
private static FhirContext ourCtx = FhirContext.forDstu3();
|
||||||
|
private FhirInstanceValidator myInstanceVal;
|
||||||
|
private FhirValidator myVal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
IValidationSupport myValSupport = mock(IValidationSupport.class);
|
||||||
|
|
||||||
|
myVal = ourCtx.newValidator();
|
||||||
|
myVal.setValidateAgainstStandardSchema(false);
|
||||||
|
myVal.setValidateAgainstStandardSchematron(false);
|
||||||
|
|
||||||
|
ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport);
|
||||||
|
myInstanceVal = new FhirInstanceValidator(validationSupport);
|
||||||
|
|
||||||
|
myVal.registerValidatorModule(myInstanceVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithPredefinedExtensionDomainsForCoding() {
|
||||||
|
String[] extensionDomainsToTest = new String[] {
|
||||||
|
"http://example.org/questionnaire-color-control-1",
|
||||||
|
"https://example.org/questionnaire-color-control-2",
|
||||||
|
"http://acme.com/questionnaire-color-control-3",
|
||||||
|
"https://acme.com/questionnaire-color-control-4",
|
||||||
|
"http://nema.org/questionnaire-color-control-5",
|
||||||
|
"https://nema.org/questionnaire-color-control-6",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/questionnaire-scoreItem",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/structuredefinition-expression",
|
||||||
|
};
|
||||||
|
for (String extensionDomainToTest : extensionDomainsToTest) {
|
||||||
|
Questionnaire q = new Questionnaire();
|
||||||
|
q.setStatus(PublicationStatus.ACTIVE)
|
||||||
|
.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionDomainToTest)
|
||||||
|
.setValue(new Coding(null, "text-box", null));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithPredefinedExtensionDomainsForCodeableConcept() {
|
||||||
|
String[] extensionDomainsToTest = new String[] {
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
|
||||||
|
};
|
||||||
|
for (String extensionDomainToTest : extensionDomainsToTest) {
|
||||||
|
Questionnaire q = new Questionnaire();
|
||||||
|
q.setStatus(PublicationStatus.ACTIVE)
|
||||||
|
.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionDomainToTest)
|
||||||
|
.setValue(new CodeableConcept().addCoding(new Coding(null, "text-box", null)));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithCustomExtensionDomain() {
|
||||||
|
Questionnaire q = new Questionnaire();
|
||||||
|
String extensionUrl = "http://my.own.domain/StructureDefinition/";
|
||||||
|
q.setStatus(PublicationStatus.ACTIVE)
|
||||||
|
.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionUrl + "questionnaire-itemControl")
|
||||||
|
.setValue(new Coding(null, "text-box", null));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.hasSize(1));
|
||||||
|
assertEquals(errors.getMessages().get(0).getSeverity(), ResultSeverityEnum.INFORMATION);
|
||||||
|
assertThat(errors.getMessages().get(0).getMessage(), Matchers.startsWith("Unknown extension " + extensionUrl));
|
||||||
|
|
||||||
|
myInstanceVal.setCustomExtensionDomains(Collections.singletonList(extensionUrl));
|
||||||
|
errors = myVal.validateWithResult(q);
|
||||||
|
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void afterClassClearContext() {
|
||||||
|
myDefaultValidationSupport.flush();
|
||||||
|
myDefaultValidationSupport = null;
|
||||||
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package org.hl7.fhir.r4.validation;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
import ca.uhn.fhir.validation.FhirValidator;
|
||||||
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
|
import ca.uhn.fhir.validation.ValidationResult;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport;
|
||||||
|
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
|
||||||
|
import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator;
|
||||||
|
import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain;
|
||||||
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
||||||
|
import org.hl7.fhir.r4.model.Narrative;
|
||||||
|
import org.hl7.fhir.r4.model.Narrative.NarrativeStatus;
|
||||||
|
import org.hl7.fhir.r4.model.Questionnaire;
|
||||||
|
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class QuestionnaireValidatorR4Test {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(QuestionnaireValidatorR4Test.class);
|
||||||
|
private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport();
|
||||||
|
private static FhirContext ourCtx = FhirContext.forR4();
|
||||||
|
private FhirInstanceValidator myInstanceVal;
|
||||||
|
private FhirValidator myVal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
IValidationSupport myValSupport = mock(IValidationSupport.class);
|
||||||
|
|
||||||
|
myVal = ourCtx.newValidator();
|
||||||
|
myVal.setValidateAgainstStandardSchema(false);
|
||||||
|
myVal.setValidateAgainstStandardSchematron(false);
|
||||||
|
|
||||||
|
ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport);
|
||||||
|
myInstanceVal = new FhirInstanceValidator(validationSupport);
|
||||||
|
|
||||||
|
myVal.registerValidatorModule(myInstanceVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithPredefinedExtensionDomains() {
|
||||||
|
String[] extensionDomainsToTest = new String[] {
|
||||||
|
"http://example.org/questionnaire-color-control-1",
|
||||||
|
"https://example.org/questionnaire-color-control-2",
|
||||||
|
"http://acme.com/questionnaire-color-control-3",
|
||||||
|
"https://acme.com/questionnaire-color-control-4",
|
||||||
|
"http://nema.org/questionnaire-color-control-5",
|
||||||
|
"https://nema.org/questionnaire-color-control-6",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/questionnaire-scoreItem",
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/structuredefinition-expression",
|
||||||
|
|
||||||
|
};
|
||||||
|
for (String extensionDomainToTest : extensionDomainsToTest) {
|
||||||
|
Questionnaire q = minimalValidQuestionnaire();
|
||||||
|
q.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionDomainToTest)
|
||||||
|
.setValue(new Coding(null, "text-box", null));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithPredefinedExtensionDomainsForCodeableConcept() {
|
||||||
|
String[] extensionDomainsToTest = new String[] {
|
||||||
|
"http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
|
||||||
|
};
|
||||||
|
for (String extensionDomainToTest : extensionDomainsToTest) {
|
||||||
|
Questionnaire q = minimalValidQuestionnaire();
|
||||||
|
q.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionDomainToTest)
|
||||||
|
.setValue(new CodeableConcept().addCoding(new Coding(null, "text-box", null)));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuestionnaireWithCustomExtensionDomain() {
|
||||||
|
String extensionUrl = "http://my.own.domain/StructureDefinition/";
|
||||||
|
Questionnaire q = minimalValidQuestionnaire();
|
||||||
|
q.addItem()
|
||||||
|
.setLinkId("link0")
|
||||||
|
.setType(QuestionnaireItemType.STRING)
|
||||||
|
.addExtension()
|
||||||
|
.setUrl(extensionUrl + "questionnaire-itemControl")
|
||||||
|
.setValue(new Coding(null, "text-box", null));
|
||||||
|
|
||||||
|
ValidationResult errors = myVal.validateWithResult(q);
|
||||||
|
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.hasSize(1));
|
||||||
|
assertEquals(errors.getMessages().get(0).getSeverity(), ResultSeverityEnum.INFORMATION);
|
||||||
|
assertThat(errors.getMessages().get(0).getMessage(), Matchers.startsWith("Unknown extension " + extensionUrl));
|
||||||
|
|
||||||
|
myInstanceVal.setCustomExtensionDomains(extensionUrl);
|
||||||
|
errors = myVal.validateWithResult(q);
|
||||||
|
|
||||||
|
ourLog.info(errors.toString());
|
||||||
|
assertThat(errors.isSuccessful(), Matchers.is(true));
|
||||||
|
assertThat(errors.getMessages(), Matchers.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Questionnaire minimalValidQuestionnaire() {
|
||||||
|
Narrative n = new Narrative().setStatus(NarrativeStatus.GENERATED);
|
||||||
|
n.setDivAsString("simple example");
|
||||||
|
Questionnaire q = new Questionnaire();
|
||||||
|
q.setText(n);
|
||||||
|
q.setName("SomeName");
|
||||||
|
q.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void afterClassClearContext() {
|
||||||
|
myDefaultValidationSupport.flush();
|
||||||
|
myDefaultValidationSupport = null;
|
||||||
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package ca.uhn.fhir.tinder;
|
||||||
|
|
||||||
|
import org.apache.maven.plugin.AbstractMojo;
|
||||||
|
import org.apache.maven.plugin.MojoExecutionException;
|
||||||
|
import org.apache.maven.plugin.MojoFailureException;
|
||||||
|
import org.apache.maven.plugins.annotations.Component;
|
||||||
|
import org.apache.maven.plugins.annotations.Parameter;
|
||||||
|
import org.apache.maven.project.MavenProject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for mojo generatorss.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractGeneratorMojo extends AbstractMojo {
|
||||||
|
|
||||||
|
protected final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
@Parameter(required = true, defaultValue = "${project.build.directory}/..")
|
||||||
|
protected String baseDir;
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
protected String packageBase = "";
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
protected List<String> baseResourceNames;
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
protected List<String> excludeResourceNames;
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
protected String templateName;
|
||||||
|
|
||||||
|
@Parameter(required = true)
|
||||||
|
protected String version;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
protected MavenProject myProject;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void execute() throws MojoExecutionException, MojoFailureException {
|
||||||
|
doExecute(new Configuration(this.version, baseDir, getTargetDirectory(), this.packageBase, this.baseResourceNames, this.excludeResourceNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void doExecute(Configuration mavenGeneratorConfiguration) throws MojoExecutionException, MojoFailureException;
|
||||||
|
|
||||||
|
protected abstract File getTargetDirectory();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package ca.uhn.fhir.tinder;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser;
|
||||||
|
import org.apache.commons.lang.WordUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public class Configuration {
|
||||||
|
|
||||||
|
private final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Configuration.class);
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
private File targetDirectory;
|
||||||
|
private String packageSuffix;
|
||||||
|
|
||||||
|
private String packageBase;
|
||||||
|
private FhirContext fhirContext;
|
||||||
|
private File packageDirectoryBase;
|
||||||
|
|
||||||
|
private final List<String> resourceNames = new ArrayList<>();
|
||||||
|
private String baseDir;
|
||||||
|
|
||||||
|
public Configuration(String version, String baseDir, File targetDirectory, String packageBase, List<String> baseResourceNames, List<String> excludeResourceNames) {
|
||||||
|
this.targetDirectory = targetDirectory;
|
||||||
|
this.packageBase = packageBase;
|
||||||
|
this.packageDirectoryBase = new File(targetDirectory, packageBase.replace(".", File.separatorChar + ""));
|
||||||
|
|
||||||
|
switch (version) {
|
||||||
|
case "dstu2":
|
||||||
|
fhirContext = FhirContext.forDstu2();
|
||||||
|
break;
|
||||||
|
case "dstu3":
|
||||||
|
fhirContext = FhirContext.forDstu3();
|
||||||
|
packageSuffix = ".dstu3";
|
||||||
|
break;
|
||||||
|
case "r4":
|
||||||
|
fhirContext = FhirContext.forR4();
|
||||||
|
packageSuffix = ".r4";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown version configured: " + version);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.version = version;
|
||||||
|
if (baseResourceNames == null || baseResourceNames.isEmpty()) {
|
||||||
|
ourLog.info("No resource names supplied, going to use all resources from version: {}", fhirContext.getVersion().getVersion());
|
||||||
|
|
||||||
|
Properties p = new Properties();
|
||||||
|
try {
|
||||||
|
p.load(fhirContext.getVersion().getFhirVersionPropertiesFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to load version property file", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ourLog.debug("Property file contains: {}", p);
|
||||||
|
|
||||||
|
TreeSet<String> keys = new TreeSet<String>();
|
||||||
|
for (Object next : p.keySet()) {
|
||||||
|
keys.add((String) next);
|
||||||
|
}
|
||||||
|
for (String next : keys) {
|
||||||
|
if (next.startsWith("resource.")) {
|
||||||
|
resourceNames.add(next.substring("resource.".length()).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) {
|
||||||
|
resourceNames.remove("conformance");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (String resourceName : baseResourceNames) {
|
||||||
|
resourceNames.add(resourceName.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeResourceNames != null) {
|
||||||
|
for (String resourceName : excludeResourceNames) {
|
||||||
|
resourceNames.remove(resourceName.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ourLog.info("Including the following resources: {}", resourceNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getPackageDirectoryBase() {
|
||||||
|
return packageDirectoryBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPackageSuffix() {
|
||||||
|
return packageSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getResourceNames() {
|
||||||
|
return resourceNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPackageBase() {
|
||||||
|
return packageBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourcePackage() {
|
||||||
|
if (BaseStructureSpreadsheetParser.determineVersionEnum(version).isRi()) {
|
||||||
|
return "org.hl7.fhir." + version + ".model";
|
||||||
|
}
|
||||||
|
return "ca.uhn.fhir.model." + version + ".resource";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersionCapitalized() {
|
||||||
|
String capitalize = WordUtils.capitalize(version);
|
||||||
|
if ("Dstu".equals(capitalize)) {
|
||||||
|
return "Dstu1";
|
||||||
|
}
|
||||||
|
return capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getTargetDirectory() {
|
||||||
|
return targetDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBaseDir() {
|
||||||
|
return baseDir;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package ca.uhn.fhir.tinder;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingModel;
|
||||||
|
import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingSpreadsheet;
|
||||||
|
import org.apache.maven.model.Resource;
|
||||||
|
import org.apache.maven.plugin.MojoExecutionException;
|
||||||
|
import org.apache.maven.plugin.MojoFailureException;
|
||||||
|
import org.apache.maven.plugins.annotations.LifecyclePhase;
|
||||||
|
import org.apache.maven.plugins.annotations.Mojo;
|
||||||
|
import org.apache.maven.plugins.annotations.Parameter;
|
||||||
|
import org.apache.velocity.VelocityContext;
|
||||||
|
import org.apache.velocity.app.VelocityEngine;
|
||||||
|
import org.apache.velocity.tools.generic.EscapeTool;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
@Mojo(name = "generate-resource", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
|
||||||
|
public class TinderResourceGeneratorMojo extends AbstractGeneratorMojo {
|
||||||
|
|
||||||
|
@Parameter(required = true, defaultValue = "${project.build.directory}/generated-resources/tinder")
|
||||||
|
protected File targetDirectory;
|
||||||
|
|
||||||
|
@Parameter(required = true)
|
||||||
|
protected String fileName = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(Configuration configuration) throws MojoExecutionException, MojoFailureException {
|
||||||
|
File packageDirectoryBase = configuration.getPackageDirectoryBase();
|
||||||
|
packageDirectoryBase.mkdirs();
|
||||||
|
|
||||||
|
ResourceGeneratorUsingModel gen = new ResourceGeneratorUsingModel(configuration.getVersion(), configuration.getBaseDir());
|
||||||
|
gen.setBaseResourceNames(configuration.getResourceNames());
|
||||||
|
|
||||||
|
try {
|
||||||
|
gen.parse();
|
||||||
|
|
||||||
|
VelocityContext ctx = new VelocityContext();
|
||||||
|
ctx.put("resources", gen.getResources());
|
||||||
|
ctx.put("packageBase", configuration.getPackageBase());
|
||||||
|
ctx.put("version", configuration.getVersion());
|
||||||
|
ctx.put("package_suffix", configuration.getPackageSuffix());
|
||||||
|
ctx.put("esc", new EscapeTool());
|
||||||
|
|
||||||
|
ctx.put("resourcePackage", configuration.getResourcePackage());
|
||||||
|
ctx.put("versionCapitalized", configuration.getVersionCapitalized());
|
||||||
|
|
||||||
|
VelocityEngine v = new VelocityEngine();
|
||||||
|
v.setProperty("resource.loader", "cp");
|
||||||
|
v.setProperty("cp.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
|
||||||
|
v.setProperty("runtime.references.strict", Boolean.TRUE);
|
||||||
|
|
||||||
|
InputStream templateIs = ResourceGeneratorUsingSpreadsheet.class.getResourceAsStream(templateName);
|
||||||
|
InputStreamReader templateReader = new InputStreamReader(templateIs);
|
||||||
|
|
||||||
|
File file = new File(packageDirectoryBase, fileName);
|
||||||
|
OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(file, false), "UTF-8");
|
||||||
|
v.evaluate(ctx, w, "", templateReader);
|
||||||
|
w.close();
|
||||||
|
|
||||||
|
Resource resource = new Resource();
|
||||||
|
resource.setDirectory(packageDirectoryBase.getAbsolutePath());
|
||||||
|
//resource.setDirectory(targetDirectory.getAbsolutePath());
|
||||||
|
//resource.addInclude(packageBase);
|
||||||
|
myProject.addResource(resource);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MojoFailureException("Failed to generate resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getTargetDirectory() {
|
||||||
|
return targetDirectory;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package ca.uhn.fhir.tinder;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.apache.maven.plugin.*;
|
||||||
|
import org.apache.maven.plugins.annotations.*;
|
||||||
|
import org.apache.maven.plugins.annotations.Mojo;
|
||||||
|
import org.apache.maven.project.MavenProject;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.tinder.parser.*;
|
||||||
|
|
||||||
|
@Mojo(name = "generate-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
|
||||||
|
public class TinderSourcesGeneratorMojo extends AbstractGeneratorMojo {
|
||||||
|
|
||||||
|
@Parameter(required = true, defaultValue = "${project.build.directory}/generated-sources/tinder")
|
||||||
|
protected File targetDirectory;
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
private String filenameSuffix = "ResourceProvider";
|
||||||
|
|
||||||
|
@Parameter
|
||||||
|
private String filenamePrefix = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doExecute(Configuration configuration) throws MojoExecutionException, MojoFailureException {
|
||||||
|
File packageDirectoryBase = configuration.getPackageDirectoryBase();
|
||||||
|
packageDirectoryBase.mkdirs();
|
||||||
|
|
||||||
|
ResourceGeneratorUsingModel gen = new ResourceGeneratorUsingModel(configuration.getVersion(), configuration.getBaseDir());
|
||||||
|
gen.setBaseResourceNames(configuration.getResourceNames());
|
||||||
|
|
||||||
|
try {
|
||||||
|
gen.parse();
|
||||||
|
|
||||||
|
gen.setFilenameSuffix(filenameSuffix);
|
||||||
|
gen.setFilenamePrefix(filenamePrefix);
|
||||||
|
gen.setTemplate(templateName);
|
||||||
|
gen.writeAll(packageDirectoryBase, null, configuration.getPackageBase());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MojoFailureException("Failed to generate server", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
myProject.addCompileSourceRoot(configuration.getTargetDirectory().getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected File getTargetDirectory() {
|
||||||
|
return targetDirectory;
|
||||||
|
}
|
||||||
|
}
|
|
@ -711,7 +711,7 @@ public abstract class BaseStructureParser {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FhirVersionEnum determineVersionEnum(String version) throws MojoFailureException {
|
public static FhirVersionEnum determineVersionEnum(String version) {
|
||||||
FhirVersionEnum versionEnum;
|
FhirVersionEnum versionEnum;
|
||||||
if ("dstu2".equals(version)) {
|
if ("dstu2".equals(version)) {
|
||||||
versionEnum = FhirVersionEnum.DSTU2;
|
versionEnum = FhirVersionEnum.DSTU2;
|
||||||
|
@ -720,7 +720,7 @@ public abstract class BaseStructureParser {
|
||||||
} else if ("r4".equals(version)) {
|
} else if ("r4".equals(version)) {
|
||||||
versionEnum = FhirVersionEnum.R4;
|
versionEnum = FhirVersionEnum.R4;
|
||||||
} else {
|
} else {
|
||||||
throw new MojoFailureException("Unknown version: " + version);
|
throw new IllegalArgumentException("Unknown version: " + version);
|
||||||
}
|
}
|
||||||
return versionEnum;
|
return versionEnum;
|
||||||
}
|
}
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -518,7 +518,7 @@
|
||||||
<jaxb_api_version>2.3.0</jaxb_api_version>
|
<jaxb_api_version>2.3.0</jaxb_api_version>
|
||||||
<jaxb_core_version>2.3.0</jaxb_core_version>
|
<jaxb_core_version>2.3.0</jaxb_core_version>
|
||||||
<jersey_version>2.25.1</jersey_version>
|
<jersey_version>2.25.1</jersey_version>
|
||||||
<jetty_version>9.4.12.v20180830</jetty_version>
|
<jetty_version>9.4.14.v20181114</jetty_version>
|
||||||
<jsr305_version>3.0.2</jsr305_version>
|
<jsr305_version>3.0.2</jsr305_version>
|
||||||
<!--<hibernate_version>5.2.10.Final</hibernate_version>-->
|
<!--<hibernate_version>5.2.10.Final</hibernate_version>-->
|
||||||
<hibernate_version>5.3.6.Final</hibernate_version>
|
<hibernate_version>5.3.6.Final</hibernate_version>
|
||||||
|
|
|
@ -98,9 +98,11 @@
|
||||||
or authorizing the contents of the response.
|
or authorizing the contents of the response.
|
||||||
</action>
|
</action>
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
JPA Migrator tool enhancements:
|
||||||
An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against
|
An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against
|
||||||
Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL
|
Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL
|
||||||
statements will be logged at the end of the run.
|
statements will be logged at the end of the run. Also, a case sensitivity issue when running against
|
||||||
|
some Postgres databases has been corrected.
|
||||||
</action>
|
</action>
|
||||||
<action type="add">
|
<action type="add">
|
||||||
In the JPA server, when performing a chained reference search on a search parameter with
|
In the JPA server, when performing a chained reference search on a search parameter with
|
||||||
|
@ -142,6 +144,18 @@
|
||||||
causes Media resources to be served as raw content if the client explicitly requests
|
causes Media resources to be served as raw content if the client explicitly requests
|
||||||
the correct content type cia the Accept header.
|
the correct content type cia the Accept header.
|
||||||
</action>
|
</action>
|
||||||
|
<action type="add" issue="917">
|
||||||
|
A new configuration item has been added to the FhirInstanceValidator that
|
||||||
|
allows you to specify additional "known extension domains", meaning
|
||||||
|
domains in which the validator will not complain about when it
|
||||||
|
encounters new extensions. Thanks to Heinz-Dieter Conradi for the
|
||||||
|
pull request!
|
||||||
|
</action>
|
||||||
|
<action type="fix">
|
||||||
|
Under some circumstances, when a custom search parameter was added to the JPA server
|
||||||
|
resources could start reindexing before the new search parameter had been saved, meaning that
|
||||||
|
it was not applied to all resources. This has been corrected.
|
||||||
|
</action>
|
||||||
</release>
|
</release>
|
||||||
<release version="3.6.0" date="2018-11-12" description="Food">
|
<release version="3.6.0" date="2018-11-12" description="Food">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
Loading…
Reference in New Issue