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:
James Agnew 2021-04-05 08:29:04 -04:00 committed by GitHub
parent b27559caa4
commit 0bf746f23b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 842 additions and 147 deletions

View File

@ -101,10 +101,12 @@ public class FhirContext {
private static final Map<FhirVersionEnum, FhirContext> ourStaticContexts = Collections.synchronizedMap(new EnumMap<>(FhirVersionEnum.class));
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
private final IFhirVersion myVersion;
private final Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<>();
private final Set<PerformanceOptionsEnum> myPerformanceOptions = new HashSet<>();
private final Collection<Class<? extends IBaseResource>> myResourceTypesToScan;
private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM;
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap();
private ArrayList<Class<? extends IBase>> myCustomTypes;
private final Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<>();
private volatile Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap();
private volatile boolean myInitialized;
private volatile boolean myInitializing = false;
@ -115,13 +117,14 @@ public class FhirContext {
private volatile INarrativeGenerator myNarrativeGenerator;
private volatile IParserErrorHandler myParserErrorHandler = new LenientErrorHandler();
private ParserOptions myParserOptions = new ParserOptions();
private final Set<PerformanceOptionsEnum> myPerformanceOptions = new HashSet<>();
private final Collection<Class<? extends IBaseResource>> myResourceTypesToScan;
private volatile IRestfulClientFactory myRestfulClientFactory;
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
private IValidationSupport myValidationSupport;
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
private volatile Set<String> myResourceNames;
private volatile Boolean myFormatXmlSupported;
private volatile Boolean myFormatJsonSupported;
private volatile Boolean myFormatRdfSupported;
/**
* @deprecated It is recommended that you use one of the static initializer methods instead
@ -673,6 +676,51 @@ public class FhirContext {
return !myDefaultTypeForProfile.isEmpty();
}
/**
* @return Returns <code>true</code> if the XML serialization format is supported, based on the
* available libraries on the classpath.
*
* @since 5.4.0
*/
public boolean isFormatXmlSupported() {
Boolean retVal = myFormatXmlSupported;
if (retVal == null) {
retVal = tryToInitParser(() -> newXmlParser());
myFormatXmlSupported = retVal;
}
return retVal;
}
/**
* @return Returns <code>true</code> if the JSON serialization format is supported, based on the
* available libraries on the classpath.
*
* @since 5.4.0
*/
public boolean isFormatJsonSupported() {
Boolean retVal = myFormatJsonSupported;
if (retVal == null) {
retVal = tryToInitParser(() -> newJsonParser());
myFormatJsonSupported = retVal;
}
return retVal;
}
/**
* @return Returns <code>true</code> if the RDF serialization format is supported, based on the
* available libraries on the classpath.
*
* @since 5.4.0
*/
public boolean isFormatRdfSupported() {
Boolean retVal = myFormatRdfSupported;
if (retVal == null) {
retVal = tryToInitParser(() -> newRDFParser());
myFormatRdfSupported = retVal;
}
return retVal;
}
public IVersionSpecificBundleFactory newBundleFactory() {
return myVersion.newBundleFactory(this);
}
@ -735,7 +783,6 @@ public class FhirContext {
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every message being processed
* without incurring any performance penalty
* </p>
*
*/
public IParser newRDFParser() {
return new RDFParser(this, myParserErrorHandler, Lang.TURTLE);
@ -989,6 +1036,24 @@ public class FhirContext {
return "FhirContext[" + myVersion.getVersion().name() + "]";
}
// TODO KHS add the other primitive types
public IPrimitiveType<Boolean> getPrimitiveBoolean(Boolean theValue) {
IPrimitiveType<Boolean> retval = (IPrimitiveType<Boolean>) getElementDefinition("boolean").newInstance();
retval.setValue(theValue);
return retval;
}
private static boolean tryToInitParser(Runnable run) {
boolean retVal;
try {
run.run();
retVal = true;
} catch (Exception | NoClassDefFoundError e) {
retVal = false;
}
return retVal;
}
/**
* Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2}
*/
@ -1066,11 +1131,4 @@ public class FhirContext {
}
return retVal;
}
// TODO KHS add the other primitive types
public IPrimitiveType<Boolean> getPrimitiveBoolean(Boolean theValue) {
IPrimitiveType<Boolean> retval = (IPrimitiveType<Boolean>) getElementDefinition("boolean").newInstance();
retval.setValue(theValue);
return retval;
}
}

View File

@ -142,16 +142,16 @@ public abstract class BaseInterceptorService<POINTCUT extends IPointcut> impleme
@VisibleForTesting
public void unregisterAllInterceptors() {
synchronized (myRegistryMutex) {
myAnonymousInvokers.clear();
myGlobalInvokers.clear();
myInterceptors.clear();
unregisterInterceptors(myAnonymousInvokers.values());
unregisterInterceptors(myGlobalInvokers.values());
unregisterInterceptors(myInterceptors);
}
}
@Override
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
if (theInterceptors != null) {
theInterceptors.forEach(t -> unregisterInterceptor(t));
new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t));
}
}

View File

@ -125,4 +125,11 @@ public class CacheControlDirective {
return this;
}
/**
* Convenience factory method for a no-cache directivel
*/
public static CacheControlDirective noCache() {
return new CacheControlDirective().setNoCache(true);
}
}

View File

@ -85,7 +85,8 @@ public class Constants {
public static final String FORMAT_HTML = "html";
public static final String FORMAT_JSON = "json";
public static final String FORMAT_XML = "xml";
public static final String FORMAT_TURTLE = "text/turtle";
public static final String CT_RDF_TURTLE_LEGACY = "text/turtle";
public static final String FORMAT_TURTLE = "ttl";
/**
@ -94,6 +95,7 @@ public class Constants {
public static final Set<String> FORMATS_HTML;
public static final String FORMATS_HTML_JSON = "html/json";
public static final String FORMATS_HTML_XML = "html/xml";
public static final String FORMATS_HTML_TTL = "html/turtle";
public static final String HEADER_ACCEPT = "Accept";
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String HEADER_ACCEPT_VALUE_JSON_NON_LEGACY = CT_FHIR_JSON_NEW + ";q=1.0, " + CT_FHIR_JSON + ";q=0.9";

View File

@ -46,7 +46,7 @@ public enum EncodingEnum {
}
},
RDF(Constants.CT_RDF_TURTLE, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
RDF(Constants.CT_RDF_TURTLE_LEGACY, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
@Override
public IParser newParser(FhirContext theContext) {
return theContext.newRDFParser();
@ -114,6 +114,7 @@ public enum EncodingEnum {
ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF);
ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy);

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2521
title: "The automatically generated CapabilityStatement for R4+ will now incude the list of supported
revinclude values."

View File

@ -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."

View File

@ -67,10 +67,10 @@ public abstract class BaseBulkItemReader implements ItemReader<List<ResourcePers
@Autowired
protected FhirContext myContext;
@Autowired
private IBulkExportJobDao myBulkExportJobDao;
@Autowired
protected SearchBuilderFactory mySearchBuilderFactory;
@Autowired
private IBulkExportJobDao myBulkExportJobDao;
@Autowired
private MatchUrlService myMatchUrlService;
private ISearchBuilder mySearchBuilder;

View File

@ -22,6 +22,7 @@ import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -34,8 +35,8 @@ import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.ContextLoader;
@ -52,7 +53,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.slf4j.LoggerFactory.getLogger;
public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
@ -64,6 +64,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected static SearchParamRegistryImpl ourSearchParamRegistry;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
protected static Server ourServer;
protected static JpaCapabilityStatementProvider ourCapabilityStatementProvider;
private static DatabaseBackedPagingProvider ourPagingProvider;
private static GenericWebApplicationContext ourWebApplicationContext;
protected IGenericClient myClient;
@ -153,9 +154,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class);
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
confProvider.setImplementationDescription("THIS IS THE DESC");
ourRestServer.setServerConformanceProvider(confProvider);
ourCapabilityStatementProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport);
ourCapabilityStatementProvider.setImplementationDescription("THIS IS THE DESC");
ourRestServer.setServerConformanceProvider(ourCapabilityStatementProvider);
server.setHandler(proxyHandler);
JettyUtil.startServer(server);
@ -179,6 +180,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
}
ourRestServer.setPagingProvider(ourPagingProvider);
ourRestServer.registerInterceptor(new ResponseHighlighterInterceptor());
myClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
if (shouldLogClient()) {
@ -186,13 +188,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
}
}
protected static void clearRestfulServer() throws Exception {
if (ourServer != null) {
JettyUtil.closeServer(ourServer);
}
ourServer = null;
}
protected boolean shouldLogClient() {
return true;
}

View File

@ -1,13 +1,18 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import java.io.IOException;
@ -15,8 +20,11 @@ import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test {
@ -32,7 +40,7 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
}
@Test
public void testCustomSearchParamsReflected() {
public void testCustomSearchParamsReflectedInSearchParams() {
SearchParameter fooSp = new SearchParameter();
fooSp.addBase("Patient");
fooSp.setCode("foo");
@ -57,6 +65,101 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
}
@Override
@AfterEach
public void after() throws Exception {
super.after();
ourCapabilityStatementProvider.setRestResourceRevIncludesEnabled(ServerCapabilityStatementProvider.DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED);
}
@Test
public void testFormats() {
CapabilityStatement cs = myClient
.capabilities()
.ofType(CapabilityStatement.class)
.cacheControl(CacheControlDirective.noCache())
.execute();
List<String> formats = cs
.getFormat()
.stream()
.map(t -> t.getCode())
.collect(Collectors.toList());
assertThat(formats.toString(), formats, containsInAnyOrder(
"application/fhir+xml",
"application/fhir+json",
"json",
"xml",
"html/xml",
"html/json"));
}
@Test
public void testCustomSearchParamsReflectedInIncludesAndRevIncludes_TargetSpecified() {
SearchParameter fooSp = new SearchParameter();
fooSp.addBase("Observation");
fooSp.setCode("foo");
fooSp.setUrl("http://acme.com/foo");
fooSp.setType(Enumerations.SearchParamType.REFERENCE);
fooSp.setTitle("FOO SP");
fooSp.setDescription("This is a search param!");
fooSp.setExpression("Observation.subject");
fooSp.addTarget("Patient");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(fooSp);
mySearchParamRegistry.forceRefresh();
ourCapabilityStatementProvider.setRestResourceRevIncludesEnabled(true);
CapabilityStatement cs = myClient
.capabilities()
.ofType(CapabilityStatement.class)
.cacheControl(CacheControlDirective.noCache())
.execute();
List<String> includes = findIncludes(cs, "Patient");
assertThat(includes.toString(), includes, contains("*", "Patient:general-practitioner", "Patient:link", "Patient:organization"));
includes = findIncludes(cs, "Observation");
assertThat(includes.toString(), includes, contains("*", "Observation:based-on", "Observation:derived-from", "Observation:device", "Observation:encounter", "Observation:focus", "Observation:foo", "Observation:has-member", "Observation:part-of", "Observation:patient", "Observation:performer", "Observation:specimen", "Observation:subject"));
List<String> revIncludes = findRevIncludes(cs, "Patient");
assertThat(revIncludes.toString(), revIncludes, hasItems(
"Account:patient", // Standard SP reference
"Observation:foo", // Standard SP reference with no explicit target
"Provenance:entity" // Reference in custom SP
));
assertThat(revIncludes.toString(), revIncludes, not(hasItem(
"CarePlan:based-on" // Standard SP reference with non-matching target
)));
}
@Nonnull
private List<String> findIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) {
return theCapabilityStatement
.getRest()
.stream()
.flatMap(t -> t.getResource().stream())
.filter(t -> t.getType().equals(theResourceName))
.flatMap(t -> t.getSearchInclude().stream())
.map(t -> t.getValue())
.collect(Collectors.toList());
}
@Nonnull
private List<String> findRevIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) {
return theCapabilityStatement
.getRest()
.stream()
.flatMap(t -> t.getResource().stream())
.filter(t -> t.getType().equals(theResourceName))
.flatMap(t -> t.getSearchRevInclude().stream())
.map(t -> t.getValue())
.collect(Collectors.toList());
}
@Test
public void testRegisteredProfilesReflected_StoredInServer() throws IOException {
StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json");

View File

@ -46,6 +46,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
@ -94,6 +95,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
}
}
@Nonnull
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
requiresActiveSearchParams();
@ -122,7 +124,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
int size = allSearchParamsBp.size();
int size = allSearchParamsBp.sizeOrThrowNpe();
ourLog.trace("Loaded {} search params from the DB", size);

View File

@ -322,14 +322,32 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
invokeDestroy(iResourceProvider);
}
}
if (myServerConformanceProvider != null) {
invokeDestroy(myServerConformanceProvider);
}
if (getPlainProviders() != null) {
for (Object next : getPlainProviders()) {
invokeDestroy(next);
}
}
if (myServerConformanceMethod != null) {
myServerConformanceMethod.close();
}
myResourceNameToBinding
.values()
.stream()
.flatMap(t->t.getMethodBindings().stream())
.forEach(t->t.close());
myGlobalBinding
.getMethodBindings()
.forEach(t->t.close());
myServerBinding
.getMethodBindings()
.forEach(t->t.close());
}
/**

View File

@ -31,12 +31,14 @@ import ca.uhn.fhir.rest.server.method.SearchParameter;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import ca.uhn.fhir.util.VersionUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -278,7 +280,7 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
}
public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<>();
for (ResourceBinding next : getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
@ -365,14 +367,15 @@ public class RestfulServerConfiguration implements ISearchParamRetriever {
}
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
public Map<String, RuntimeSearchParam> getActiveSearchParams(@Nonnull String theResourceName) {
Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
Map<String, RuntimeSearchParam> retVal = new LinkedHashMap<>();
collectMethodBindings()
.getOrDefault(theResourceName, Collections.emptyList())
.stream()
.filter(t -> t.getResourceName().equals(theResourceName))
.filter(t -> theResourceName.equals(t.getResourceName()))
.filter(t -> t instanceof SearchMethodBinding)
.map(t -> (SearchMethodBinding) t)
.filter(t -> t.getQueryName() == null)

View File

@ -18,23 +18,35 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
/*
* #%L
@ -71,10 +83,10 @@ public class ResponseHighlighterInterceptor {
*/
public static final String PARAM_RAW = "_raw";
public static final String PARAM_RAW_TRUE = "true";
public static final String PARAM_TRUE = "true";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class);
private static final String[] PARAM_FORMAT_VALUE_JSON = new String[]{Constants.FORMAT_JSON};
private static final String[] PARAM_FORMAT_VALUE_XML = new String[]{Constants.FORMAT_XML};
private static final String[] PARAM_FORMAT_VALUE_TTL = new String[]{Constants.FORMAT_TURTLE};
private boolean myShowRequestHeaders = false;
private boolean myShowResponseHeaders = true;
@ -128,6 +140,9 @@ public class ResponseHighlighterInterceptor {
boolean inValue = false;
boolean inQuote = false;
boolean inTag = false;
boolean inTurtleDirective = false;
boolean startingLineNext = true;
boolean startingLine = false;
int lineCount = 1;
for (int i = 0; i < str.length(); i++) {
@ -140,13 +155,23 @@ public class ResponseHighlighterInterceptor {
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
if (nextChar == '\n') {
if (inTurtleDirective) {
theTarget.append("</span>");
inTurtleDirective = false;
}
lineCount++;
theTarget.append("</div><div id=\"line");
theTarget.append(lineCount);
theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
theTarget.append(lineCount);
theTarget.append("');\">");
startingLineNext = true;
continue;
} else if (startingLineNext) {
startingLineNext = false;
startingLine = true;
} else {
startingLine = false;
}
if (theEncodingEnum == EncodingEnum.JSON) {
@ -194,7 +219,45 @@ public class ResponseHighlighterInterceptor {
}
}
} else if (theEncodingEnum == EncodingEnum.RDF) {
if (inQuote) {
theTarget.append(nextChar);
if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
theTarget.append("quot;</span>");
i += 5;
inQuote = false;
} else if (nextChar == '\\' && nextChar2 == '"') {
theTarget.append("quot;</span>");
i += 5;
inQuote = false;
}
} else if (startingLine && nextChar == '@') {
inTurtleDirective = true;
theTarget.append("<span class='hlTagName'>");
theTarget.append(nextChar);
} else if (startingLine) {
inTurtleDirective = true;
theTarget.append("<span class='hlTagName'>");
theTarget.append(nextChar);
} else if (nextChar == '[' || nextChar == ']' || nextChar == ';' || nextChar == ':') {
theTarget.append("<span class='hlControl'>");
theTarget.append(nextChar);
theTarget.append("</span>");
} else {
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
theTarget.append("<span class='hlQuot'>&quot;");
inQuote = true;
i += 5;
} else {
theTarget.append(nextChar);
}
}
} else {
// Ok it's XML
if (inQuote) {
theTarget.append(nextChar);
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
@ -345,6 +408,26 @@ public class ResponseHighlighterInterceptor {
return false;
}
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
public void capabilityStatementGenerated(RequestDetails theRequestDetails, IBaseConformance theCapabilityStatement) {
FhirTerser terser = theRequestDetails.getFhirContext().newTerser();
Set<String> formats = terser.getValues(theCapabilityStatement, "format", IPrimitiveType.class)
.stream()
.map(t -> t.getValueAsString())
.collect(Collectors.toSet());
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_JSON_NEW, Constants.FORMATS_HTML_JSON);
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_XML_NEW, Constants.FORMATS_HTML_XML);
addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_RDF_TURTLE, Constants.FORMATS_HTML_TTL);
}
private void addFormatConditionally(IBaseConformance theCapabilityStatement, FhirTerser terser, Set<String> formats, String wanted, String toAdd) {
if (formats.contains(wanted)) {
terser.addElement(theCapabilityStatement, "format", toAdd);
}
}
private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) {
/*
* Request for _raw
@ -373,6 +456,9 @@ public class ResponseHighlighterInterceptor {
} else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) {
force = true;
theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON);
} else if (Constants.FORMATS_HTML_TTL.equals(formatParam)) {
force = true;
theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_TTL);
} else {
return true;
}
@ -542,8 +628,6 @@ public class ResponseHighlighterInterceptor {
outputBuffer.append(" position: relative;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTableFirstColumn {");
// outputBuffer.append(" position: absolute;\n");
// outputBuffer.append(" width: 70px;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTableSecondColumn {");
outputBuffer.append(" position: absolute;\n");
@ -589,24 +673,47 @@ public class ResponseHighlighterInterceptor {
outputBuffer.append("This result is being rendered in HTML for easy viewing. ");
outputBuffer.append("You may access this content as ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
outputBuffer.append("\">Raw JSON</a> or ");
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
outputBuffer.append("\">Raw JSON</a> or ");
}
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
outputBuffer.append("\">Raw XML</a>, ");
if (theRequestDetails.getFhirContext().isFormatXmlSupported()) {
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
outputBuffer.append("\">Raw XML</a> or ");
}
outputBuffer.append(" or view this content in ");
if (theRequestDetails.getFhirContext().isFormatRdfSupported()) {
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_TURTLE));
outputBuffer.append("\">Raw Turtle</a> or ");
}
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
outputBuffer.append("\">HTML JSON</a> ");
outputBuffer.append("view this content in ");
outputBuffer.append("or ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
outputBuffer.append("\">HTML XML</a>.");
if (theRequestDetails.getFhirContext().isFormatJsonSupported()) {
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
outputBuffer.append("\">HTML JSON</a> ");
}
if (theRequestDetails.getFhirContext().isFormatXmlSupported()) {
outputBuffer.append("or ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
outputBuffer.append("\">HTML XML</a> ");
}
if (theRequestDetails.getFhirContext().isFormatRdfSupported()) {
outputBuffer.append("or ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_TTL));
outputBuffer.append("\">HTML Turtle</a> ");
}
outputBuffer.append(".");
}
Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
@ -680,7 +787,7 @@ public class ResponseHighlighterInterceptor {
outputBuffer.append("\n");
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
String jsStr = jsStream != null ? IOUtils.toString(jsStream, StandardCharsets.UTF_8) : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
outputBuffer.append("<script type=\"text/javascript\">");
outputBuffer.append(jsStr);

View File

@ -138,10 +138,21 @@ public abstract class BaseMethodBinding<T> {
}
public Set<String> getIncludes() {
Set<String> retVal = new TreeSet<String>();
return doGetIncludesOrRevIncludes(false);
}
public Set<String> getRevIncludes() {
return doGetIncludesOrRevIncludes(true);
}
private Set<String> doGetIncludesOrRevIncludes(boolean reverse) {
Set<String> retVal = new TreeSet<>();
for (IParameter next : myParameters) {
if (next instanceof IncludeParameter) {
retVal.addAll(((IncludeParameter) next).getAllow());
IncludeParameter includeParameter = (IncludeParameter) next;
if (includeParameter.isReverse() == reverse) {
retVal.addAll(includeParameter.getAllow());
}
}
}
return retVal;
@ -313,6 +324,10 @@ public abstract class BaseMethodBinding<T> {
}
}
public void close() {
// subclasses may override
}
@SuppressWarnings("unchecked")
public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
Read read = theMethod.getAnnotation(Read.class);

View File

@ -42,10 +42,16 @@ import org.hl7.fhir.instance.model.api.IBaseConformance;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
public static final String CACHE_THREAD_PREFIX = "capabilitystatement-cache-";
/*
* Note: This caching mechanism should probably be configurable and maybe
* even applicable to other bindings. It's particularly important for this
@ -53,6 +59,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
*/
private final AtomicReference<IBaseConformance> myCachedResponse = new AtomicReference<>();
private final AtomicLong myCachedResponseExpires = new AtomicLong(0L);
private final ExecutorService myThreadPool;
private long myCacheMillis = 60 * 1000;
ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -69,6 +76,17 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
setCacheMillis(metadata.cacheMillis());
}
ThreadFactory threadFactory = r -> {
Thread t = new Thread(r);
t.setName(CACHE_THREAD_PREFIX + t.getId());
t.setDaemon(false);
return t;
};
myThreadPool = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
threadFactory,
new ThreadPoolExecutor.DiscardOldestPolicy());
}
/**
@ -100,6 +118,13 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
return ReturnTypeEnum.RESOURCE;
}
@Override
public void close() {
super.close();
myThreadPool.shutdown();
}
@Override
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
IBaseConformance conf;
@ -116,7 +141,8 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
if (conf != null) {
long expires = myCachedResponseExpires.get();
if (expires < System.currentTimeMillis()) {
conf = null;
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
myThreadPool.submit(() -> createCapabilityStatement(theRequest, theMethodParams));
}
}
}
@ -141,32 +167,36 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
}
if (conf == null) {
conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams);
if (myCacheMillis > 0) {
// Interceptor hook: SERVER_CAPABILITY_STATEMENT_GENERATED
if (theRequest.getInterceptorBroadcaster() != null) {
HookParams params = new HookParams();
params.add(IBaseConformance.class, conf);
params.add(RequestDetails.class, theRequest);
params.addIfMatchesType(ServletRequestDetails.class, theRequest);
IBaseConformance outcome = (IBaseConformance) theRequest
.getInterceptorBroadcaster()
.callHooksAndReturnObject(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED, params);
if (outcome != null) {
conf = outcome;
}
}
myCachedResponse.set(conf);
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
}
conf = createCapabilityStatement(theRequest, theMethodParams);
}
return new SimpleBundleProvider(conf);
}
private IBaseConformance createCapabilityStatement(RequestDetails theRequest, Object[] theMethodParams) {
IBaseConformance conf = (IBaseConformance) invokeServerMethod(theRequest, theMethodParams);
// Interceptor hook: SERVER_CAPABILITY_STATEMENT_GENERATED
if (theRequest.getInterceptorBroadcaster() != null) {
HookParams params = new HookParams();
params.add(IBaseConformance.class, conf);
params.add(RequestDetails.class, theRequest);
params.addIfMatchesType(ServletRequestDetails.class, theRequest);
IBaseConformance outcome = (IBaseConformance) theRequest
.getInterceptorBroadcaster()
.callHooksAndReturnObject(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED, params);
if (outcome != null) {
conf = outcome;
}
}
if (myCacheMillis > 0) {
myCachedResponse.set(conf);
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
}
return conf;
}
@Override
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) {

View File

@ -67,6 +67,10 @@ class IncludeParameter extends BaseQueryParameter {
}
public boolean isReverse() {
return myReverse;
}
@SuppressWarnings("unchecked")
@Override
public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {

View File

@ -4,7 +4,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.annotation.IdParam;
@ -45,8 +44,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -81,6 +82,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> {
public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true;
private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
private final FhirContext myContext;
private final RestfulServer myServer;
@ -88,6 +90,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
private final RestfulServerConfiguration myServerConfiguration;
private final IValidationSupport myValidationSupport;
private String myPublisher = "Not provided";
private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED;
/**
* Constructor
@ -189,6 +192,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
terser.addElement(retVal, "id", UUID.randomUUID().toString());
terser.addElement(retVal, "name", "RestServer");
terser.addElement(retVal, "publisher", myPublisher);
terser.addElement(retVal, "date", conformanceDate(configuration));
@ -201,10 +205,18 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
terser.addElement(retVal, "kind", "instance");
terser.addElement(retVal, "software.name", configuration.getServerName());
terser.addElement(retVal, "software.version", configuration.getServerVersion());
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
terser.addElement(retVal, "format", Constants.FORMAT_JSON);
terser.addElement(retVal, "format", Constants.FORMAT_XML);
if (myContext.isFormatXmlSupported()) {
terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
terser.addElement(retVal, "format", Constants.FORMAT_XML);
}
if (myContext.isFormatJsonSupported()) {
terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
terser.addElement(retVal, "format", Constants.FORMAT_JSON);
}
if (myContext.isFormatRdfSupported()) {
terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE);
terser.addElement(retVal, "format", Constants.FORMAT_TURTLE);
}
terser.addElement(retVal, "status", "active");
IBase rest = terser.addElement(retVal, "rest");
@ -216,13 +228,25 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype();
TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create();
TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
String resourceName = nextEntry.getKey();
for (BaseMethodBinding<?> nextMethod : nextEntry.getValue()) {
if (nextMethod instanceof SearchMethodBinding) {
resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes());
resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes());
}
}
}
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
String resourceName = nextEntry.getKey();
if (nextEntry.getKey().isEmpty() == false) {
Set<String> resourceOps = new HashSet<>();
Set<String> resourceIncludes = new HashSet<>();
IBase resource = terser.addElement(rest, "resource");
String resourceName = nextEntry.getKey();
postProcessRestResource(terser, resource, resourceName);
@ -272,6 +296,29 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
case UPDATE:
terser.setElement(resource, "conditionalUpdate", "true");
break;
case HISTORY_INSTANCE:
case HISTORY_SYSTEM:
case HISTORY_TYPE:
case READ:
case SEARCH_SYSTEM:
case SEARCH_TYPE:
case TRANSACTION:
case VALIDATE:
case VREAD:
case METADATA:
case META_ADD:
case META:
case META_DELETE:
case PATCH:
case BATCH:
case ADD_TAGS:
case DELETE_TAGS:
case GET_TAGS:
case GET_PAGE:
case GRAPHQL_REQUEST:
case EXTENDED_OPERATION_SERVER:
case EXTENDED_OPERATION_TYPE:
case EXTENDED_OPERATION_INSTANCE:
default:
break;
}
@ -288,10 +335,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
terser.addElement(operation, "name", methodBinding.getQueryName());
terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName));
}
} else {
resourceIncludes.addAll(methodBinding.getIncludes());
}
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
@ -306,54 +349,101 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
}
ISearchParamRetriever searchParamRetriever = mySearchParamRetriever;
if (searchParamRetriever == null && myServerConfiguration != null) {
ISearchParamRetriever searchParamRetriever;
if (mySearchParamRetriever != null) {
searchParamRetriever = mySearchParamRetriever;
} else if (myServerConfiguration != null) {
searchParamRetriever = myServerConfiguration;
} else if (searchParamRetriever == null) {
} else {
searchParamRetriever = myServer.createConfiguration();
}
Map<String, RuntimeSearchParam> searchParams = searchParamRetriever.getActiveSearchParams(resourceName);
if (searchParams != null) {
for (RuntimeSearchParam next : searchParams.values()) {
IBase searchParam = terser.addElement(resource, "searchParam");
terser.addElement(searchParam, "name", next.getName());
terser.addElement(searchParam, "type", next.getParamType().getCode());
if (isNotBlank(next.getDescription())) {
terser.addElement(searchParam, "documentation", next.getDescription());
}
String spUri = next.getUri();
if (isBlank(spUri) && servletRequest != null) {
String id;
if (next.getId() != null) {
id = next.getId().toUnqualifiedVersionless().getValue();
} else {
id = resourceName + "-" + next.getName();
}
spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id;
}
if (isNotBlank(spUri)) {
terser.addElement(searchParam, "definition", spUri);
}
for (RuntimeSearchParam next : searchParams.values()) {
IBase searchParam = terser.addElement(resource, "searchParam");
terser.addElement(searchParam, "name", next.getName());
terser.addElement(searchParam, "type", next.getParamType().getCode());
if (isNotBlank(next.getDescription())) {
terser.addElement(searchParam, "documentation", next.getDescription());
}
if (resourceIncludes.isEmpty()) {
for (String nextInclude : searchParams.values().stream().filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE).map(t -> t.getName()).sorted().collect(Collectors.toList())) {
terser.addElement(resource, "searchInclude", nextInclude);
String spUri = next.getUri();
if (isBlank(spUri) && servletRequest != null) {
String id;
if (next.getId() != null) {
id = next.getId().toUnqualifiedVersionless().getValue();
} else {
id = resourceName + "-" + next.getName();
}
spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id;
}
if (isNotBlank(spUri)) {
terser.addElement(searchParam, "definition", spUri);
}
}
// Add Include to CapabilityStatement.rest.resource
NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName);
if (resourceIncludes.isEmpty()) {
List<String> includes = searchParams
.values()
.stream()
.filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
.map(t -> resourceName + ":" + t.getName())
.sorted()
.collect(Collectors.toList());
terser.addElement(resource, "searchInclude", "*");
for (String nextInclude : includes) {
terser.addElement(resource, "searchInclude", nextInclude);
}
} else {
for (String resourceInclude : resourceIncludes) {
terser.addElement(resource, "searchInclude", resourceInclude);
}
}
// Add RevInclude to CapabilityStatement.rest.resource
if (myRestResourceRevIncludesEnabled) {
NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
if (resourceRevIncludes.isEmpty()) {
TreeSet<String> revIncludes = new TreeSet<>();
for (String nextResourceName : resourceToMethods.keySet()) {
if (isBlank(nextResourceName)) {
continue;
}
for (RuntimeSearchParam t : searchParamRetriever
.getActiveSearchParams(nextResourceName)
.values()) {
if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
if (isNotBlank(t.getName())) {
boolean appropriateTarget = false;
if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) {
appropriateTarget = true;
}
if (appropriateTarget) {
revIncludes.add(nextResourceName + ":" + t.getName());
}
}
}
}
}
for (String nextInclude : revIncludes) {
terser.addElement(resource, "searchRevInclude", nextInclude);
}
} else {
for (String resourceInclude : resourceRevIncludes) {
terser.addElement(resource, "searchRevInclude", resourceInclude);
}
}
}
// Add SupportedProfile to CapabilityStatement.rest.resource
for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
terser.addElement(resource, "supportedProfile", supportedProfile);
}
for (String resourceInclude : resourceIncludes) {
terser.addElement(resource, "searchInclude", resourceInclude);
}
} else {
for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
@ -385,7 +475,6 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
if (allStructureDefinitions != null) {
for (IBaseResource next : allStructureDefinitions) {
String id = next.getIdElement().getValue();
String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
String url = terser.getSinglePrimitiveValueOrNull(next, "url");
String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
@ -610,4 +699,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv
// ignore
}
public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) {
myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled;
}
}

View File

@ -22,17 +22,21 @@ package ca.uhn.fhir.rest.server.util;
import ca.uhn.fhir.context.RuntimeSearchParam;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;
public interface ISearchParamRetriever {
/**
* @return Returns {@literal null} if no match
*/
@Nullable
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
/**
* @return Returns all active search params for the given resource
*/
@Nonnull
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
}

View File

@ -53,7 +53,8 @@ public class ConformanceMethodBindingTest {
sleepAtLeast(20);
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(2)).getServerConformance(any(), any());
verify(provider, timeout(10000).times(2)).getServerConformance(any(), any());
}
@Test

View File

@ -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
}
}
}
}

View File

@ -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'>&quot;urn:hapitest:mrns&quot;</span>"));
assertThat(responseContent, containsString(Constants.HEADER_REQUEST_ID));
}
@Test
public void testForceHtmlJsonWithAdditionalParts() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("html/json; fhirVersion=1.0"));

View File

@ -598,6 +598,7 @@ public class ServerCapabilityStatementProviderR5Test {
rsNoType.init(createServletConfig());
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
conformance.setId("");
String confNoType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(confNoType);
@ -615,6 +616,7 @@ public class ServerCapabilityStatementProviderR5Test {
rsWithType.init(createServletConfig());
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
conformanceWithType.setId("");
String confWithType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType);
ourLog.info(confWithType);

View File

@ -171,4 +171,8 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall
public RestfulServerExtension registerInterceptor(Object theInterceptor) {
return withServer(t -> t.getInterceptorService().registerInterceptor(theInterceptor));
}
public void shutDownServer() throws Exception {
JettyUtil.closeServer(myServer);
}
}

View File

@ -285,6 +285,18 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.jena</groupId>
<artifactId>apache-jena-libs</artifactId>
<scope>test</scope>
<type>pom</type>
<exclusions>
<exclusion>
<groupId>com.github.jsonld-java</groupId>
<artifactId>jsonld-java</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
@ -45,6 +46,7 @@ import ca.uhn.fhir.validation.ValidationResult;
import com.google.common.collect.Lists;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
@ -54,12 +56,12 @@ import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResource
import org.hl7.fhir.r4.model.CapabilityStatement.ConditionalDeleteStatus;
import org.hl7.fhir.r4.model.CapabilityStatement.SystemRestfulInteraction;
import org.hl7.fhir.r4.model.CapabilityStatement.TypeRestfulInteraction;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
@ -76,6 +78,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -139,6 +142,33 @@ public class ServerCapabilityStatementProviderR4Test {
return resource;
}
@Test
public void testFormats() throws ServletException {
RestfulServer rs = new RestfulServer(myCtx);
rs.setProviders(new ConditionalProvider());
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
List<String> formats = cs
.getFormat()
.stream()
.map(t -> t.getCode())
.collect(Collectors.toList());
assertThat(formats.toString(), formats, containsInAnyOrder(
"application/fhir+xml",
"xml",
"application/fhir+json",
"json",
"application/x-turtle",
"ttl"
));
}
@Test
public void testConditionalOperations() throws Exception {
@ -629,6 +659,7 @@ public class ServerCapabilityStatementProviderR4Test {
rsNoType.init(createServletConfig());
CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType));
conformance.setId("");
String confNoType = validate(conformance);
RestfulServer rsWithType = new RestfulServer(myCtx) {
@ -645,6 +676,7 @@ public class ServerCapabilityStatementProviderR4Test {
rsWithType.init(createServletConfig());
CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType));
conformanceWithType.setId("");
String confWithType = validate(conformanceWithType);
assertEquals(confNoType, confWithType);
@ -860,6 +892,106 @@ public class ServerCapabilityStatementProviderR4Test {
assertThat(patientResource.getProfile(), containsString(PATIENT_SUB));
}
@Test
public void testRevIncludes_Explicit() throws Exception {
class PatientResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<Patient> search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set<Include> theRevIncludes) {
return Collections.emptyList();
}
}
class ObservationResourceProvider implements IResourceProvider {
@Override
public Class<Observation> getResourceType() {
return Observation.class;
}
@Search
public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) {
return Collections.emptyList();
}
}
RestfulServer rs = new RestfulServer(myCtx);
rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider());
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
sc.setRestResourceRevIncludesEnabled(true);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance));
List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource();
CapabilityStatementRestResourceComponent patientResource = resources.stream()
.filter(resource -> "Patient".equals(resource.getType()))
.findFirst().get();
assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar"));
}
@Test
public void testRevIncludes_Inferred() throws Exception {
class PatientResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<Patient> search(@IncludeParam(reverse = true) Set<Include> theRevIncludes) {
return Collections.emptyList();
}
}
class ObservationResourceProvider implements IResourceProvider {
@Override
public Class<Observation> getResourceType() {
return Observation.class;
}
@Search
public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) {
return Collections.emptyList();
}
}
RestfulServer rs = new RestfulServer(myCtx);
rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider());
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
sc.setRestResourceRevIncludesEnabled(true);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs));
ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance));
List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource();
CapabilityStatementRestResourceComponent patientResource = resources.stream()
.filter(resource -> "Patient".equals(resource.getType()))
.findFirst().get();
assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject"));
}
private List<String> toOperationIdParts(List<CapabilityStatementRestResourceOperationComponent> theOperation) {
ArrayList<String> retVal = Lists.newArrayList();
for (CapabilityStatementRestResourceOperationComponent next : theOperation) {
@ -876,14 +1008,6 @@ public class ServerCapabilityStatementProviderR4Test {
return retVal;
}
private Set<String> toStrings(List<CodeType> theType) {
HashSet<String> retVal = new HashSet<String>();
for (CodeType next : theType) {
retVal.add(next.getValueAsString());
}
return retVal;
}
private String validate(IBaseResource theResource) {
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource);
ourLog.info("Def:\n{}", conf);
@ -1262,6 +1386,14 @@ public class ServerCapabilityStatementProviderR4Test {
public static class PatientTripleSub extends PatientSubSub {
}
private static Set<String> toStrings(Collection<? extends IPrimitiveType> theType) {
HashSet<String> retVal = new HashSet<String>();
for (IPrimitiveType next : theType) {
retVal.add(next.getValueAsString());
}
return retVal;
}
@AfterAll
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -112,23 +112,17 @@ public class ${className}ResourceProvider extends
@RawParam
Map<String, List<String>> theAdditionalRawParams,
#if ( $version != 'dstu' )
@IncludeParam(reverse=true)
Set<Include> theRevIncludes,
@Description(shortDefinition="Only return resources which were last updated as specified by the given range")
@OptionalParam(name="_lastUpdated")
DateRangeParam theLastUpdated,
#end
@IncludeParam(allow= {
#foreach ( $include in $includes )
"${include.path}",
#end
"*"
})
@IncludeParam
Set<Include> theIncludes,
@Sort
@IncludeParam(reverse=true)
Set<Include> theRevIncludes,
@Sort
SortSpec theSort,
@ca.uhn.fhir.rest.annotation.Count