Add support for revincludes to CapabilityStatement generator (#2521)
* Add support for revincludes to CapabilotyStatement generator * Add changelog * Fixes * Test fix * Test fixes * Test fix * Test fix * More fixes to CS cache * Test fix * Test fix * Test fix
This commit is contained in:
parent
b27559caa4
commit
0bf746f23b
|
@ -101,10 +101,12 @@ public class FhirContext {
|
|||
private static final Map<FhirVersionEnum, FhirContext> ourStaticContexts = Collections.synchronizedMap(new EnumMap<>(FhirVersionEnum.class));
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
|
||||
private final IFhirVersion myVersion;
|
||||
private final Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<>();
|
||||
private final Set<PerformanceOptionsEnum> myPerformanceOptions = new HashSet<>();
|
||||
private final Collection<Class<? extends IBaseResource>> myResourceTypesToScan;
|
||||
private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM;
|
||||
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap();
|
||||
private ArrayList<Class<? extends IBase>> myCustomTypes;
|
||||
private final Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<>();
|
||||
private volatile Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap();
|
||||
private volatile boolean myInitialized;
|
||||
private volatile boolean myInitializing = false;
|
||||
|
@ -115,13 +117,14 @@ public class FhirContext {
|
|||
private volatile INarrativeGenerator myNarrativeGenerator;
|
||||
private volatile IParserErrorHandler myParserErrorHandler = new LenientErrorHandler();
|
||||
private ParserOptions myParserOptions = new ParserOptions();
|
||||
private final Set<PerformanceOptionsEnum> myPerformanceOptions = new HashSet<>();
|
||||
private final Collection<Class<? extends IBaseResource>> myResourceTypesToScan;
|
||||
private volatile IRestfulClientFactory myRestfulClientFactory;
|
||||
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
|
||||
private IValidationSupport myValidationSupport;
|
||||
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
|
||||
private volatile Set<String> myResourceNames;
|
||||
private volatile Boolean myFormatXmlSupported;
|
||||
private volatile Boolean myFormatJsonSupported;
|
||||
private volatile Boolean myFormatRdfSupported;
|
||||
|
||||
/**
|
||||
* @deprecated It is recommended that you use one of the static initializer methods instead
|
||||
|
@ -673,6 +676,51 @@ public class FhirContext {
|
|||
return !myDefaultTypeForProfile.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns <code>true</code> if the XML serialization format is supported, based on the
|
||||
* available libraries on the classpath.
|
||||
*
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isFormatXmlSupported() {
|
||||
Boolean retVal = myFormatXmlSupported;
|
||||
if (retVal == null) {
|
||||
retVal = tryToInitParser(() -> newXmlParser());
|
||||
myFormatXmlSupported = retVal;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns <code>true</code> if the JSON serialization format is supported, based on the
|
||||
* available libraries on the classpath.
|
||||
*
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isFormatJsonSupported() {
|
||||
Boolean retVal = myFormatJsonSupported;
|
||||
if (retVal == null) {
|
||||
retVal = tryToInitParser(() -> newJsonParser());
|
||||
myFormatJsonSupported = retVal;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns <code>true</code> if the RDF serialization format is supported, based on the
|
||||
* available libraries on the classpath.
|
||||
*
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isFormatRdfSupported() {
|
||||
Boolean retVal = myFormatRdfSupported;
|
||||
if (retVal == null) {
|
||||
retVal = tryToInitParser(() -> newRDFParser());
|
||||
myFormatRdfSupported = retVal;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public IVersionSpecificBundleFactory newBundleFactory() {
|
||||
return myVersion.newBundleFactory(this);
|
||||
}
|
||||
|
@ -735,7 +783,6 @@ public class FhirContext {
|
|||
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every message being processed
|
||||
* without incurring any performance penalty
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
public IParser newRDFParser() {
|
||||
return new RDFParser(this, myParserErrorHandler, Lang.TURTLE);
|
||||
|
@ -989,6 +1036,24 @@ public class FhirContext {
|
|||
return "FhirContext[" + myVersion.getVersion().name() + "]";
|
||||
}
|
||||
|
||||
// TODO KHS add the other primitive types
|
||||
public IPrimitiveType<Boolean> getPrimitiveBoolean(Boolean theValue) {
|
||||
IPrimitiveType<Boolean> retval = (IPrimitiveType<Boolean>) getElementDefinition("boolean").newInstance();
|
||||
retval.setValue(theValue);
|
||||
return retval;
|
||||
}
|
||||
|
||||
private static boolean tryToInitParser(Runnable run) {
|
||||
boolean retVal;
|
||||
try {
|
||||
run.run();
|
||||
retVal = true;
|
||||
} catch (Exception | NoClassDefFoundError e) {
|
||||
retVal = false;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2}
|
||||
*/
|
||||
|
@ -1066,11 +1131,4 @@ public class FhirContext {
|
|||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
// TODO KHS add the other primitive types
|
||||
public IPrimitiveType<Boolean> getPrimitiveBoolean(Boolean theValue) {
|
||||
IPrimitiveType<Boolean> retval = (IPrimitiveType<Boolean>) getElementDefinition("boolean").newInstance();
|
||||
retval.setValue(theValue);
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,16 +142,16 @@ public abstract class BaseInterceptorService<POINTCUT extends IPointcut> impleme
|
|||
@VisibleForTesting
|
||||
public void unregisterAllInterceptors() {
|
||||
synchronized (myRegistryMutex) {
|
||||
myAnonymousInvokers.clear();
|
||||
myGlobalInvokers.clear();
|
||||
myInterceptors.clear();
|
||||
unregisterInterceptors(myAnonymousInvokers.values());
|
||||
unregisterInterceptors(myGlobalInvokers.values());
|
||||
unregisterInterceptors(myInterceptors);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
|
||||
if (theInterceptors != null) {
|
||||
theInterceptors.forEach(t -> unregisterInterceptor(t));
|
||||
new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -125,4 +125,11 @@ public class CacheControlDirective {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience factory method for a no-cache directivel
|
||||
*/
|
||||
public static CacheControlDirective noCache() {
|
||||
return new CacheControlDirective().setNoCache(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,8 @@ public class Constants {
|
|||
public static final String FORMAT_HTML = "html";
|
||||
public static final String FORMAT_JSON = "json";
|
||||
public static final String FORMAT_XML = "xml";
|
||||
public static final String FORMAT_TURTLE = "text/turtle";
|
||||
public static final String CT_RDF_TURTLE_LEGACY = "text/turtle";
|
||||
public static final String FORMAT_TURTLE = "ttl";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -94,6 +95,7 @@ public class Constants {
|
|||
public static final Set<String> FORMATS_HTML;
|
||||
public static final String FORMATS_HTML_JSON = "html/json";
|
||||
public static final String FORMATS_HTML_XML = "html/xml";
|
||||
public static final String FORMATS_HTML_TTL = "html/turtle";
|
||||
public static final String HEADER_ACCEPT = "Accept";
|
||||
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
|
||||
public static final String HEADER_ACCEPT_VALUE_JSON_NON_LEGACY = CT_FHIR_JSON_NEW + ";q=1.0, " + CT_FHIR_JSON + ";q=0.9";
|
||||
|
|
|
@ -46,7 +46,7 @@ public enum EncodingEnum {
|
|||
}
|
||||
},
|
||||
|
||||
RDF(Constants.CT_RDF_TURTLE, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
|
||||
RDF(Constants.CT_RDF_TURTLE_LEGACY, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
|
||||
@Override
|
||||
public IParser newParser(FhirContext theContext) {
|
||||
return theContext.newRDFParser();
|
||||
|
@ -114,6 +114,7 @@ public enum EncodingEnum {
|
|||
ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
|
||||
ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
|
||||
ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
|
||||
ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF);
|
||||
|
||||
ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy);
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2521
|
||||
title: "The automatically generated CapabilityStatement for R4+ will now incude the list of supported
|
||||
revinclude values."
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2521
|
||||
title: "The server generated CapabilityStatement now reflects whether RDF/Turtle is supported by
|
||||
the server. In addition, the ResponseHighlightingInterceptor will now provide some TTL support."
|
|
@ -67,10 +67,10 @@ public abstract class BaseBulkItemReader implements ItemReader<List<ResourcePers
|
|||
@Autowired
|
||||
protected FhirContext myContext;
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
@Autowired
|
||||
protected SearchBuilderFactory mySearchBuilderFactory;
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
@Autowired
|
||||
private MatchUrlService myMatchUrlService;
|
||||
|
||||
private ISearchBuilder mySearchBuilder;
|
||||
|
|
|
@ -22,6 +22,7 @@ import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
|||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
|
@ -34,8 +35,8 @@ import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
|
|||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.context.ContextLoader;
|
||||
|
@ -52,7 +53,6 @@ import java.util.List;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
||||
|
||||
|
@ -64,6 +64,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
|||
protected static SearchParamRegistryImpl ourSearchParamRegistry;
|
||||
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
|
||||
protected static Server ourServer;
|
||||
protected static JpaCapabilityStatementProvider ourCapabilityStatementProvider;
|
||||
private static DatabaseBackedPagingProvider ourPagingProvider;
|
||||
private static GenericWebApplicationContext ourWebApplicationContext;
|
||||
protected IGenericClient myClient;
|
||||
|
@ -153,9 +154,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
|||
ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class);
|
||||
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
|
||||
|
||||
JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
|
||||
confProvider.setImplementationDescription("THIS IS THE DESC");
|
||||
ourRestServer.setServerConformanceProvider(confProvider);
|
||||
ourCapabilityStatementProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
|
||||
ourCapabilityStatementProvider.setImplementationDescription("THIS IS THE DESC");
|
||||
ourRestServer.setServerConformanceProvider(ourCapabilityStatementProvider);
|
||||
|
||||
server.setHandler(proxyHandler);
|
||||
JettyUtil.startServer(server);
|
||||
|
@ -179,6 +180,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
ourRestServer.setPagingProvider(ourPagingProvider);
|
||||
ourRestServer.registerInterceptor(new ResponseHighlighterInterceptor());
|
||||
|
||||
myClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
|
||||
if (shouldLogClient()) {
|
||||
|
@ -186,13 +188,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
protected static void clearRestfulServer() throws Exception {
|
||||
if (ourServer != null) {
|
||||
JettyUtil.closeServer(ourServer);
|
||||
}
|
||||
ourServer = null;
|
||||
}
|
||||
|
||||
protected boolean shouldLogClient() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
|
||||
import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider;
|
||||
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.hl7.fhir.r4.model.StructureDefinition;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
@ -15,8 +20,11 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test {
|
||||
|
@ -32,7 +40,7 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testCustomSearchParamsReflected() {
|
||||
public void testCustomSearchParamsReflectedInSearchParams() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
|
@ -57,6 +65,101 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
@AfterEach
|
||||
public void after() throws Exception {
|
||||
super.after();
|
||||
ourCapabilityStatementProvider.setRestResourceRevIncludesEnabled(ServerCapabilityStatementProvider.DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFormats() {
|
||||
CapabilityStatement cs = myClient
|
||||
.capabilities()
|
||||
.ofType(CapabilityStatement.class)
|
||||
.cacheControl(CacheControlDirective.noCache())
|
||||
.execute();
|
||||
List<String> formats = cs
|
||||
.getFormat()
|
||||
.stream()
|
||||
.map(t -> t.getCode())
|
||||
.collect(Collectors.toList());
|
||||
assertThat(formats.toString(), formats, containsInAnyOrder(
|
||||
"application/fhir+xml",
|
||||
"application/fhir+json",
|
||||
"json",
|
||||
"xml",
|
||||
"html/xml",
|
||||
"html/json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomSearchParamsReflectedInIncludesAndRevIncludes_TargetSpecified() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Observation");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setUrl("http://acme.com/foo");
|
||||
fooSp.setType(Enumerations.SearchParamType.REFERENCE);
|
||||
fooSp.setTitle("FOO SP");
|
||||
fooSp.setDescription("This is a search param!");
|
||||
fooSp.setExpression("Observation.subject");
|
||||
fooSp.addTarget("Patient");
|
||||
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
|
||||
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
|
||||
mySearchParameterDao.create(fooSp);
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
ourCapabilityStatementProvider.setRestResourceRevIncludesEnabled(true);
|
||||
|
||||
CapabilityStatement cs = myClient
|
||||
.capabilities()
|
||||
.ofType(CapabilityStatement.class)
|
||||
.cacheControl(CacheControlDirective.noCache())
|
||||
.execute();
|
||||
|
||||
List<String> includes = findIncludes(cs, "Patient");
|
||||
assertThat(includes.toString(), includes, contains("*", "Patient:general-practitioner", "Patient:link", "Patient:organization"));
|
||||
|
||||
includes = findIncludes(cs, "Observation");
|
||||
assertThat(includes.toString(), includes, contains("*", "Observation:based-on", "Observation:derived-from", "Observation:device", "Observation:encounter", "Observation:focus", "Observation:foo", "Observation:has-member", "Observation:part-of", "Observation:patient", "Observation:performer", "Observation:specimen", "Observation:subject"));
|
||||
|
||||
List<String> revIncludes = findRevIncludes(cs, "Patient");
|
||||
assertThat(revIncludes.toString(), revIncludes, hasItems(
|
||||
"Account:patient", // Standard SP reference
|
||||
"Observation:foo", // Standard SP reference with no explicit target
|
||||
"Provenance:entity" // Reference in custom SP
|
||||
));
|
||||
assertThat(revIncludes.toString(), revIncludes, not(hasItem(
|
||||
"CarePlan:based-on" // Standard SP reference with non-matching target
|
||||
)));
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<String> findIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) {
|
||||
return theCapabilityStatement
|
||||
.getRest()
|
||||
.stream()
|
||||
.flatMap(t -> t.getResource().stream())
|
||||
.filter(t -> t.getType().equals(theResourceName))
|
||||
.flatMap(t -> t.getSearchInclude().stream())
|
||||
.map(t -> t.getValue())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<String> findRevIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) {
|
||||
return theCapabilityStatement
|
||||
.getRest()
|
||||
.stream()
|
||||
.flatMap(t -> t.getResource().stream())
|
||||
.filter(t -> t.getType().equals(theResourceName))
|
||||
.flatMap(t -> t.getSearchRevInclude().stream())
|
||||
.map(t -> t.getValue())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisteredProfilesReflected_StoredInServer() throws IOException {
|
||||
StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json");
|
||||
|
|
|
@ -46,6 +46,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.ArrayList;
|
||||
|
@ -94,6 +95,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
||||
requiresActiveSearchParams();
|
||||
|
@ -122,7 +124,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
||||
|
||||
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
||||
int size = allSearchParamsBp.size();
|
||||
int size = allSearchParamsBp.sizeOrThrowNpe();
|
||||
|
||||
ourLog.trace("Loaded {} search params from the DB", size);
|
||||
|
||||
|
|
|
@ -322,14 +322,32 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
|
|||
invokeDestroy(iResourceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
if (myServerConformanceProvider != null) {
|
||||
invokeDestroy(myServerConformanceProvider);
|
||||
}
|
||||
|
||||
if (getPlainProviders() != null) {
|
||||
for (Object next : getPlainProviders()) {
|
||||
invokeDestroy(next);
|
||||
}
|
||||
}
|
||||
|
||||
if (myServerConformanceMethod != null) {
|
||||
myServerConformanceMethod.close();
|
||||
}
|
||||
myResourceNameToBinding
|
||||
.values()
|
||||
.stream()
|
||||
.flatMap(t->t.getMethodBindings().stream())
|
||||
.forEach(t->t.close());
|
||||
myGlobalBinding
|
||||
.getMethodBindings()
|
||||
.forEach(t->t.close());
|
||||
myServerBinding
|
||||
.getMethodBindings()
|
||||
.forEach(t->t.close());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,12 +31,14 @@ import ca.uhn.fhir.rest.server.method.SearchParameter;
|
|||
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
|
||||
import ca.uhn.fhir.util.VersionUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -278,7 +280,7 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
|
|||
}
|
||||
|
||||
public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
|
||||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
|
||||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<>();
|
||||
for (ResourceBinding next : getResourceBindings()) {
|
||||
String resourceName = next.getResourceName();
|
||||
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
|
||||
|
@ -365,14 +367,15 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(@Nonnull String theResourceName) {
|
||||
Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
|
||||
|
||||
Map<String, RuntimeSearchParam> retVal = new LinkedHashMap<>();
|
||||
|
||||
collectMethodBindings()
|
||||
.getOrDefault(theResourceName, Collections.emptyList())
|
||||
.stream()
|
||||
.filter(t -> t.getResourceName().equals(theResourceName))
|
||||
.filter(t -> theResourceName.equals(t.getResourceName()))
|
||||
.filter(t -> t instanceof SearchMethodBinding)
|
||||
.map(t -> (SearchMethodBinding) t)
|
||||
.filter(t -> t.getQueryName() == null)
|
||||
|
|
|
@ -18,23 +18,35 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.*;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.trim;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
|
@ -71,10 +83,10 @@ public class ResponseHighlighterInterceptor {
|
|||
*/
|
||||
public static final String PARAM_RAW = "_raw";
|
||||
public static final String PARAM_RAW_TRUE = "true";
|
||||
public static final String PARAM_TRUE = "true";
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class);
|
||||
private static final String[] PARAM_FORMAT_VALUE_JSON = new String[]{Constants.FORMAT_JSON};
|
||||
private static final String[] PARAM_FORMAT_VALUE_XML = new String[]{Constants.FORMAT_XML};
|
||||
private static final String[] PARAM_FORMAT_VALUE_TTL = new String[]{Constants.FORMAT_TURTLE};
|
||||
private boolean myShowRequestHeaders = false;
|
||||
private boolean myShowResponseHeaders = true;
|
||||
|
||||
|
@ -128,6 +140,9 @@ public class ResponseHighlighterInterceptor {
|
|||
boolean inValue = false;
|
||||
boolean inQuote = false;
|
||||
boolean inTag = false;
|
||||
boolean inTurtleDirective = false;
|
||||
boolean startingLineNext = true;
|
||||
boolean startingLine = false;
|
||||
int lineCount = 1;
|
||||
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
|
@ -140,13 +155,23 @@ public class ResponseHighlighterInterceptor {
|
|||
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
|
||||
|
||||
if (nextChar == '\n') {
|
||||
if (inTurtleDirective) {
|
||||
theTarget.append("</span>");
|
||||
inTurtleDirective = false;
|
||||
}
|
||||
lineCount++;
|
||||
theTarget.append("</div><div id=\"line");
|
||||
theTarget.append(lineCount);
|
||||
theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
|
||||
theTarget.append(lineCount);
|
||||
theTarget.append("');\">");
|
||||
startingLineNext = true;
|
||||
continue;
|
||||
} else if (startingLineNext) {
|
||||
startingLineNext = false;
|
||||
startingLine = true;
|
||||
} else {
|
||||
startingLine = false;
|
||||
}
|
||||
|
||||
if (theEncodingEnum == EncodingEnum.JSON) {
|
||||
|
@ -194,7 +219,45 @@ public class ResponseHighlighterInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
} else if (theEncodingEnum == EncodingEnum.RDF) {
|
||||
|
||||
if (inQuote) {
|
||||
theTarget.append(nextChar);
|
||||
if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
|
||||
theTarget.append("quot;</span>");
|
||||
i += 5;
|
||||
inQuote = false;
|
||||
} else if (nextChar == '\\' && nextChar2 == '"') {
|
||||
theTarget.append("quot;</span>");
|
||||
i += 5;
|
||||
inQuote = false;
|
||||
}
|
||||
} else if (startingLine && nextChar == '@') {
|
||||
inTurtleDirective = true;
|
||||
theTarget.append("<span class='hlTagName'>");
|
||||
theTarget.append(nextChar);
|
||||
} else if (startingLine) {
|
||||
inTurtleDirective = true;
|
||||
theTarget.append("<span class='hlTagName'>");
|
||||
theTarget.append(nextChar);
|
||||
} else if (nextChar == '[' || nextChar == ']' || nextChar == ';' || nextChar == ':') {
|
||||
theTarget.append("<span class='hlControl'>");
|
||||
theTarget.append(nextChar);
|
||||
theTarget.append("</span>");
|
||||
} else {
|
||||
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
|
||||
theTarget.append("<span class='hlQuot'>"");
|
||||
inQuote = true;
|
||||
i += 5;
|
||||
} else {
|
||||
theTarget.append(nextChar);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Ok it's XML
|
||||
|
||||
if (inQuote) {
|
||||
theTarget.append(nextChar);
|
||||
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
|
||||
|
@ -345,6 +408,26 @@ public class ResponseHighlighterInterceptor {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
|
||||
public void capabilityStatementGenerated(RequestDetails theRequestDetails, IBaseConformance theCapabilityStatement) {
|
||||
FhirTerser terser = theRequestDetails.getFhirContext().newTerser();
|
||||
|
||||
Set<String> formats = terser.getValues(theCapabilityStatement, "format", IPrimitiveType.class)
|
||||
.stream()
|
||||
.map(t -> t.getValueAsString())
|
||||
.collect(Collectors.toSet());
|
||||
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_JSON_NEW, Constants.FORMATS_HTML_JSON);
|
||||
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_XML_NEW, Constants.FORMATS_HTML_XML);
|
||||
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_RDF_TURTLE, Constants.FORMATS_HTML_TTL);
|
||||
}
|
||||
|
||||
private void addFormatConditionally(IBaseConformance theCapabilityStatement, FhirTerser terser, Set<String> formats, String wanted, String toAdd) {
|
||||
if (formats.contains(wanted)) {
|
||||
terser.addElement(theCapabilityStatement, "format", toAdd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) {
|
||||
/*
|
||||
* Request for _raw
|
||||
|
@ -373,6 +456,9 @@ public class ResponseHighlighterInterceptor {
|
|||
} else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) {
|
||||
force = true;
|
||||
theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON);
|
||||
} else if (Constants.FORMATS_HTML_TTL.equals(formatParam)) {
|
||||
force = true;
|
||||
theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_TTL);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -542,8 +628,6 @@ public class ResponseHighlighterInterceptor {
|
|||
outputBuffer.append(" position: relative;\n");
|
||||
outputBuffer.append("}");
|
||||
outputBuffer.append(".responseBodyTableFirstColumn {");
|
||||
// outputBuffer.append(" position: absolute;\n");
|
||||
// outputBuffer.append(" width: 70px;\n");
|
||||
outputBuffer.append("}");
|
||||
outputBuffer.append(".responseBodyTableSecondColumn {");
|
||||
outputBuffer.append(" position: absolute;\n");
|
||||
|
@ -589,24 +673,47 @@ public class ResponseHighlighterInterceptor {
|
|||
outputBuffer.append("This result is being rendered in HTML for easy viewing. ");
|
||||
outputBuffer.append("You may access this content as ");
|
||||
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
|
||||
outputBuffer.append("\">Raw JSON</a> or ");
|
||||
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
|
||||
outputBuffer.append("\">Raw JSON</a> or ");
|
||||
}
|
||||
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
|
||||
outputBuffer.append("\">Raw XML</a>, ");
|
||||
if (theRequestDetails.getFhirContext().isFormatXmlSupported()) {
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
|
||||
outputBuffer.append("\">Raw XML</a> or ");
|
||||
}
|
||||
|
||||
outputBuffer.append(" or view this content in ");
|
||||
if (theRequestDetails.getFhirContext().isFormatRdfSupported()) {
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_TURTLE));
|
||||
outputBuffer.append("\">Raw Turtle</a> or ");
|
||||
}
|
||||
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
|
||||
outputBuffer.append("\">HTML JSON</a> ");
|
||||
outputBuffer.append("view this content in ");
|
||||
|
||||
outputBuffer.append("or ");
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
|
||||
outputBuffer.append("\">HTML XML</a>.");
|
||||
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
|
||||
outputBuffer.append("\">HTML JSON</a> ");
|
||||
}
|
||||
|
||||
if (theRequestDetails.getFhirContext().isFormatXmlSupported()) {
|
||||
outputBuffer.append("or ");
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
|
||||
outputBuffer.append("\">HTML XML</a> ");
|
||||
}
|
||||
|
||||
if (theRequestDetails.getFhirContext().isFormatRdfSupported()) {
|
||||
outputBuffer.append("or ");
|
||||
outputBuffer.append("<a href=\"");
|
||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_TTL));
|
||||
outputBuffer.append("\">HTML Turtle</a> ");
|
||||
}
|
||||
|
||||
outputBuffer.append(".");
|
||||
}
|
||||
|
||||
Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
|
||||
|
@ -680,7 +787,7 @@ public class ResponseHighlighterInterceptor {
|
|||
outputBuffer.append("\n");
|
||||
|
||||
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
|
||||
String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
|
||||
String jsStr = jsStream != null ? IOUtils.toString(jsStream, StandardCharsets.UTF_8) : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
|
||||
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
|
||||
outputBuffer.append("<script type=\"text/javascript\">");
|
||||
outputBuffer.append(jsStr);
|
||||
|
|
|
@ -138,10 +138,21 @@ public abstract class BaseMethodBinding<T> {
|
|||
}
|
||||
|
||||
public Set<String> getIncludes() {
|
||||
Set<String> retVal = new TreeSet<String>();
|
||||
return doGetIncludesOrRevIncludes(false);
|
||||
}
|
||||
|
||||
public Set<String> getRevIncludes() {
|
||||
return doGetIncludesOrRevIncludes(true);
|
||||
}
|
||||
|
||||
private Set<String> doGetIncludesOrRevIncludes(boolean reverse) {
|
||||
Set<String> retVal = new TreeSet<>();
|
||||
for (IParameter next : myParameters) {
|
||||
if (next instanceof IncludeParameter) {
|
||||
retVal.addAll(((IncludeParameter) next).getAllow());
|
||||
IncludeParameter includeParameter = (IncludeParameter) next;
|
||||
if (includeParameter.isReverse() == reverse) {
|
||||
retVal.addAll(includeParameter.getAllow());
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
|
@ -313,6 +324,10 @@ public abstract class BaseMethodBinding<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// subclasses may override
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
|
||||
Read read = theMethod.getAnnotation(Read.class);
|
||||
|
|
|
@ -42,10 +42,16 @@ import org.hl7.fhir.instance.model.api.IBaseConformance;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
|
||||
public static final String CACHE_THREAD_PREFIX = "capabilitystatement-cache-";
|
||||
/*
|
||||
* Note: This caching mechanism should probably be configurable and maybe
|
||||
* even applicable to other bindings. It's particularly important for this
|
||||
|
@ -53,6 +59,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
|||
*/
|
||||
private final AtomicReference<IBaseConformance> myCachedResponse = new AtomicReference<>();
|
||||
private final AtomicLong myCachedResponseExpires = new AtomicLong(0L);
|
||||
private final ExecutorService myThreadPool;
|
||||
private long myCacheMillis = 60 * 1000;
|
||||
|
||||
ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
|
||||
|
@ -69,6 +76,17 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
|||
setCacheMillis(metadata.cacheMillis());
|
||||
}
|
||||
|
||||
ThreadFactory threadFactory = r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName(CACHE_THREAD_PREFIX + t.getId());
|
||||
t.setDaemon(false);
|
||||
return t;
|
||||
};
|
||||
myThreadPool = new ThreadPoolExecutor(1, 1,
|
||||
0L, TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(1),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,6 +118,13 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
|||
return ReturnTypeEnum.RESOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
|
||||
myThreadPool.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
|
||||
IBaseConformance conf;
|
||||
|
@ -116,7 +141,8 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
|||
if (conf != null) {
|
||||
long expires = myCachedResponseExpires.get();
|
||||
if (expires < System.currentTimeMillis()) {
|
||||
conf = null;
|
||||
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
|
||||
myThreadPool.submit(() -> createCapabilityStatement(theRequest, theMethodParams));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,32 +167,36 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
|||
}
|
||||
|
||||
if (conf == null) {
|
||||
conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams);
|
||||
if (myCacheMillis > 0) {
|
||||
|
||||
// Interceptor hook: SERVER_CAPABILITY_STATEMENT_GENERATED
|
||||
if (theRequest.getInterceptorBroadcaster() != null) {
|
||||
HookParams params = new HookParams();
|
||||
params.add(IBaseConformance.class, conf);
|
||||
params.add(RequestDetails.class, theRequest);
|
||||
params.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
IBaseConformance outcome = (IBaseConformance) theRequest
|
||||
.getInterceptorBroadcaster()
|
||||
.callHooksAndReturnObject(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED, params);
|
||||
if (outcome != null) {
|
||||
conf = outcome;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
myCachedResponse.set(conf);
|
||||
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
|
||||
}
|
||||
conf = createCapabilityStatement(theRequest, theMethodParams);
|
||||
}
|
||||
|
||||
return new SimpleBundleProvider(conf);
|
||||
}
|
||||
|
||||
private IBaseConformance createCapabilityStatement(RequestDetails theRequest, Object[] theMethodParams) {
|
||||
IBaseConformance conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams);
|
||||
|
||||
// Interceptor hook: SERVER_CAPABILITY_STATEMENT_GENERATED
|
||||
if (theRequest.getInterceptorBroadcaster() != null) {
|
||||
HookParams params = new HookParams();
|
||||
params.add(IBaseConformance.class, conf);
|
||||
params.add(RequestDetails.class, theRequest);
|
||||
params.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
IBaseConformance outcome = (IBaseConformance) theRequest
|
||||
.getInterceptorBroadcaster()
|
||||
.callHooksAndReturnObject(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED, params);
|
||||
if (outcome != null) {
|
||||
conf = outcome;
|
||||
}
|
||||
}
|
||||
|
||||
if (myCacheMillis > 0) {
|
||||
myCachedResponse.set(conf);
|
||||
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
|
||||
if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) {
|
||||
|
|
|
@ -67,6 +67,10 @@ class IncludeParameter extends BaseQueryParameter {
|
|||
|
||||
}
|
||||
|
||||
public boolean isReverse() {
|
||||
return myReverse;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
|
||||
|
|
|
@ -4,7 +4,6 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
import ca.uhn.fhir.model.primitive.InstantDt;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
|
@ -45,8 +44,10 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
|
@ -81,6 +82,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
*/
|
||||
public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> {
|
||||
|
||||
public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true;
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
|
||||
private final FhirContext myContext;
|
||||
private final RestfulServer myServer;
|
||||
|
@ -88,6 +90,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
private final RestfulServerConfiguration myServerConfiguration;
|
||||
private final IValidationSupport myValidationSupport;
|
||||
private String myPublisher = "Not provided";
|
||||
private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -189,6 +192,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
|
||||
TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
|
||||
|
||||
terser.addElement(retVal, "id", UUID.randomUUID().toString());
|
||||
terser.addElement(retVal, "name", "RestServer");
|
||||
terser.addElement(retVal, "publisher", myPublisher);
|
||||
terser.addElement(retVal, "date", conformanceDate(configuration));
|
||||
|
@ -201,10 +205,18 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
terser.addElement(retVal, "kind", "instance");
|
||||
terser.addElement(retVal, "software.name", configuration.getServerName());
|
||||
terser.addElement(retVal, "software.version", configuration.getServerVersion());
|
||||
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
|
||||
terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
|
||||
terser.addElement(retVal, "format", Constants.FORMAT_JSON);
|
||||
terser.addElement(retVal, "format", Constants.FORMAT_XML);
|
||||
if (myContext.isFormatXmlSupported()) {
|
||||
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
|
||||
terser.addElement(retVal, "format", Constants.FORMAT_XML);
|
||||
}
|
||||
if (myContext.isFormatJsonSupported()) {
|
||||
terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
|
||||
terser.addElement(retVal, "format", Constants.FORMAT_JSON);
|
||||
}
|
||||
if (myContext.isFormatRdfSupported()) {
|
||||
terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE);
|
||||
terser.addElement(retVal, "format", Constants.FORMAT_TURTLE);
|
||||
}
|
||||
terser.addElement(retVal, "status", "active");
|
||||
|
||||
IBase rest = terser.addElement(retVal, "rest");
|
||||
|
@ -216,13 +228,25 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
|
||||
Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype();
|
||||
|
||||
TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create();
|
||||
TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create();
|
||||
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
||||
String resourceName = nextEntry.getKey();
|
||||
for (BaseMethodBinding<?> nextMethod : nextEntry.getValue()) {
|
||||
if (nextMethod instanceof SearchMethodBinding) {
|
||||
resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes());
|
||||
resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
||||
|
||||
String resourceName = nextEntry.getKey();
|
||||
if (nextEntry.getKey().isEmpty() == false) {
|
||||
Set<String> resourceOps = new HashSet<>();
|
||||
Set<String> resourceIncludes = new HashSet<>();
|
||||
IBase resource = terser.addElement(rest, "resource");
|
||||
String resourceName = nextEntry.getKey();
|
||||
|
||||
postProcessRestResource(terser, resource, resourceName);
|
||||
|
||||
|
@ -272,6 +296,29 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
case UPDATE:
|
||||
terser.setElement(resource, "conditionalUpdate", "true");
|
||||
break;
|
||||
case HISTORY_INSTANCE:
|
||||
case HISTORY_SYSTEM:
|
||||
case HISTORY_TYPE:
|
||||
case READ:
|
||||
case SEARCH_SYSTEM:
|
||||
case SEARCH_TYPE:
|
||||
case TRANSACTION:
|
||||
case VALIDATE:
|
||||
case VREAD:
|
||||
case METADATA:
|
||||
case META_ADD:
|
||||
case META:
|
||||
case META_DELETE:
|
||||
case PATCH:
|
||||
case BATCH:
|
||||
case ADD_TAGS:
|
||||
case DELETE_TAGS:
|
||||
case GET_TAGS:
|
||||
case GET_PAGE:
|
||||
case GRAPHQL_REQUEST:
|
||||
case EXTENDED_OPERATION_SERVER:
|
||||
case EXTENDED_OPERATION_TYPE:
|
||||
case EXTENDED_OPERATION_INSTANCE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -288,10 +335,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
terser.addElement(operation, "name", methodBinding.getQueryName());
|
||||
terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName));
|
||||
}
|
||||
} else {
|
||||
|
||||
resourceIncludes.addAll(methodBinding.getIncludes());
|
||||
|
||||
}
|
||||
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||
|
@ -306,54 +349,101 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
|
||||
}
|
||||
|
||||
ISearchParamRetriever searchParamRetriever = mySearchParamRetriever;
|
||||
if (searchParamRetriever == null && myServerConfiguration != null) {
|
||||
ISearchParamRetriever searchParamRetriever;
|
||||
if (mySearchParamRetriever != null) {
|
||||
searchParamRetriever = mySearchParamRetriever;
|
||||
} else if (myServerConfiguration != null) {
|
||||
searchParamRetriever = myServerConfiguration;
|
||||
} else if (searchParamRetriever == null) {
|
||||
} else {
|
||||
searchParamRetriever = myServer.createConfiguration();
|
||||
}
|
||||
|
||||
Map<String, RuntimeSearchParam> searchParams = searchParamRetriever.getActiveSearchParams(resourceName);
|
||||
if (searchParams != null) {
|
||||
for (RuntimeSearchParam next : searchParams.values()) {
|
||||
IBase searchParam = terser.addElement(resource, "searchParam");
|
||||
terser.addElement(searchParam, "name", next.getName());
|
||||
terser.addElement(searchParam, "type", next.getParamType().getCode());
|
||||
if (isNotBlank(next.getDescription())) {
|
||||
terser.addElement(searchParam, "documentation", next.getDescription());
|
||||
}
|
||||
|
||||
String spUri = next.getUri();
|
||||
if (isBlank(spUri) && servletRequest != null) {
|
||||
String id;
|
||||
if (next.getId() != null) {
|
||||
id = next.getId().toUnqualifiedVersionless().getValue();
|
||||
} else {
|
||||
id = resourceName + "-" + next.getName();
|
||||
}
|
||||
spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id;
|
||||
}
|
||||
if (isNotBlank(spUri)) {
|
||||
terser.addElement(searchParam, "definition", spUri);
|
||||
}
|
||||
for (RuntimeSearchParam next : searchParams.values()) {
|
||||
IBase searchParam = terser.addElement(resource, "searchParam");
|
||||
terser.addElement(searchParam, "name", next.getName());
|
||||
terser.addElement(searchParam, "type", next.getParamType().getCode());
|
||||
if (isNotBlank(next.getDescription())) {
|
||||
terser.addElement(searchParam, "documentation", next.getDescription());
|
||||
}
|
||||
|
||||
if (resourceIncludes.isEmpty()) {
|
||||
for (String nextInclude : searchParams.values().stream().filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE).map(t -> t.getName()).sorted().collect(Collectors.toList())) {
|
||||
terser.addElement(resource, "searchInclude", nextInclude);
|
||||
String spUri = next.getUri();
|
||||
if (isBlank(spUri) && servletRequest != null) {
|
||||
String id;
|
||||
if (next.getId() != null) {
|
||||
id = next.getId().toUnqualifiedVersionless().getValue();
|
||||
} else {
|
||||
id = resourceName + "-" + next.getName();
|
||||
}
|
||||
spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id;
|
||||
}
|
||||
if (isNotBlank(spUri)) {
|
||||
terser.addElement(searchParam, "definition", spUri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add Include to CapabilityStatement.rest.resource
|
||||
NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName);
|
||||
if (resourceIncludes.isEmpty()) {
|
||||
List<String> includes = searchParams
|
||||
.values()
|
||||
.stream()
|
||||
.filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
|
||||
.map(t -> resourceName + ":" + t.getName())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
terser.addElement(resource, "searchInclude", "*");
|
||||
for (String nextInclude : includes) {
|
||||
terser.addElement(resource, "searchInclude", nextInclude);
|
||||
}
|
||||
} else {
|
||||
for (String resourceInclude : resourceIncludes) {
|
||||
terser.addElement(resource, "searchInclude", resourceInclude);
|
||||
}
|
||||
}
|
||||
|
||||
// Add RevInclude to CapabilityStatement.rest.resource
|
||||
if (myRestResourceRevIncludesEnabled) {
|
||||
NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
|
||||
if (resourceRevIncludes.isEmpty()) {
|
||||
TreeSet<String> revIncludes = new TreeSet<>();
|
||||
for (String nextResourceName : resourceToMethods.keySet()) {
|
||||
if (isBlank(nextResourceName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (RuntimeSearchParam t : searchParamRetriever
|
||||
.getActiveSearchParams(nextResourceName)
|
||||
.values()) {
|
||||
if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
|
||||
if (isNotBlank(t.getName())) {
|
||||
boolean appropriateTarget = false;
|
||||
if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) {
|
||||
appropriateTarget = true;
|
||||
}
|
||||
|
||||
if (appropriateTarget) {
|
||||
revIncludes.add(nextResourceName + ":" + t.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String nextInclude : revIncludes) {
|
||||
terser.addElement(resource, "searchRevInclude", nextInclude);
|
||||
}
|
||||
} else {
|
||||
for (String resourceInclude : resourceRevIncludes) {
|
||||
terser.addElement(resource, "searchRevInclude", resourceInclude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add SupportedProfile to CapabilityStatement.rest.resource
|
||||
for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
|
||||
terser.addElement(resource, "supportedProfile", supportedProfile);
|
||||
}
|
||||
|
||||
for (String resourceInclude : resourceIncludes) {
|
||||
terser.addElement(resource, "searchInclude", resourceInclude);
|
||||
}
|
||||
|
||||
} else {
|
||||
for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
|
||||
checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
|
||||
|
@ -385,7 +475,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
|
||||
if (allStructureDefinitions != null) {
|
||||
for (IBaseResource next : allStructureDefinitions) {
|
||||
String id = next.getIdElement().getValue();
|
||||
String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
|
||||
String url = terser.getSinglePrimitiveValueOrNull(next, "url");
|
||||
String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
|
||||
|
@ -610,4 +699,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
|||
// ignore
|
||||
}
|
||||
|
||||
public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) {
|
||||
myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,17 +22,21 @@ package ca.uhn.fhir.rest.server.util;
|
|||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ISearchParamRetriever {
|
||||
/**
|
||||
* @return Returns {@literal null} if no match
|
||||
*/
|
||||
@Nullable
|
||||
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
|
||||
|
||||
/**
|
||||
* @return Returns all active search params for the given resource
|
||||
*/
|
||||
@Nonnull
|
||||
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
|
||||
|
||||
}
|
||||
|
|
|
@ -53,7 +53,8 @@ public class ConformanceMethodBindingTest {
|
|||
sleepAtLeast(20);
|
||||
|
||||
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
|
||||
verify(provider, times(2)).getServerConformance(any(), any());
|
||||
|
||||
verify(provider, timeout(10000).times(2)).getServerConformance(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.rest.annotation.Metadata;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
|
||||
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class CapabilityStatementCacheR4Test {
|
||||
|
||||
private final FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4);
|
||||
|
||||
@RegisterExtension
|
||||
protected final RestfulServerExtension myServerExtension = new RestfulServerExtension(myFhirContext)
|
||||
.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))
|
||||
.withServer(t -> t.setServerConformanceProvider(new MyCapabilityStatementProvider(t)));
|
||||
|
||||
@Test
|
||||
public void testCacheThreadShutsDownWhenServerShutsDown() throws Exception {
|
||||
CapabilityStatement response = myServerExtension.getFhirClient().capabilities().ofType(CapabilityStatement.class).execute();
|
||||
sleepAtLeast(20);
|
||||
CapabilityStatement response2 = myServerExtension.getFhirClient().capabilities().ofType(CapabilityStatement.class).execute();
|
||||
CapabilityStatement response3 = myServerExtension.getFhirClient().capabilities().ofType(CapabilityStatement.class).execute();
|
||||
CapabilityStatement response4 = myServerExtension.getFhirClient().capabilities().ofType(CapabilityStatement.class).execute();
|
||||
|
||||
assertEquals(response.getId(), response2.getId());
|
||||
|
||||
List<String> threadNames = Thread.getAllStackTraces().keySet().stream().map(t -> t.getName()).filter(t -> t.startsWith(ConformanceMethodBinding.CACHE_THREAD_PREFIX)).sorted().collect(Collectors.toList());
|
||||
assertEquals(1, threadNames.size());
|
||||
|
||||
// Shut down the server
|
||||
myServerExtension.shutDownServer();
|
||||
|
||||
await().until(() -> Thread.getAllStackTraces().keySet().stream().map(t -> t.getName()).filter(t -> t.startsWith(ConformanceMethodBinding.CACHE_THREAD_PREFIX)).sorted().collect(Collectors.toList()), empty());
|
||||
}
|
||||
|
||||
private static class MyCapabilityStatementProvider extends ServerCapabilityStatementProvider {
|
||||
|
||||
public MyCapabilityStatementProvider(RestfulServer theServer) {
|
||||
super(theServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Metadata(cacheMillis = 10)
|
||||
public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
|
||||
return super.getServerConformance(theRequest, theRequestDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
public static void sleepAtLeast(long theMillis) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() <= start + theMillis) {
|
||||
try {
|
||||
long timeSinceStarted = System.currentTimeMillis() - start;
|
||||
long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
|
||||
Thread.sleep(timeToSleep);
|
||||
} catch (InterruptedException theE) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -308,6 +308,25 @@ public class ResponseHighlightingInterceptorTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceHtmlTurtle() throws Exception {
|
||||
String url = "http://localhost:" + ourPort + "/Patient/1?_format=html/turtle";
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
|
||||
|
||||
CloseableHttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
status.close();
|
||||
ourLog.info(responseContent);
|
||||
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
|
||||
assertThat(responseContent, containsString("html"));
|
||||
assertThat(responseContent, containsString("<span class='hlQuot'>"urn:hapitest:mrns"</span>"));
|
||||
assertThat(responseContent, containsString(Constants.HEADER_REQUEST_ID));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceHtmlJsonWithAdditionalParts() throws Exception {
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("html/json; fhirVersion=1.0"));
|
||||
|
|
|
@ -598,6 +598,7 @@ public class ServerCapabilityStatementProviderR5Test {
|
|||
rsNoType.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
||||
conformance.setId("");
|
||||
String confNoType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
|
||||
ourLog.info(confNoType);
|
||||
|
||||
|
@ -615,6 +616,7 @@ public class ServerCapabilityStatementProviderR5Test {
|
|||
rsWithType.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
||||
conformanceWithType.setId("");
|
||||
String confWithType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType);
|
||||
ourLog.info(confWithType);
|
||||
|
||||
|
|
|
@ -171,4 +171,8 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall
|
|||
public RestfulServerExtension registerInterceptor(Object theInterceptor) {
|
||||
return withServer(t -> t.getInterceptorService().registerInterceptor(theInterceptor));
|
||||
}
|
||||
|
||||
public void shutDownServer() throws Exception {
|
||||
JettyUtil.closeServer(myServer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,6 +285,18 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.jena</groupId>
|
||||
<artifactId>apache-jena-libs</artifactId>
|
||||
<scope>test</scope>
|
||||
<type>pom</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.github.jsonld-java</groupId>
|
||||
<artifactId>jsonld-java</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.param.DateParam;
|
|||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
|
@ -45,6 +46,7 @@ import ca.uhn.fhir.validation.ValidationResult;
|
|||
import com.google.common.collect.Lists;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
|
||||
|
@ -54,12 +56,12 @@ import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResource
|
|||
import org.hl7.fhir.r4.model.CapabilityStatement.ConditionalDeleteStatus;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement.SystemRestfulInteraction;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement.TypeRestfulInteraction;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
import org.hl7.fhir.r4.model.DiagnosticReport;
|
||||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.OperationDefinition;
|
||||
import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
|
||||
import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
|
||||
|
@ -76,6 +78,7 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -139,6 +142,33 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
return resource;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormats() throws ServletException {
|
||||
RestfulServer rs = new RestfulServer(myCtx);
|
||||
rs.setProviders(new ConditionalProvider());
|
||||
|
||||
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
|
||||
rs.setServerConformanceProvider(sc);
|
||||
|
||||
rs.init(createServletConfig());
|
||||
|
||||
CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
|
||||
List<String> formats = cs
|
||||
.getFormat()
|
||||
.stream()
|
||||
.map(t -> t.getCode())
|
||||
.collect(Collectors.toList());
|
||||
assertThat(formats.toString(), formats, containsInAnyOrder(
|
||||
"application/fhir+xml",
|
||||
"xml",
|
||||
"application/fhir+json",
|
||||
"json",
|
||||
"application/x-turtle",
|
||||
"ttl"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testConditionalOperations() throws Exception {
|
||||
|
||||
|
@ -629,6 +659,7 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
rsNoType.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
||||
conformance.setId("");
|
||||
String confNoType = validate(conformance);
|
||||
|
||||
RestfulServer rsWithType = new RestfulServer(myCtx) {
|
||||
|
@ -645,6 +676,7 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
rsWithType.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
||||
conformanceWithType.setId("");
|
||||
String confWithType = validate(conformanceWithType);
|
||||
|
||||
assertEquals(confNoType, confWithType);
|
||||
|
@ -860,6 +892,106 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
assertThat(patientResource.getProfile(), containsString(PATIENT_SUB));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevIncludes_Explicit() throws Exception {
|
||||
|
||||
class PatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<Patient> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set<Include> theRevIncludes) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ObservationResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<Observation> getResourceType() {
|
||||
return Observation.class;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RestfulServer rs = new RestfulServer(myCtx);
|
||||
rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider());
|
||||
|
||||
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
|
||||
sc.setRestResourceRevIncludesEnabled(true);
|
||||
rs.setServerConformanceProvider(sc);
|
||||
|
||||
rs.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
|
||||
ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance));
|
||||
|
||||
List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource();
|
||||
CapabilityStatementRestResourceComponent patientResource = resources.stream()
|
||||
.filter(resource -> "Patient".equals(resource.getType()))
|
||||
.findFirst().get();
|
||||
assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevIncludes_Inferred() throws Exception {
|
||||
|
||||
class PatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<Patient> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> search(@IncludeParam(reverse = true) Set<Include> theRevIncludes) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ObservationResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<Observation> getResourceType() {
|
||||
return Observation.class;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RestfulServer rs = new RestfulServer(myCtx);
|
||||
rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider());
|
||||
|
||||
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
|
||||
sc.setRestResourceRevIncludesEnabled(true);
|
||||
rs.setServerConformanceProvider(sc);
|
||||
|
||||
rs.init(createServletConfig());
|
||||
|
||||
CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
|
||||
ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance));
|
||||
|
||||
List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource();
|
||||
CapabilityStatementRestResourceComponent patientResource = resources.stream()
|
||||
.filter(resource -> "Patient".equals(resource.getType()))
|
||||
.findFirst().get();
|
||||
assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject"));
|
||||
}
|
||||
|
||||
private List<String> toOperationIdParts(List<CapabilityStatementRestResourceOperationComponent> theOperation) {
|
||||
ArrayList<String> retVal = Lists.newArrayList();
|
||||
for (CapabilityStatementRestResourceOperationComponent next : theOperation) {
|
||||
|
@ -876,14 +1008,6 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
return retVal;
|
||||
}
|
||||
|
||||
private Set<String> toStrings(List<CodeType> theType) {
|
||||
HashSet<String> retVal = new HashSet<String>();
|
||||
for (CodeType next : theType) {
|
||||
retVal.add(next.getValueAsString());
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private String validate(IBaseResource theResource) {
|
||||
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource);
|
||||
ourLog.info("Def:\n{}", conf);
|
||||
|
@ -1262,6 +1386,14 @@ public class ServerCapabilityStatementProviderR4Test {
|
|||
public static class PatientTripleSub extends PatientSubSub {
|
||||
}
|
||||
|
||||
private static Set<String> toStrings(Collection<? extends IPrimitiveType> theType) {
|
||||
HashSet<String> retVal = new HashSet<String>();
|
||||
for (IPrimitiveType next : theType) {
|
||||
retVal.add(next.getValueAsString());
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
|
|
|
@ -112,23 +112,17 @@ public class ${className}ResourceProvider extends
|
|||
@RawParam
|
||||
Map<String, List<String>> theAdditionalRawParams,
|
||||
|
||||
#if ( $version != 'dstu' )
|
||||
@IncludeParam(reverse=true)
|
||||
Set<Include> theRevIncludes,
|
||||
@Description(shortDefinition="Only return resources which were last updated as specified by the given range")
|
||||
@OptionalParam(name="_lastUpdated")
|
||||
DateRangeParam theLastUpdated,
|
||||
#end
|
||||
|
||||
@IncludeParam(allow= {
|
||||
#foreach ( $include in $includes )
|
||||
"${include.path}",
|
||||
#end
|
||||
"*"
|
||||
})
|
||||
@IncludeParam
|
||||
Set<Include> theIncludes,
|
||||
|
||||
@Sort
|
||||
|
||||
@IncludeParam(reverse=true)
|
||||
Set<Include> theRevIncludes,
|
||||
|
||||
@Sort
|
||||
SortSpec theSort,
|
||||
|
||||
@ca.uhn.fhir.rest.annotation.Count
|
||||
|
|
Loading…
Reference in New Issue