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 Map<FhirVersionEnum, FhirContext> ourStaticContexts = Collections.synchronizedMap(new EnumMap<>(FhirVersionEnum.class));
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
|
||||||
private final IFhirVersion myVersion;
|
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 AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM;
|
||||||
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap();
|
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap();
|
||||||
private ArrayList<Class<? extends IBase>> myCustomTypes;
|
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 Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap();
|
||||||
private volatile boolean myInitialized;
|
private volatile boolean myInitialized;
|
||||||
private volatile boolean myInitializing = false;
|
private volatile boolean myInitializing = false;
|
||||||
|
@ -115,13 +117,14 @@ public class FhirContext {
|
||||||
private volatile INarrativeGenerator myNarrativeGenerator;
|
private volatile INarrativeGenerator myNarrativeGenerator;
|
||||||
private volatile IParserErrorHandler myParserErrorHandler = new LenientErrorHandler();
|
private volatile IParserErrorHandler myParserErrorHandler = new LenientErrorHandler();
|
||||||
private ParserOptions myParserOptions = new ParserOptions();
|
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 IRestfulClientFactory myRestfulClientFactory;
|
||||||
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
|
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
|
||||||
private IValidationSupport myValidationSupport;
|
private IValidationSupport myValidationSupport;
|
||||||
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
|
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
|
||||||
private volatile Set<String> myResourceNames;
|
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
|
* @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 !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() {
|
public IVersionSpecificBundleFactory newBundleFactory() {
|
||||||
return myVersion.newBundleFactory(this);
|
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
|
* 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
|
* without incurring any performance penalty
|
||||||
* </p>
|
* </p>
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public IParser newRDFParser() {
|
public IParser newRDFParser() {
|
||||||
return new RDFParser(this, myParserErrorHandler, Lang.TURTLE);
|
return new RDFParser(this, myParserErrorHandler, Lang.TURTLE);
|
||||||
|
@ -989,6 +1036,24 @@ public class FhirContext {
|
||||||
return "FhirContext[" + myVersion.getVersion().name() + "]";
|
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}
|
* Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2}
|
||||||
*/
|
*/
|
||||||
|
@ -1066,11 +1131,4 @@ public class FhirContext {
|
||||||
}
|
}
|
||||||
return retVal;
|
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
|
@VisibleForTesting
|
||||||
public void unregisterAllInterceptors() {
|
public void unregisterAllInterceptors() {
|
||||||
synchronized (myRegistryMutex) {
|
synchronized (myRegistryMutex) {
|
||||||
myAnonymousInvokers.clear();
|
unregisterInterceptors(myAnonymousInvokers.values());
|
||||||
myGlobalInvokers.clear();
|
unregisterInterceptors(myGlobalInvokers.values());
|
||||||
myInterceptors.clear();
|
unregisterInterceptors(myInterceptors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
|
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
|
||||||
if (theInterceptors != null) {
|
if (theInterceptors != null) {
|
||||||
theInterceptors.forEach(t -> unregisterInterceptor(t));
|
new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,4 +125,11 @@ public class CacheControlDirective {
|
||||||
|
|
||||||
return this;
|
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_HTML = "html";
|
||||||
public static final String FORMAT_JSON = "json";
|
public static final String FORMAT_JSON = "json";
|
||||||
public static final String FORMAT_XML = "xml";
|
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 Set<String> FORMATS_HTML;
|
||||||
public static final String FORMATS_HTML_JSON = "html/json";
|
public static final String FORMATS_HTML_JSON = "html/json";
|
||||||
public static final String FORMATS_HTML_XML = "html/xml";
|
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 = "Accept";
|
||||||
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
|
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";
|
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
|
@Override
|
||||||
public IParser newParser(FhirContext theContext) {
|
public IParser newParser(FhirContext theContext) {
|
||||||
return theContext.newRDFParser();
|
return theContext.newRDFParser();
|
||||||
|
@ -114,6 +114,7 @@ public enum EncodingEnum {
|
||||||
ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
|
ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
|
||||||
ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
|
ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
|
||||||
ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
|
ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
|
||||||
|
ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF);
|
||||||
|
|
||||||
ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy);
|
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
|
@Autowired
|
||||||
protected FhirContext myContext;
|
protected FhirContext myContext;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IBulkExportJobDao myBulkExportJobDao;
|
|
||||||
@Autowired
|
|
||||||
protected SearchBuilderFactory mySearchBuilderFactory;
|
protected SearchBuilderFactory mySearchBuilderFactory;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private IBulkExportJobDao myBulkExportJobDao;
|
||||||
|
@Autowired
|
||||||
private MatchUrlService myMatchUrlService;
|
private MatchUrlService myMatchUrlService;
|
||||||
|
|
||||||
private ISearchBuilder mySearchBuilder;
|
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.client.interceptor.LoggingInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
||||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
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;
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.context.ContextLoader;
|
import org.springframework.web.context.ContextLoader;
|
||||||
|
@ -52,7 +53,6 @@ import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
import static org.slf4j.LoggerFactory.getLogger;
|
|
||||||
|
|
||||||
public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
||||||
protected static SearchParamRegistryImpl ourSearchParamRegistry;
|
protected static SearchParamRegistryImpl ourSearchParamRegistry;
|
||||||
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
|
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
|
||||||
protected static Server ourServer;
|
protected static Server ourServer;
|
||||||
|
protected static JpaCapabilityStatementProvider ourCapabilityStatementProvider;
|
||||||
private static DatabaseBackedPagingProvider ourPagingProvider;
|
private static DatabaseBackedPagingProvider ourPagingProvider;
|
||||||
private static GenericWebApplicationContext ourWebApplicationContext;
|
private static GenericWebApplicationContext ourWebApplicationContext;
|
||||||
protected IGenericClient myClient;
|
protected IGenericClient myClient;
|
||||||
|
@ -153,9 +154,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
||||||
ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class);
|
ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class);
|
||||||
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
|
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
|
||||||
|
|
||||||
JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
|
ourCapabilityStatementProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
|
||||||
confProvider.setImplementationDescription("THIS IS THE DESC");
|
ourCapabilityStatementProvider.setImplementationDescription("THIS IS THE DESC");
|
||||||
ourRestServer.setServerConformanceProvider(confProvider);
|
ourRestServer.setServerConformanceProvider(ourCapabilityStatementProvider);
|
||||||
|
|
||||||
server.setHandler(proxyHandler);
|
server.setHandler(proxyHandler);
|
||||||
JettyUtil.startServer(server);
|
JettyUtil.startServer(server);
|
||||||
|
@ -179,6 +180,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
ourRestServer.setPagingProvider(ourPagingProvider);
|
ourRestServer.setPagingProvider(ourPagingProvider);
|
||||||
|
ourRestServer.registerInterceptor(new ResponseHighlighterInterceptor());
|
||||||
|
|
||||||
myClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
|
myClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
|
||||||
if (shouldLogClient()) {
|
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() {
|
protected boolean shouldLogClient() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
package ca.uhn.fhir.jpa.provider.r4;
|
package ca.uhn.fhir.jpa.provider.r4;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
|
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.CapabilityStatement;
|
||||||
import org.hl7.fhir.r4.model.Enumerations;
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.SearchParameter;
|
import org.hl7.fhir.r4.model.SearchParameter;
|
||||||
import org.hl7.fhir.r4.model.StructureDefinition;
|
import org.hl7.fhir.r4.model.StructureDefinition;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -15,8 +20,11 @@ import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
import static org.hamcrest.Matchers.hasItems;
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test {
|
public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test {
|
||||||
|
@ -32,7 +40,7 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCustomSearchParamsReflected() {
|
public void testCustomSearchParamsReflectedInSearchParams() {
|
||||||
SearchParameter fooSp = new SearchParameter();
|
SearchParameter fooSp = new SearchParameter();
|
||||||
fooSp.addBase("Patient");
|
fooSp.addBase("Patient");
|
||||||
fooSp.setCode("foo");
|
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
|
@Test
|
||||||
public void testRegisteredProfilesReflected_StoredInServer() throws IOException {
|
public void testRegisteredProfilesReflected_StoredInServer() throws IOException {
|
||||||
StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json");
|
StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json");
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -94,6 +95,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
||||||
requiresActiveSearchParams();
|
requiresActiveSearchParams();
|
||||||
|
@ -122,7 +124,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
||||||
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
||||||
|
|
||||||
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
||||||
int size = allSearchParamsBp.size();
|
int size = allSearchParamsBp.sizeOrThrowNpe();
|
||||||
|
|
||||||
ourLog.trace("Loaded {} search params from the DB", size);
|
ourLog.trace("Loaded {} search params from the DB", size);
|
||||||
|
|
||||||
|
|
|
@ -322,14 +322,32 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
|
||||||
invokeDestroy(iResourceProvider);
|
invokeDestroy(iResourceProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myServerConformanceProvider != null) {
|
if (myServerConformanceProvider != null) {
|
||||||
invokeDestroy(myServerConformanceProvider);
|
invokeDestroy(myServerConformanceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getPlainProviders() != null) {
|
if (getPlainProviders() != null) {
|
||||||
for (Object next : getPlainProviders()) {
|
for (Object next : getPlainProviders()) {
|
||||||
invokeDestroy(next);
|
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.rest.server.util.ISearchParamRetriever;
|
||||||
import ca.uhn.fhir.util.VersionUtil;
|
import ca.uhn.fhir.util.VersionUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -278,7 +280,7 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
|
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()) {
|
for (ResourceBinding next : getResourceBindings()) {
|
||||||
String resourceName = next.getResourceName();
|
String resourceName = next.getResourceName();
|
||||||
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
|
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
|
||||||
|
@ -365,14 +367,15 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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<>();
|
Map<String, RuntimeSearchParam> retVal = new LinkedHashMap<>();
|
||||||
|
|
||||||
collectMethodBindings()
|
collectMethodBindings()
|
||||||
.getOrDefault(theResourceName, Collections.emptyList())
|
.getOrDefault(theResourceName, Collections.emptyList())
|
||||||
.stream()
|
.stream()
|
||||||
.filter(t -> t.getResourceName().equals(theResourceName))
|
.filter(t -> theResourceName.equals(t.getResourceName()))
|
||||||
.filter(t -> t instanceof SearchMethodBinding)
|
.filter(t -> t instanceof SearchMethodBinding)
|
||||||
.map(t -> (SearchMethodBinding) t)
|
.map(t -> (SearchMethodBinding) t)
|
||||||
.filter(t -> t.getQueryName() == null)
|
.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.BaseServerResponseException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
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.StopWatch;
|
||||||
import ca.uhn.fhir.util.UrlUtil;
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.text.StringEscapeUtils;
|
import org.apache.commons.text.StringEscapeUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
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.IBaseOperationOutcome;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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
|
* #%L
|
||||||
|
@ -71,10 +83,10 @@ public class ResponseHighlighterInterceptor {
|
||||||
*/
|
*/
|
||||||
public static final String PARAM_RAW = "_raw";
|
public static final String PARAM_RAW = "_raw";
|
||||||
public static final String PARAM_RAW_TRUE = "true";
|
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 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_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_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 myShowRequestHeaders = false;
|
||||||
private boolean myShowResponseHeaders = true;
|
private boolean myShowResponseHeaders = true;
|
||||||
|
|
||||||
|
@ -128,6 +140,9 @@ public class ResponseHighlighterInterceptor {
|
||||||
boolean inValue = false;
|
boolean inValue = false;
|
||||||
boolean inQuote = false;
|
boolean inQuote = false;
|
||||||
boolean inTag = false;
|
boolean inTag = false;
|
||||||
|
boolean inTurtleDirective = false;
|
||||||
|
boolean startingLineNext = true;
|
||||||
|
boolean startingLine = false;
|
||||||
int lineCount = 1;
|
int lineCount = 1;
|
||||||
|
|
||||||
for (int i = 0; i < str.length(); i++) {
|
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) : ' ';
|
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
|
||||||
|
|
||||||
if (nextChar == '\n') {
|
if (nextChar == '\n') {
|
||||||
|
if (inTurtleDirective) {
|
||||||
|
theTarget.append("</span>");
|
||||||
|
inTurtleDirective = false;
|
||||||
|
}
|
||||||
lineCount++;
|
lineCount++;
|
||||||
theTarget.append("</div><div id=\"line");
|
theTarget.append("</div><div id=\"line");
|
||||||
theTarget.append(lineCount);
|
theTarget.append(lineCount);
|
||||||
theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
|
theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
|
||||||
theTarget.append(lineCount);
|
theTarget.append(lineCount);
|
||||||
theTarget.append("');\">");
|
theTarget.append("');\">");
|
||||||
|
startingLineNext = true;
|
||||||
continue;
|
continue;
|
||||||
|
} else if (startingLineNext) {
|
||||||
|
startingLineNext = false;
|
||||||
|
startingLine = true;
|
||||||
|
} else {
|
||||||
|
startingLine = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theEncodingEnum == EncodingEnum.JSON) {
|
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 {
|
} else {
|
||||||
|
|
||||||
|
// Ok it's XML
|
||||||
|
|
||||||
if (inQuote) {
|
if (inQuote) {
|
||||||
theTarget.append(nextChar);
|
theTarget.append(nextChar);
|
||||||
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
|
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
|
||||||
|
@ -345,6 +408,26 @@ public class ResponseHighlighterInterceptor {
|
||||||
return false;
|
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) {
|
private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) {
|
||||||
/*
|
/*
|
||||||
* Request for _raw
|
* Request for _raw
|
||||||
|
@ -373,6 +456,9 @@ public class ResponseHighlighterInterceptor {
|
||||||
} else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) {
|
} else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) {
|
||||||
force = true;
|
force = true;
|
||||||
theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON);
|
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 {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -542,8 +628,6 @@ public class ResponseHighlighterInterceptor {
|
||||||
outputBuffer.append(" position: relative;\n");
|
outputBuffer.append(" position: relative;\n");
|
||||||
outputBuffer.append("}");
|
outputBuffer.append("}");
|
||||||
outputBuffer.append(".responseBodyTableFirstColumn {");
|
outputBuffer.append(".responseBodyTableFirstColumn {");
|
||||||
// outputBuffer.append(" position: absolute;\n");
|
|
||||||
// outputBuffer.append(" width: 70px;\n");
|
|
||||||
outputBuffer.append("}");
|
outputBuffer.append("}");
|
||||||
outputBuffer.append(".responseBodyTableSecondColumn {");
|
outputBuffer.append(".responseBodyTableSecondColumn {");
|
||||||
outputBuffer.append(" position: absolute;\n");
|
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("This result is being rendered in HTML for easy viewing. ");
|
||||||
outputBuffer.append("You may access this content as ");
|
outputBuffer.append("You may access this content as ");
|
||||||
|
|
||||||
outputBuffer.append("<a href=\"");
|
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
|
||||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
|
outputBuffer.append("<a href=\"");
|
||||||
outputBuffer.append("\">Raw JSON</a> or ");
|
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
|
||||||
|
outputBuffer.append("\">Raw JSON</a> or ");
|
||||||
|
}
|
||||||
|
|
||||||
outputBuffer.append("<a href=\"");
|
if (theRequestDetails.getFhirContext().isFormatXmlSupported()) {
|
||||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
|
outputBuffer.append("<a href=\"");
|
||||||
outputBuffer.append("\">Raw XML</a>, ");
|
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("view this content in ");
|
||||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
|
|
||||||
outputBuffer.append("\">HTML JSON</a> ");
|
|
||||||
|
|
||||||
outputBuffer.append("or ");
|
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
|
||||||
outputBuffer.append("<a href=\"");
|
outputBuffer.append("<a href=\"");
|
||||||
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
|
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
|
||||||
outputBuffer.append("\">HTML XML</a>.");
|
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);
|
Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
|
||||||
|
@ -680,7 +787,7 @@ public class ResponseHighlighterInterceptor {
|
||||||
outputBuffer.append("\n");
|
outputBuffer.append("\n");
|
||||||
|
|
||||||
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
|
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());
|
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
|
||||||
outputBuffer.append("<script type=\"text/javascript\">");
|
outputBuffer.append("<script type=\"text/javascript\">");
|
||||||
outputBuffer.append(jsStr);
|
outputBuffer.append(jsStr);
|
||||||
|
|
|
@ -138,10 +138,21 @@ public abstract class BaseMethodBinding<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getIncludes() {
|
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) {
|
for (IParameter next : myParameters) {
|
||||||
if (next instanceof IncludeParameter) {
|
if (next instanceof IncludeParameter) {
|
||||||
retVal.addAll(((IncludeParameter) next).getAllow());
|
IncludeParameter includeParameter = (IncludeParameter) next;
|
||||||
|
if (includeParameter.isReverse() == reverse) {
|
||||||
|
retVal.addAll(includeParameter.getAllow());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
|
@ -313,6 +324,10 @@ public abstract class BaseMethodBinding<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
// subclasses may override
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
|
public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
|
||||||
Read read = theMethod.getAnnotation(Read.class);
|
Read read = theMethod.getAnnotation(Read.class);
|
||||||
|
|
|
@ -42,10 +42,16 @@ import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.lang.reflect.Method;
|
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.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
|
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
|
||||||
|
public static final String CACHE_THREAD_PREFIX = "capabilitystatement-cache-";
|
||||||
/*
|
/*
|
||||||
* Note: This caching mechanism should probably be configurable and maybe
|
* Note: This caching mechanism should probably be configurable and maybe
|
||||||
* even applicable to other bindings. It's particularly important for this
|
* 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 AtomicReference<IBaseConformance> myCachedResponse = new AtomicReference<>();
|
||||||
private final AtomicLong myCachedResponseExpires = new AtomicLong(0L);
|
private final AtomicLong myCachedResponseExpires = new AtomicLong(0L);
|
||||||
|
private final ExecutorService myThreadPool;
|
||||||
private long myCacheMillis = 60 * 1000;
|
private long myCacheMillis = 60 * 1000;
|
||||||
|
|
||||||
ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
|
ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
|
||||||
|
@ -69,6 +76,17 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
||||||
setCacheMillis(metadata.cacheMillis());
|
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;
|
return ReturnTypeEnum.RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
super.close();
|
||||||
|
|
||||||
|
myThreadPool.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
|
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
|
||||||
IBaseConformance conf;
|
IBaseConformance conf;
|
||||||
|
@ -116,7 +141,8 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
|
||||||
if (conf != null) {
|
if (conf != null) {
|
||||||
long expires = myCachedResponseExpires.get();
|
long expires = myCachedResponseExpires.get();
|
||||||
if (expires < System.currentTimeMillis()) {
|
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) {
|
if (conf == null) {
|
||||||
conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams);
|
conf = createCapabilityStatement(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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SimpleBundleProvider(conf);
|
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
|
@Override
|
||||||
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
|
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
|
||||||
if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) {
|
if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) {
|
||||||
|
|
|
@ -67,6 +67,10 @@ class IncludeParameter extends BaseQueryParameter {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReverse() {
|
||||||
|
return myReverse;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
|
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.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
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.model.primitive.InstantDt;
|
||||||
import ca.uhn.fhir.parser.DataFormatException;
|
import ca.uhn.fhir.parser.DataFormatException;
|
||||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||||
|
@ -45,8 +44,10 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.NavigableSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
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 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 static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
|
||||||
private final FhirContext myContext;
|
private final FhirContext myContext;
|
||||||
private final RestfulServer myServer;
|
private final RestfulServer myServer;
|
||||||
|
@ -88,6 +90,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
private final RestfulServerConfiguration myServerConfiguration;
|
private final RestfulServerConfiguration myServerConfiguration;
|
||||||
private final IValidationSupport myValidationSupport;
|
private final IValidationSupport myValidationSupport;
|
||||||
private String myPublisher = "Not provided";
|
private String myPublisher = "Not provided";
|
||||||
|
private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -189,6 +192,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
|
|
||||||
TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
|
TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
|
||||||
|
|
||||||
|
terser.addElement(retVal, "id", UUID.randomUUID().toString());
|
||||||
terser.addElement(retVal, "name", "RestServer");
|
terser.addElement(retVal, "name", "RestServer");
|
||||||
terser.addElement(retVal, "publisher", myPublisher);
|
terser.addElement(retVal, "publisher", myPublisher);
|
||||||
terser.addElement(retVal, "date", conformanceDate(configuration));
|
terser.addElement(retVal, "date", conformanceDate(configuration));
|
||||||
|
@ -201,10 +205,18 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
terser.addElement(retVal, "kind", "instance");
|
terser.addElement(retVal, "kind", "instance");
|
||||||
terser.addElement(retVal, "software.name", configuration.getServerName());
|
terser.addElement(retVal, "software.name", configuration.getServerName());
|
||||||
terser.addElement(retVal, "software.version", configuration.getServerVersion());
|
terser.addElement(retVal, "software.version", configuration.getServerVersion());
|
||||||
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
|
if (myContext.isFormatXmlSupported()) {
|
||||||
terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
|
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
|
||||||
terser.addElement(retVal, "format", Constants.FORMAT_JSON);
|
terser.addElement(retVal, "format", Constants.FORMAT_XML);
|
||||||
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");
|
terser.addElement(retVal, "status", "active");
|
||||||
|
|
||||||
IBase rest = terser.addElement(retVal, "rest");
|
IBase rest = terser.addElement(retVal, "rest");
|
||||||
|
@ -216,13 +228,25 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
|
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
|
||||||
Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype();
|
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()) {
|
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
||||||
|
|
||||||
|
String resourceName = nextEntry.getKey();
|
||||||
if (nextEntry.getKey().isEmpty() == false) {
|
if (nextEntry.getKey().isEmpty() == false) {
|
||||||
Set<String> resourceOps = new HashSet<>();
|
Set<String> resourceOps = new HashSet<>();
|
||||||
Set<String> resourceIncludes = new HashSet<>();
|
|
||||||
IBase resource = terser.addElement(rest, "resource");
|
IBase resource = terser.addElement(rest, "resource");
|
||||||
String resourceName = nextEntry.getKey();
|
|
||||||
|
|
||||||
postProcessRestResource(terser, resource, resourceName);
|
postProcessRestResource(terser, resource, resourceName);
|
||||||
|
|
||||||
|
@ -272,6 +296,29 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
case UPDATE:
|
case UPDATE:
|
||||||
terser.setElement(resource, "conditionalUpdate", "true");
|
terser.setElement(resource, "conditionalUpdate", "true");
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -288,10 +335,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
terser.addElement(operation, "name", methodBinding.getQueryName());
|
terser.addElement(operation, "name", methodBinding.getQueryName());
|
||||||
terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName));
|
terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
|
||||||
resourceIncludes.addAll(methodBinding.getIncludes());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||||
|
@ -306,54 +349,101 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ISearchParamRetriever searchParamRetriever = mySearchParamRetriever;
|
ISearchParamRetriever searchParamRetriever;
|
||||||
if (searchParamRetriever == null && myServerConfiguration != null) {
|
if (mySearchParamRetriever != null) {
|
||||||
|
searchParamRetriever = mySearchParamRetriever;
|
||||||
|
} else if (myServerConfiguration != null) {
|
||||||
searchParamRetriever = myServerConfiguration;
|
searchParamRetriever = myServerConfiguration;
|
||||||
} else if (searchParamRetriever == null) {
|
} else {
|
||||||
searchParamRetriever = myServer.createConfiguration();
|
searchParamRetriever = myServer.createConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, RuntimeSearchParam> searchParams = searchParamRetriever.getActiveSearchParams(resourceName);
|
Map<String, RuntimeSearchParam> searchParams = searchParamRetriever.getActiveSearchParams(resourceName);
|
||||||
if (searchParams != null) {
|
for (RuntimeSearchParam next : searchParams.values()) {
|
||||||
for (RuntimeSearchParam next : searchParams.values()) {
|
IBase searchParam = terser.addElement(resource, "searchParam");
|
||||||
IBase searchParam = terser.addElement(resource, "searchParam");
|
terser.addElement(searchParam, "name", next.getName());
|
||||||
terser.addElement(searchParam, "name", next.getName());
|
terser.addElement(searchParam, "type", next.getParamType().getCode());
|
||||||
terser.addElement(searchParam, "type", next.getParamType().getCode());
|
if (isNotBlank(next.getDescription())) {
|
||||||
if (isNotBlank(next.getDescription())) {
|
terser.addElement(searchParam, "documentation", 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceIncludes.isEmpty()) {
|
String spUri = next.getUri();
|
||||||
for (String nextInclude : searchParams.values().stream().filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE).map(t -> t.getName()).sorted().collect(Collectors.toList())) {
|
if (isBlank(spUri) && servletRequest != null) {
|
||||||
terser.addElement(resource, "searchInclude", nextInclude);
|
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)) {
|
for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
|
||||||
terser.addElement(resource, "supportedProfile", supportedProfile);
|
terser.addElement(resource, "supportedProfile", supportedProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String resourceInclude : resourceIncludes) {
|
|
||||||
terser.addElement(resource, "searchInclude", resourceInclude);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
|
for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
|
||||||
checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
|
checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
|
||||||
|
@ -385,7 +475,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
|
List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
|
||||||
if (allStructureDefinitions != null) {
|
if (allStructureDefinitions != null) {
|
||||||
for (IBaseResource next : allStructureDefinitions) {
|
for (IBaseResource next : allStructureDefinitions) {
|
||||||
String id = next.getIdElement().getValue();
|
|
||||||
String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
|
String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
|
||||||
String url = terser.getSinglePrimitiveValueOrNull(next, "url");
|
String url = terser.getSinglePrimitiveValueOrNull(next, "url");
|
||||||
String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
|
String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
|
||||||
|
@ -610,4 +699,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
|
||||||
// ignore
|
// 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 ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ISearchParamRetriever {
|
public interface ISearchParamRetriever {
|
||||||
/**
|
/**
|
||||||
* @return Returns {@literal null} if no match
|
* @return Returns {@literal null} if no match
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
|
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Returns all active search params for the given resource
|
* @return Returns all active search params for the given resource
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
|
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,8 @@ public class ConformanceMethodBindingTest {
|
||||||
sleepAtLeast(20);
|
sleepAtLeast(20);
|
||||||
|
|
||||||
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
|
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
|
@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
|
@Test
|
||||||
public void testForceHtmlJsonWithAdditionalParts() throws Exception {
|
public void testForceHtmlJsonWithAdditionalParts() throws Exception {
|
||||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("html/json; fhirVersion=1.0"));
|
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());
|
rsNoType.init(createServletConfig());
|
||||||
|
|
||||||
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
||||||
|
conformance.setId("");
|
||||||
String confNoType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
|
String confNoType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
|
||||||
ourLog.info(confNoType);
|
ourLog.info(confNoType);
|
||||||
|
|
||||||
|
@ -615,6 +616,7 @@ public class ServerCapabilityStatementProviderR5Test {
|
||||||
rsWithType.init(createServletConfig());
|
rsWithType.init(createServletConfig());
|
||||||
|
|
||||||
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
||||||
|
conformanceWithType.setId("");
|
||||||
String confWithType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType);
|
String confWithType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType);
|
||||||
ourLog.info(confWithType);
|
ourLog.info(confWithType);
|
||||||
|
|
||||||
|
|
|
@ -171,4 +171,8 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall
|
||||||
public RestfulServerExtension registerInterceptor(Object theInterceptor) {
|
public RestfulServerExtension registerInterceptor(Object theInterceptor) {
|
||||||
return withServer(t -> t.getInterceptorService().registerInterceptor(theInterceptor));
|
return withServer(t -> t.getInterceptorService().registerInterceptor(theInterceptor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void shutDownServer() throws Exception {
|
||||||
|
JettyUtil.closeServer(myServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,6 +285,18 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
</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.DateRangeParam;
|
||||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
|
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.StringParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
|
@ -45,6 +46,7 @@ import ca.uhn.fhir.validation.ValidationResult;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
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.Bundle;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
|
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.ConditionalDeleteStatus;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement.SystemRestfulInteraction;
|
import org.hl7.fhir.r4.model.CapabilityStatement.SystemRestfulInteraction;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement.TypeRestfulInteraction;
|
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.DateType;
|
||||||
import org.hl7.fhir.r4.model.DiagnosticReport;
|
import org.hl7.fhir.r4.model.DiagnosticReport;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
import org.hl7.fhir.r4.model.Encounter;
|
||||||
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
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;
|
||||||
import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
|
import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
|
||||||
import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
|
import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
|
||||||
|
@ -76,6 +78,7 @@ import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -139,6 +142,33 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
return resource;
|
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
|
@Test
|
||||||
public void testConditionalOperations() throws Exception {
|
public void testConditionalOperations() throws Exception {
|
||||||
|
|
||||||
|
@ -629,6 +659,7 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
rsNoType.init(createServletConfig());
|
rsNoType.init(createServletConfig());
|
||||||
|
|
||||||
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
|
||||||
|
conformance.setId("");
|
||||||
String confNoType = validate(conformance);
|
String confNoType = validate(conformance);
|
||||||
|
|
||||||
RestfulServer rsWithType = new RestfulServer(myCtx) {
|
RestfulServer rsWithType = new RestfulServer(myCtx) {
|
||||||
|
@ -645,6 +676,7 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
rsWithType.init(createServletConfig());
|
rsWithType.init(createServletConfig());
|
||||||
|
|
||||||
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
|
||||||
|
conformanceWithType.setId("");
|
||||||
String confWithType = validate(conformanceWithType);
|
String confWithType = validate(conformanceWithType);
|
||||||
|
|
||||||
assertEquals(confNoType, confWithType);
|
assertEquals(confNoType, confWithType);
|
||||||
|
@ -860,6 +892,106 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
assertThat(patientResource.getProfile(), containsString(PATIENT_SUB));
|
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) {
|
private List<String> toOperationIdParts(List<CapabilityStatementRestResourceOperationComponent> theOperation) {
|
||||||
ArrayList<String> retVal = Lists.newArrayList();
|
ArrayList<String> retVal = Lists.newArrayList();
|
||||||
for (CapabilityStatementRestResourceOperationComponent next : theOperation) {
|
for (CapabilityStatementRestResourceOperationComponent next : theOperation) {
|
||||||
|
@ -876,14 +1008,6 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
return retVal;
|
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) {
|
private String validate(IBaseResource theResource) {
|
||||||
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource);
|
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource);
|
||||||
ourLog.info("Def:\n{}", conf);
|
ourLog.info("Def:\n{}", conf);
|
||||||
|
@ -1262,6 +1386,14 @@ public class ServerCapabilityStatementProviderR4Test {
|
||||||
public static class PatientTripleSub extends PatientSubSub {
|
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
|
@AfterAll
|
||||||
public static void afterClassClearContext() {
|
public static void afterClassClearContext() {
|
||||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
|
|
@ -112,22 +112,16 @@ public class ${className}ResourceProvider extends
|
||||||
@RawParam
|
@RawParam
|
||||||
Map<String, List<String>> theAdditionalRawParams,
|
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")
|
@Description(shortDefinition="Only return resources which were last updated as specified by the given range")
|
||||||
@OptionalParam(name="_lastUpdated")
|
@OptionalParam(name="_lastUpdated")
|
||||||
DateRangeParam theLastUpdated,
|
DateRangeParam theLastUpdated,
|
||||||
#end
|
|
||||||
|
|
||||||
@IncludeParam(allow= {
|
@IncludeParam
|
||||||
#foreach ( $include in $includes )
|
|
||||||
"${include.path}",
|
|
||||||
#end
|
|
||||||
"*"
|
|
||||||
})
|
|
||||||
Set<Include> theIncludes,
|
Set<Include> theIncludes,
|
||||||
|
|
||||||
|
@IncludeParam(reverse=true)
|
||||||
|
Set<Include> theRevIncludes,
|
||||||
|
|
||||||
@Sort
|
@Sort
|
||||||
SortSpec theSort,
|
SortSpec theSort,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue